From cf017f81e2822896f9261755b7887d0745c241a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sun, 14 Jun 2026 14:08:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20Phase2=20=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E6=94=B6=E5=B0=BE=EF=BC=88Sprint=201-20=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。 --- ARCHITECTURE.md | 20 +- Cargo.lock | 227 +- PROGRESS.md | 406 +++- crates/df-ai/src/ai_tools.rs | 13 + crates/df-ai/src/anthropic_compat.rs | 375 +++- crates/df-ai/src/context.rs | 494 +++- crates/df-ai/src/coordinator.rs | 2 + crates/df-ai/src/lib.rs | 24 + crates/df-ai/src/openai_compat.rs | 373 ++- crates/df-ai/src/provider.rs | 26 +- crates/df-evolve/Cargo.toml | 12 - crates/df-evolve/src/evolve_engine.rs | 58 - crates/df-evolve/src/knowledge.rs | 98 - crates/df-evolve/src/lib.rs | 12 - crates/df-evolve/src/pattern.rs | 47 - crates/df-evolve/src/prompt_template.rs | 41 - crates/df-evolve/src/review_rule.rs | 45 - crates/df-execute/src/docker.rs | 46 - crates/df-execute/src/git_ops.rs | 47 - crates/df-execute/src/lib.rs | 5 +- crates/df-execute/src/ssh.rs | 38 - crates/df-ideas/src/adversarial.rs | 468 ++-- crates/df-ideas/src/evaluator.rs | 79 - crates/df-ideas/src/graph.rs | 76 - crates/df-ideas/src/lib.rs | 4 +- crates/df-ideas/src/promotion.rs | 7 +- crates/df-ideas/src/scoring.rs | 199 +- crates/df-nodes/src/ai_node.rs | 319 ++- crates/df-nodes/src/docker_node.rs | 41 - crates/df-nodes/src/git_node.rs | 41 - crates/df-nodes/src/http_node.rs | 43 - crates/df-nodes/src/lib.rs | 7 +- crates/df-nodes/src/notify_node.rs | 41 - crates/df-nodes/src/subflow_node.rs | 39 - crates/df-plugin/Cargo.toml | 14 - crates/df-plugin/src/host.rs | 66 - crates/df-plugin/src/lib.rs | 5 - crates/df-plugin/src/loader.rs | 94 - crates/df-plugin/src/sdk.rs | 90 - crates/df-project/src/context.rs | 52 - crates/df-project/src/lib.rs | 6 +- crates/df-project/src/scan.rs | 303 +++ crates/df-project/src/scheduler.rs | 66 - crates/df-project/src/timeline.rs | 64 - crates/df-stages/Cargo.toml | 14 - crates/df-stages/src/coding.rs | 50 - crates/df-stages/src/idea.rs | 57 - crates/df-stages/src/lib.rs | 7 - crates/df-stages/src/release.rs | 73 - crates/df-stages/src/requirement.rs | 50 - crates/df-stages/src/testing.rs | 50 - crates/df-storage/src/crud.rs | 1273 ++++++++++- crates/df-storage/src/migrations.rs | 336 ++- crates/df-storage/src/models.rs | 42 + .../df-storage/tests/project_soft_delete.rs | 271 +++ crates/df-task/Cargo.toml | 13 - crates/df-task/src/branch.rs | 74 - crates/df-task/src/lib.rs | 6 - crates/df-task/src/merge.rs | 91 - crates/df-task/src/release.rs | 132 -- crates/df-task/src/task.rs | 87 - crates/df-traceability/Cargo.toml | 11 - crates/df-traceability/src/annotation.rs | 130 -- crates/df-traceability/src/decision.rs | 95 - crates/df-traceability/src/lib.rs | 9 - crates/df-traceability/src/traceability.rs | 78 - crates/df-workflow/src/conditions.rs | 72 +- crates/df-workflow/src/registry.rs | 14 +- dev-tools.bat | 30 + docs/01-技术文档/SQLite-CRUD模式.md | 65 +- docs/02-架构设计/B-03-人工审批响应机制.md | 248 ++ docs/02-架构设计/功能决策记录-归档.md | 407 ++++ docs/02-架构设计/功能决策记录.md | 472 ++++ docs/02-架构设计/文档记录规范.md | 167 ++ docs/02-架构设计/经验记录.md | 140 ++ docs/02-架构设计/规格契约自检机制.md | 359 +++ docs/03-模块文档/df-ai-AI集成模块.md | 488 ++-- docs/03-模块文档/df-knowledge-知识库.md | 135 ++ docs/03-模块文档/df-storage-存储层.md | 134 +- docs/03-模块文档/df-workflow-工作流引擎.md | 180 +- docs/07-项目管理/Phase2计划.md | 63 + docs/INDEX.md | 18 +- docs/todo.md | 94 + package-lock.json | 1993 +++++++++++++++++ package.json | 3 + src-tauri/Cargo.toml | 5 + src-tauri/capabilities/default.json | 4 +- src-tauri/src/commands/ai.rs | 1421 ------------ src-tauri/src/commands/ai/agentic.rs | 271 +++ src-tauri/src/commands/ai/audit.rs | 238 ++ src-tauri/src/commands/ai/commands.rs | 531 +++++ src-tauri/src/commands/ai/conversation.rs | 269 +++ src-tauri/src/commands/ai/knowledge_inject.rs | 557 +++++ src-tauri/src/commands/ai/mod.rs | 146 ++ src-tauri/src/commands/ai/prompt.rs | 86 + src-tauri/src/commands/ai/skills.rs | 169 ++ src-tauri/src/commands/ai/stream_recv.rs | 115 + src-tauri/src/commands/ai/title.rs | 140 ++ src-tauri/src/commands/ai/tool_registry.rs | 394 ++++ src-tauri/src/commands/idea.rs | 230 +- src-tauri/src/commands/knowledge.rs | 450 ++++ src-tauri/src/commands/knowledge_timeline.rs | 123 + src-tauri/src/commands/mod.rs | 3 + src-tauri/src/commands/project.rs | 301 ++- src-tauri/src/commands/settings.rs | 47 + src-tauri/src/commands/task.rs | 2 +- src-tauri/src/lib.rs | 49 +- src-tauri/src/state.rs | 335 ++- src/App.vue | 93 +- src/api/ai.ts | 42 +- src/api/idea.ts | 17 +- src/api/index.ts | 2 + src/api/knowledge.ts | 82 + src/api/project.ts | 39 +- src/api/settings.ts | 28 + src/api/types.ts | 132 +- src/components/AiChat.vue | 1108 +++++---- src/components/ConfirmDialog.vue | 96 + src/components/ToolCard.vue | 678 ++++++ src/components/ToolCardList.vue | 62 + src/composables/ai/useAiConversations.ts | 178 ++ src/composables/ai/useAiEvents.ts | 257 +++ src/composables/ai/useAiPanel.ts | 107 + src/composables/ai/useAiSend.ts | 124 + src/composables/ai/useAiStream.ts | 52 + src/composables/ai/useAiWindow.ts | 167 ++ src/constants/project.ts | 101 + src/i18n/en/ai.ts | 10 + src/i18n/en/aiChat.ts | 64 + src/i18n/en/aiTool.ts | 31 + src/i18n/en/common.ts | 17 + src/i18n/{en.ts => en/dashboard.ts} | 29 - src/i18n/en/ideas.ts | 79 + src/i18n/en/index.ts | 14 + src/i18n/en/knowledge.ts | 78 + src/i18n/en/nav.ts | 15 + src/i18n/en/projectDetail.ts | 54 + src/i18n/en/projects.ts | 42 + src/i18n/en/settings.ts | 118 + src/i18n/en/tasks.ts | 56 + src/i18n/index.ts | 18 +- src/i18n/zh-CN/ai.ts | 10 + src/i18n/zh-CN/aiChat.ts | 64 + src/i18n/zh-CN/aiTool.ts | 33 + src/i18n/zh-CN/common.ts | 17 + src/i18n/{zh-CN.ts => zh-CN/dashboard.ts} | 29 - src/i18n/zh-CN/ideas.ts | 79 + src/i18n/zh-CN/index.ts | 14 + src/i18n/zh-CN/knowledge.ts | 94 + src/i18n/zh-CN/nav.ts | 15 + src/i18n/zh-CN/projectDetail.ts | 54 + src/i18n/zh-CN/projects.ts | 41 + src/i18n/zh-CN/settings.ts | 118 + src/i18n/zh-CN/tasks.ts | 56 + src/main.ts | 4 +- src/stores/ai.ts | 534 +---- src/stores/appSettings.ts | 138 ++ src/stores/index.ts | 3 +- src/stores/knowledge.ts | 217 +- src/stores/project.ts | 160 +- src/styles/global.css | 1 + src/views/Ideas.vue | 263 ++- src/views/Knowledge.vue | 896 +++++--- src/views/ProjectDetail.vue | 304 +-- src/views/Projects.vue | 227 +- src/views/Settings.vue | 514 ++++- src/views/Tasks.vue | 110 +- 167 files changed, 19549 insertions(+), 6886 deletions(-) delete mode 100644 crates/df-evolve/Cargo.toml delete mode 100644 crates/df-evolve/src/evolve_engine.rs delete mode 100644 crates/df-evolve/src/knowledge.rs delete mode 100644 crates/df-evolve/src/lib.rs delete mode 100644 crates/df-evolve/src/pattern.rs delete mode 100644 crates/df-evolve/src/prompt_template.rs delete mode 100644 crates/df-evolve/src/review_rule.rs delete mode 100644 crates/df-execute/src/docker.rs delete mode 100644 crates/df-execute/src/git_ops.rs delete mode 100644 crates/df-execute/src/ssh.rs delete mode 100644 crates/df-ideas/src/evaluator.rs delete mode 100644 crates/df-ideas/src/graph.rs delete mode 100644 crates/df-nodes/src/docker_node.rs delete mode 100644 crates/df-nodes/src/git_node.rs delete mode 100644 crates/df-nodes/src/http_node.rs delete mode 100644 crates/df-nodes/src/notify_node.rs delete mode 100644 crates/df-nodes/src/subflow_node.rs delete mode 100644 crates/df-plugin/Cargo.toml delete mode 100644 crates/df-plugin/src/host.rs delete mode 100644 crates/df-plugin/src/lib.rs delete mode 100644 crates/df-plugin/src/loader.rs delete mode 100644 crates/df-plugin/src/sdk.rs delete mode 100644 crates/df-project/src/context.rs create mode 100644 crates/df-project/src/scan.rs delete mode 100644 crates/df-project/src/scheduler.rs delete mode 100644 crates/df-project/src/timeline.rs delete mode 100644 crates/df-stages/Cargo.toml delete mode 100644 crates/df-stages/src/coding.rs delete mode 100644 crates/df-stages/src/idea.rs delete mode 100644 crates/df-stages/src/lib.rs delete mode 100644 crates/df-stages/src/release.rs delete mode 100644 crates/df-stages/src/requirement.rs delete mode 100644 crates/df-stages/src/testing.rs create mode 100644 crates/df-storage/tests/project_soft_delete.rs delete mode 100644 crates/df-task/Cargo.toml delete mode 100644 crates/df-task/src/branch.rs delete mode 100644 crates/df-task/src/lib.rs delete mode 100644 crates/df-task/src/merge.rs delete mode 100644 crates/df-task/src/release.rs delete mode 100644 crates/df-task/src/task.rs delete mode 100644 crates/df-traceability/Cargo.toml delete mode 100644 crates/df-traceability/src/annotation.rs delete mode 100644 crates/df-traceability/src/decision.rs delete mode 100644 crates/df-traceability/src/lib.rs delete mode 100644 crates/df-traceability/src/traceability.rs create mode 100644 dev-tools.bat create mode 100644 docs/02-架构设计/B-03-人工审批响应机制.md create mode 100644 docs/02-架构设计/功能决策记录-归档.md create mode 100644 docs/02-架构设计/功能决策记录.md create mode 100644 docs/02-架构设计/文档记录规范.md create mode 100644 docs/02-架构设计/经验记录.md create mode 100644 docs/02-架构设计/规格契约自检机制.md create mode 100644 docs/03-模块文档/df-knowledge-知识库.md create mode 100644 docs/todo.md create mode 100644 package-lock.json delete mode 100644 src-tauri/src/commands/ai.rs create mode 100644 src-tauri/src/commands/ai/agentic.rs create mode 100644 src-tauri/src/commands/ai/audit.rs create mode 100644 src-tauri/src/commands/ai/commands.rs create mode 100644 src-tauri/src/commands/ai/conversation.rs create mode 100644 src-tauri/src/commands/ai/knowledge_inject.rs create mode 100644 src-tauri/src/commands/ai/mod.rs create mode 100644 src-tauri/src/commands/ai/prompt.rs create mode 100644 src-tauri/src/commands/ai/skills.rs create mode 100644 src-tauri/src/commands/ai/stream_recv.rs create mode 100644 src-tauri/src/commands/ai/title.rs create mode 100644 src-tauri/src/commands/ai/tool_registry.rs create mode 100644 src-tauri/src/commands/knowledge.rs create mode 100644 src-tauri/src/commands/knowledge_timeline.rs create mode 100644 src-tauri/src/commands/settings.rs create mode 100644 src/api/knowledge.ts create mode 100644 src/api/settings.ts create mode 100644 src/components/ConfirmDialog.vue create mode 100644 src/components/ToolCard.vue create mode 100644 src/components/ToolCardList.vue create mode 100644 src/composables/ai/useAiConversations.ts create mode 100644 src/composables/ai/useAiEvents.ts create mode 100644 src/composables/ai/useAiPanel.ts create mode 100644 src/composables/ai/useAiSend.ts create mode 100644 src/composables/ai/useAiStream.ts create mode 100644 src/composables/ai/useAiWindow.ts create mode 100644 src/constants/project.ts create mode 100644 src/i18n/en/ai.ts create mode 100644 src/i18n/en/aiChat.ts create mode 100644 src/i18n/en/aiTool.ts create mode 100644 src/i18n/en/common.ts rename src/i18n/{en.ts => en/dashboard.ts} (65%) create mode 100644 src/i18n/en/ideas.ts create mode 100644 src/i18n/en/index.ts create mode 100644 src/i18n/en/knowledge.ts create mode 100644 src/i18n/en/nav.ts create mode 100644 src/i18n/en/projectDetail.ts create mode 100644 src/i18n/en/projects.ts create mode 100644 src/i18n/en/settings.ts create mode 100644 src/i18n/en/tasks.ts create mode 100644 src/i18n/zh-CN/ai.ts create mode 100644 src/i18n/zh-CN/aiChat.ts create mode 100644 src/i18n/zh-CN/aiTool.ts create mode 100644 src/i18n/zh-CN/common.ts rename src/i18n/{zh-CN.ts => zh-CN/dashboard.ts} (66%) create mode 100644 src/i18n/zh-CN/ideas.ts create mode 100644 src/i18n/zh-CN/index.ts create mode 100644 src/i18n/zh-CN/knowledge.ts create mode 100644 src/i18n/zh-CN/nav.ts create mode 100644 src/i18n/zh-CN/projectDetail.ts create mode 100644 src/i18n/zh-CN/projects.ts create mode 100644 src/i18n/zh-CN/settings.ts create mode 100644 src/i18n/zh-CN/tasks.ts create mode 100644 src/stores/appSettings.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index eca2276..cc3c2d1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -84,12 +84,10 @@ devflow/ │ ├── df-execute/ # 执行运行时 (Shell/Docker/SSH/Git) │ ├── df-storage/ # 存储层 (SQLite) │ ├── df-ideas/ # 想法池引擎 (捕捉/评估/评分/晋升) -│ ├── df-project/ # 多项目管理 (调度/上下文/时间线) -│ ├── df-task/ # 任务/分支管理 (并行任务/分支/合并/冲突) -│ ├── df-traceability/ # 可追溯性 (标注/决策留痕/需求-测试映射) -│ ├── df-evolve/ # 经验进化 (知识沉淀/Prompt模板/审查规则/踩坑经验) -│ ├── df-stages/ # 阶段插件 (5 阶段内置模板) -│ └── df-plugin/ # 插件系统 (WASM/动态库) +│ └── df-project/ # 多项目管理 (调度/上下文/时间线) +│ +│ 注: df-task / df-traceability / df-evolve / df-stages / df-plugin +│ 5 个 crate 已移除(2026-06-14 零引用清理,推翻原"保留骨架"取舍) ├── src/ # Tauri 主入口 │ ├── main.rs │ ├── state.rs @@ -137,7 +135,9 @@ devflow/ 项目状态:`Planning / InProgress / Testing / Releasing / Completed / Paused / Cancelled` -### 5.3.1 Task & Branch Manager (df-task) +### 5.3.1 ~~Task & Branch Manager (df-task)~~ — 已移除 + +> **2026-06-14 零引用清理**:df-task crate 已删除。以下内容保留作为历史设计参考,不再对应实际代码。 项目内部的并行任务管理,每个任务绑定一个 Git 分支。 @@ -165,7 +165,9 @@ Task 生命周期: 2. AI 辅助解决:冲突文件交给 AI 分析并建议解决方案 3. 发布编排:选择多个 Task → 创建 release 分支 → 合并 → 集成测试 → 发布 -### 5.4 Traceability & Annotation (df-traceability) +### 5.4 ~~Traceability & Annotation (df-traceability)~~ — 已移除 + +> **2026-06-14 零引用清理**:df-traceability crate 已删除。以下内容保留作为历史设计参考,不再对应实际代码。 贯穿所有阶段的可追溯性引擎。 @@ -504,7 +506,7 @@ CREATE TABLE decisions ( ### Phase 4 — 节点丰富 + 阶段插件 (3-4 周) - df-nodes (Docker/Git/Human/HTTP) -- df-stages (5 阶段模板) +- ~~df-stages (5 阶段模板)~~ — 已移除(2026-06-14 零引用清理) - 条件分支 + 断点续跑 - 验证:跑通标准产研流程模板 diff --git a/Cargo.lock b/Cargo.lock index ebd7a87..9eb5918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -721,10 +721,13 @@ name = "devflow" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "df-ai", "df-core", "df-execute", + "df-ideas", "df-nodes", + "df-project", "df-storage", "df-workflow", "futures", @@ -732,7 +735,9 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-opener", + "tauri-plugin-window-state", "tokio", "tracing", ] @@ -764,18 +769,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "df-evolve" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "df-core", - "serde", - "serde_json", - "tracing", -] - [[package]] name = "df-execute" version = "0.1.0" @@ -818,20 +811,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "df-plugin" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "df-core", - "df-workflow", - "serde", - "serde_json", - "tokio", - "tracing", -] - [[package]] name = "df-project" version = "0.1.0" @@ -845,20 +824,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "df-stages" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "df-core", - "df-workflow", - "serde", - "serde_json", - "tokio", - "tracing", -] - [[package]] name = "df-storage" version = "0.1.0" @@ -872,30 +837,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "df-task" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "df-core", - "serde", - "serde_json", - "tokio", - "tracing", -] - -[[package]] -name = "df-traceability" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "df-core", - "serde", - "serde_json", -] - [[package]] name = "df-workflow" version = "0.1.0" @@ -2615,6 +2556,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -3234,6 +3176,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "ring" version = "0.17.14" @@ -4055,6 +4021,48 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.4" @@ -4077,6 +4085,21 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.13.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-runtime" version = "2.11.2" @@ -5239,6 +5262,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -5272,13 +5304,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -5309,6 +5358,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -5321,6 +5376,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -5333,12 +5394,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -5351,6 +5424,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5363,6 +5442,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -5375,6 +5460,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5387,6 +5478,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" diff --git a/PROGRESS.md b/PROGRESS.md index cf0072b..dee339f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,6 +1,6 @@ # DevFlow — 项目进展与工作交接 -> 创建: 2026-06-10 | 最后更新: 2026-06-12 | 当前阶段: Phase 2 aichat UX 快赢(A 路线)+ 技能联想需求调研(Sprint 8) +> 创建: 2026-06-10 | 最后更新: 2026-06-14 | 当前阶段: list_directory 防爆 + localStorage→SQLite 统一持久化 + token/发送 bug 修复(Sprint 19) → 项目管理模块代码审查 + 全局核对(Sprint 20,未改代码) --- @@ -13,47 +13,45 @@ | 路径 | `E:/wk-lab/devflow/` | | 架构文档 | `ARCHITECTURE.md` (22,745 字) | | Git 状态 | 未首次 commit,代码全在 untracked | -| AI 能力 | df-ai OpenAI 兼容 Provider + 12 工具 + Agentic Loop | +| AI 能力 | df-ai OpenAI/Anthropic 双协议 Provider + 12 工具 + Agentic Loop | --- ## 二、代码规模统计 -### Rust 后端 (71 个 .rs 文件) +### Rust 后端 (78 个 .rs 文件) | Crate | 文件数 | 总行数 | 有效行 | 实现程度 | |-------|--------|--------|--------|---------| -| df-core | 4 | 259 | 151 | ✅ 完整 — 错误/事件/状态枚举/ID生成 | -| df-workflow | 7 | 489 | 333 | ✅ 核心 — DAG拓扑排序/执行器/状态机/事件总线 可用 | -| df-storage | 4 | 294 | 217 | ✅ 部分 — SQLite连接/迁移/建表完整,**缺 CRUD 层** | +| df-core | 4 | 429 | ~260 | ✅ 完整 — 错误/事件/状态枚举/ID生成 | +| df-workflow | 9 | 908 | ~620 | ✅ 核心 — DAG拓扑排序/执行器/状态机/事件总线 可用 | +| df-storage | 5 | ~2,100 | ~1,400 | ✅ 完整 — 连接/迁移/建表(V1-V8) + impl_repo! 宏 CRUD + KnowledgeRepo(search/list_by_status/set_embedding/search_vector 等)+ 余弦相似度工具函数 | | df-execute | 5 | 203 | 129 | ⚡ 混合 — Shell 执行器真实可用,Docker/SSH/Git 骨架 | -| df-ideas | 6 | 421 | 278 | ⚡ 大部分 — 捕获/评估/晋升/关联图 有逻辑,评分固定值 | -| df-task | 5 | 390 | 252 | ⚡ 大部分 — Task/Branch/Semver 可用,合并冲突 骨架 | +| df-ideas | 7 | 761 | ~500 | ✅ 评分/对抗启发式已真实化(Sprint 9)— 捕获/评估/晋升/关联图 有逻辑,晋升/关联图仍骨架 | | df-project | 5 | 272 | 175 | ⚡ 混合 — 数据模型/Timeline 可用,调度器 骨架 | -| df-ai | 6 | 281 | 173 | 🔧 接口 — 类型/Provider trait 完整,无实际 LLM 调用 | -| df-traceability | 4 | 312 | 201 | ⚡ 混合 — 决策记录/标注构建 可用,SQLite查询 骨架 | -| df-plugin | 4 | 255 | 158 | ⚡ 混合 — Host/Builder 有逻辑,核心加载 骨架 | -| df-evolve | 6 | 301 | 172 | ⚡ 混合 — Knowledge/PromptTemplate 可用,引擎/提取器 骨架 | -| df-nodes | 9 | 344 | 277 | ⚡ 混合 — script_node 真实可用(接 Shell),human_node 半实现(审批响应 TODO),其余 6 种骨架 | -| df-stages | 6 | 287 | 209 | 🔩 全骨架 — 11个阶段节点 Schema 完整,execute() 全空 | -| **合计** | **71** | **~4,108** | **~2,625** | | +| df-ai | 9 | 1,965 | ~1,300 | ✅ 完整 — OpenAI/Anthropic 双协议 Provider + 流式 SSE + 上下文窗口(ContextManager 分组滑窗) + token usage 解析 + 12 工具 | +| df-nodes | 9 | ~500 | ~350 | ⚡ 混合 — script_node(接 Shell)/ ai_node(接 df-ai,Sprint 7)真实可用,human_node 半实现(审批响应 TODO),其余骨架 | +| **合计** | **53** | **~7,400** | **~4,800** | 已删除 5 个零引用 crate(2026-06-14,原 78/9100/5900) | -### 前端 (9 页面 + Store + i18n) +### 前端 (9 页面 + 2 组件 + Store + i18n) | 类别 | 文件数 | 行数 | 状态 | |------|--------|------|------| -| Vue 页面 | 9 | ~3,372 | ✅ UI 全部真实 — 无空壳 | -| Pinia Store | 4 | ~362 | ⚠️ 有 state/getter 但**无 action,View 未引用** | -| Router | 1 | 63 | ✅ 9 条路由含动态路由 | -| i18n | 2 | ~94 | ⚠️ 翻译完整但**未接入 main.ts** | +| Vue 页面 | 9 | ~3,372 | ✅ UI 全部真实 — 无空壳(含 AiHome/AiDetached)| +| Vue 组件 | 2 | ~1,400 | ✅ AiChat(主面板)+ ConfirmDialog | +| Pinia Store | 4+ | ~800 | ✅ composable 模式,View 已引用(Sprint 4 接入)| +| Router | 1 | ~90 | ✅ 10 条路由含动态路由(Sprint 7 增 AiHome/AiDetached)| +| i18n | 2 | ~94 | ✅ 已注册 main.ts(Sprint 7)+ localStorage 持久化 | | 设计系统 | 1 | 105 | ✅ CSS 变量/动画/Arco 暗色覆盖 | -| Tauri Commands | 1 | 13 | 🔩 仅 `greet` 示例,无业务逻辑 | +| **前端合计** | | **~8,812** | | -**前端关键问题**: -- 所有 View 组件数据**硬编码** (`ref([...])`),与 Store 完全独立 -- 所有按钮事件处理函数为**空函数** `{}` -- i18n 插件未注册到 Vue app -- Arco Design 组件库未实际使用(仅 CSS 变量覆盖) +> Tauri Commands 已达 **57 个**(project/task/idea/workflow + AI 对话/工具/审批/Provider/技能全功能 + 知识库 11 个),见后端 src-tauri/src/commands/。 + +**前端关键问题**(Sprint 4/7 已全部解决): +- ~~所有 View 组件数据硬编码~~ ✅ Sprint 4 接 Store 真数据 +- ~~所有按钮事件处理函数为空函数~~ ✅ Sprint 4 接 Store action +- ~~i18n 插件未注册~~ ✅ Sprint 7 注册 + $t() 全 view 改造 +- ~~Arco Design 组件库未实际使用~~ ✅ 已用(表单/弹窗/消息等),CSS 变量覆盖暗色 --- @@ -68,11 +66,11 @@ 待打通路径(骨架就位,需填充): df-workflow → df-nodes (8种节点 execute() 待实现) - df-workflow → df-stages (11种阶段节点 execute() 待实现) - df-ai → LlmProvider 实现 (无 HTTP client) - df-storage → 所有 crate 的 CRUD 操作 - Tauri commands → Rust 业务逻辑 - Vue Views → Pinia Store → Tauri IPC → Rust + ~~df-workflow → df-stages (11种阶段节点 execute() 待实现)~~ — df-stages 已移除(2026-06-14 零引用清理) + ~~df-ai → LlmProvider 实现~~ ✅ 已打通(OpenAI/Anthropic 双协议真实 HTTP,Sprint 5/8) + ~~df-storage → 所有 crate 的 CRUD 操作~~ ✅ 已打通(impl_repo! 宏 CRUD + 多 Repo,Sprint 2) + ~~Tauri commands → Rust 业务逻辑~~ ✅ 已打通(47 commands 全接业务,Sprint 3) + ~~Vue Views → Pinia Store → Tauri IPC → Rust~~ ✅ 已打通(Sprint 4) ``` --- @@ -89,6 +87,7 @@ | 6 | **条件表达式引擎** — 仅支持 true/false 字面量 | 条件分支不可用 | P2 | ⬜ 待办 | | 7 | **i18n 未注册** — 翻译文件存在但未挂载 | 多语言不生效 | P2 | ✅ Sprint 7 | | 8 | **未 git commit** — 代码全 untracked | 无版本基线 | P0 | ✅ Sprint 2 | +| 9 | **AI 删项目绕过软删** — tool_registry:263 `repo.delete` 物理删,IPC 走 `soft_delete` 进回收站;AI 层缺 restore/purge/list_trash 工具(仅 project 表有回收站设计,故最该修) | AI 对话删项目 = 数据丢失不可恢复,与 UI 删语义割裂 | P1 | ⬜ Sprint 20 待修(批1) | --- @@ -121,9 +120,9 @@ | AI Chat 面板 | ✅ | 侧边栏 + 分离窗口 + 拖拽调宽 + 对话管理 | | Agentic Loop | ✅ | 多轮工具调用循环(max 10 轮)+ 中/高风险人工审批 | | AI Node 实现 | ✅ | df-nodes 接 df-ai:config 驱动 provider(base_url/api_key/model/prompt)→ OpenAICompatProvider.complete → 输出 text/model/usage;注册 "ai" 节点;未端到端实测 | -| **AI Chat LLM 优化** | 🚧 P0 完成 | 任务 #43:P0 可靠性(超时/断连/停止生成)✅;P1 token 控制(usage采集/滑动窗口/摘要)⬜ | | +| **AI Chat LLM 优化** | ✅ ABCD 完成 | 任务 #43:P0 可靠性 ✅;P1 上下文窗口(ContextManager 分组滑窗)✅ + 并行化(工具 join_all / save 后台)✅;**Sprint 14 流式 token 用量记录(usage 解析 + 累加落库 + 展示 + Settings 开关)✅**;Part C 并发 Semaphore(双层 + 运行时可调)+ Part D Settings UI ✅(Sprint 16);Review 两轮 8 修复 ✅ | | -### Phase 3 — 想法池 + 多项目 (3-4 周) | ⬜ 未开始 +### Phase 3 — 想法池 + 多项目 (3-4 周) | 🚧 进行中 — Sprint 9 评估闭环已通(启发式评分 + 对抗评估接 IPC),晋升前端层闭环,多项目/关联图待续;Sprint 12 全面审查修 5 项前端 bug(置信度条/状态选择/删除确认等),死代码组缓另会话重审 ### Phase 4 — 节点丰富 + 阶段插件 (3-4 周) | ⬜ 未开始 ### Phase 5 — 体验打磨 (4-6 周) | ⬜ 未开始 @@ -318,7 +317,7 @@ - **审查项落地**:删全屏 Tool Approval Modal(保留行内卡片审批)、错误友好化(`friendlyError` 正则映射 404/401/timeout/network → 中文提示 + `isError` 红色气泡)、智能滚动(`isNearBottom` < 80px 阈值)、provider bar 点击切换、未配 provider 空状态引导、删死 class - **B 路线(规划式多智能体)单独立项**:填 `coordinator.rs` + `conditions.rs` 空壳,详见 memory `aichat-decision-capability` -**B. 技能联想功能需求(本轮重点,未实现,需求阶段)**: +**B. 技能联想功能(首批 Claude 3 类,已实现待实测)**: - **需求**:aichat 输入框输入 `/` → 联想列出本机 Claude 技能(SKILL.md),按输入过滤,选中插入 `/name`;发送时后端读 SKILL.md 全文注入 system prompt / context - **首批数据源 = Claude 3 类**(SKILL.md frontmatter 统一 `name` / `description` / `user_invocable` / `metadata.argument-hint` / `triggers[]`,只取 `user_invocable: true`): 1. Claude skills:`~/.claude/skills/*/SKILL.md`(23 个:sleep / mission-control / idea / cpa 等) @@ -338,10 +337,347 @@ **代码变更**:A 路线 — `crates/df-ai/src/{anthropic_compat.rs, lib.rs}`、`src-tauri/src/commands/ai.rs`、`crates/df-nodes/src/ai_node.rs`、`src/views/Settings.vue`、`src/stores/ai.ts`、`src/components/AiChat.vue`、`src/api/types.ts` **遗留问题 / 下一步**: -1. **技能联想(B)未实现** — 需求已记,首批聚焦 Claude 3 类,等排期(工作量中:后端扫文件 + IPC、前端联想浮层) +1. **技能联想(B)已实现待实测** — `ai_list_skills` IPC(扫 skills/commands/plugins 三类 + frontmatter 解析 + name 去重)+ `ai_chat_send` 加 `skill` 参数读 SKILL.md 注入 system prompt + 前端 `/` 浮层(↑↓ 导航 / Enter·Tab 选中 / chip 显示 / Esc 关)。cargo check ✓(7 warning 既有)/ bun build ✓(4.68s)。实测:重启 dev → AiChat 输入 `/` → 选技能 → 发送 2. **A 路线剩余场景实测**:切对话不中断(场景 2/3)、错误友好化(场景 4)待用户实测 3. **B 路线(规划式能力)**:coordinator / conditions 空壳,单独立项 +### [Sprint 9] 2026-06-12 — 灵感模块评估闭环(启发式评分 + 对抗评估接 IPC) + +**工作内容**: +- **df-ideas 修复与真实化**:① 修 adversarial.rs E0308 类型错(`AdversarialEval.negative: Argument` vs `generate_negative_argument` 返 `CounterArgument` 直接赋值,df-ideas 实际编译不过——前文档"cargo check 通过"为误记)→ 删 CounterArgument、正反方归一 Argument;② scoring.rs 三维固定 5.0 → 内容启发式(priority / 描述充实度 / tags / 中英关键词,clamp 0-10);③ adversarial.rs 正反方论点/evidence 基于真实 idea 内容生成,confidence 由评分驱动,final_score=综合评分,AssessmentLevel 由 overall 映射 +- **devflow 接 df-ideas**:`src-tauri/Cargo.toml` +df-ideas +chrono 依赖(df-ideas 此前为孤儿 crate,devflow 未依赖、零 IPC 接入) +- **evaluate_idea IPC**(commands/idea.rs):取 IdeaRecord → record_to_idea 转 df-ideas::Idea → ScoringEngine + AdversarialEngine.evaluate → 组装前端扁平结构(对齐 Ideas.vue AdversarialEval interface)→ 写回 scores(中文维度 0-100)/score(0-100)/ai_analysis/status=pending_review → 返回更新记录;lib.rs 注册 +- **前端接真实评估**:api/idea.ts +evaluate;stores/project.ts +evaluateIdea action;Ideas.vue 删 mockEval 调真实 + evaluating loading 态 +- **审核修 2 bug**:① recommendation 大小写(后端原 "With Resources" 与前端 assessmentLabel 全小写 map key 不匹配致中文标签不显示)→ 全小写空格;② assessmentClass 生成的 class 与 CSS 类名不一致致 badge 无色 → map 映射到 .immediate/.soon/.conditional/.revised/.defer/.cancel + +**代码变更**:df-ideas/{scoring,adversarial}.rs、src-tauri/{Cargo.toml,commands/idea.rs,lib.rs}、前端 {api/idea.ts,stores/project.ts,views/Ideas.vue}(8 文件) + +**验证**:cargo check -p df-ideas ✓、cargo check -p devflow ✓、bun run build ✓(vue-tsc + vite 3.3s) + +**遗留问题 / 下一步**: +1. 对抗评估仍启发式,未接 df-ai LlmProvider(设计:LLM 生成正反方论点 + 启发式 fallback) +2. promotion.rs do_promote 仍 TODO(晋升走前端 createProject 闭环,不经 df-ideas/promotion) +3. evaluate_idea 用 list_all().find() + 5 次 update_field(IdeaRepo 无 get/多字段更新,量小可接受) +4. IdeaGraph 关联图零接入(自动关联发现/聚类待续) +5. scores JSON 含「综合」维度也被雷达图渲染一行(小瑕疵) + +### [Sprint 10] 2026-06-12 — AiChat 工具卡片自动折叠(卡片级折叠 + 新内容追加自动收起) + +**工作内容**: +- **需求**:对话中工具调用卡片(List Projects / List Tasks / Create Idea / List Ideas / 读取文件等)完成后仍整卡铺开,多步调用时界面臃肿。目标——展示更友好紧凑:所有卡片可点击 header 展开/收起,新内容追加时旧卡片自动收起为单行 header(类似 ChatGPT/Cursor 旧 tool call 折叠) +- **卡片级折叠**(区别于 read_file 已有的内容级折叠):`.ai-tool-card` 内 body 区(骨架屏/审批/拒绝/file/dir/write/通用结果)包进 `.ai-tool-body`,`v-show` 控制显隐;header 加 `@click` + ▸ 箭头指示器;新增 `expandedCards` Set + `toggleCardExpand` + `cardCollapseClass`,与既有 `expandedTools`(read_file 预览级)职责分离不互相干扰 +- **自动收起**:deep watch `store.state.messages` + 轻量 JSON snapshot diff 检测新内容(新消息 / toolCall 状态变化 / 文本增长)→ 清除旧 completed/rejected 的展开态,保留 running/pending 活跃卡片;`isFirst` guard 防首次加载/切对话误触发 +- **running/pending_approval 强制展开**(需看骨架屏与审批按钮);`v-show` 保留 DOM 避免重挂载 + +**代码变更**:`src/components/AiChat.vue`(template header/body 包裹 + script 状态与 watcher + style 折叠态),单文件 7 处改动 + +**验证**:vue-tsc --noEmit ✓(运行时待 `bun run tauri dev` 实测多步工具调用折叠效果) + +**遗留问题 / 下一步**: +1. ~~审查项 ①~~ ✅ 已落地(2026-06-12):`isRunningOrPending(status)` → `shouldKeepOpen(tc)`,`running` / `pending_approval` / `rejected` / `write_file` 强制展开(短结果不折叠、原因/路径不丢失),仅 `read_file` / `list_directory` / 通用 JSON 大体量结果折叠。vue-tsc --noEmit ✓。决策详见 [功能决策记录 - 短结果保持展开](docs/02-架构设计/功能决策记录.md) +2. 实测验证:tauri dev 发多步工具调用消息,观察"执行中展开→完成→新内容追加自动收起→点击 header 重展开" + +### [Sprint 11] 2026-06-13 — AI Chat LLM 优化(上下文窗口管理 + 并行化) + +**任务 #43 续**(Part A + B 落地,Part C/D 待续) + +**工作内容**: + +**Part A: 上下文窗口管理(df-ai/context.rs + ai.rs 接线)** — 长对话无限增长致 `context_length_exceeded` 死锁 +- **A1 ContextManager**(`crates/df-ai/src/context.rs` 新增 ~350 行):TokenEstimator 字符粗估(chars×0.35 + per_message 4 + per_tool_call 30,零依赖,保守 ±15%)+ ContextConfig(max_tokens 128k / output_reserve 8192 / safety 0.85)+ 分组滑动窗口淘汰——淘汰单元保工具调用三元组原子性(ToolCallHead + ToolResultTail* + 紧随文本 Assistant 同进同出),保护区最后 6 条永不裁;裁剪仅影响发送视图(build_for_request 返裁剪 Vec),不影响持久化(all_messages_clone 返全量)。5 单测全绿(短对话无裁剪 / 超预算裁剪保保护区 / 三元组原子性双分支 / replace 重估 token / restore 重建缓存) +- **A2 ai.rs 接线**(~15 处):AiSession.messages 类型 `Vec` → `ContextManager`;核心 run_agentic_loop 用 `build_for_request(sys_tokens)` 替代全量 `.clone()`;save/title 用 `all_messages_clone()`;switch 用 `restore_from_messages()`;`replace_tool_result` 收敛为 ContextManager 方法(DRY,删自由函数) + +**Part B: 并行化改造(ai.rs)** — 串行瓶颈 + Completed 事件阻塞 +- **B1 process_tool_calls 并行化**:Low 风险工具 `futures::future::join_all` 并行 execute + 即时 emit Completed,结果串行回填 session.messages(N 个独立工具 ≈ 最慢一个耗时,原 N 倍串行);Med/High 仍串行审批门控。tool_result push 顺序变化无语义影响(LLM 按 tool_call_id 关联,不看绝对位置) +- **B2 正常完成后台化**:save+extract+title 打包同一 `tauri::async_runtime::spawn`(非裸 spawn save——保 maybe_spawn_extraction 读已落库消息的顺序依赖);Completed 事件即时下发 +- **B3 标题后台化**:`ensure_conversation_title` 签名收 `provider_config` 克隆进 task 自建 provider(`&dyn` 非 'static 无法 move),`spawn_ensure_title` 后台(含 2 处 stop 路径) + +**关键取舍**:①打包 spawn 而非裸 spawn save(保 extract 顺序依赖);②并发 upsert 低概率丢少量 token 可接受(非功能错误);③停止路径保持同步 save(避免 stop 后即发新消息的并发 upsert 竞态丢 token) + +**代码变更**:`crates/df-ai/src/context.rs`(新增)、`src-tauri/src/commands/ai.rs`(核心改造)、`docs/02-架构设计/功能决策记录.md`(8 条新决策:Part A 2 + Part B 3 + 既有的) + +**验证**:cargo check -p df-ai ✓(5 单测绿)、cargo check -p devflow ✓(6 既有 warning,无 error,无单测回归) + +**遗留问题 / 下一步**: +1. **Part C/D 未做**(todo #48-52 已建)—— Part C 双层 Semaphore(global 默认 3 / per_conv 默认 2)+ 3 LLM 调用点(stream_llm / 标题 / 提炼)加 permit + `ai_set_concurrency_config` command;Part D Settings.vue 两 setting-row + localStorage 持久化 + onMounted 同步 +2. **实测验证**(P1):tauri dev 验证长对话裁剪行为 + 多工具并行耗时 + Completed 事件即时性 + +### [Sprint 12] 2026-06-13 — 灵感模块全面审查 + A 类问题修复 + +**工作内容**: + +- **df-ideas 灵感模块全面审查**(前端 Ideas.vue / api / store + IPC idea.rs + crate 7 文件 + df-project/manager + df-storage/crud):按「界面展示 / 交互 / 功能可用性 / 合理性」+「功能架构 / 代码逻辑 / 代码架构」六维逐条核对,共 12 项发现(🔴5 🟡5 ⚪2)。复核确认 12 项全部真实存在、无误判无虚报。 +- **修复 A 类 5 项**(明确知道怎么做、零争议,全在 Ideas.vue): + - ① 置信度进度条不显示填充(`::after` 无 width + 锚点跑到 `.debate-column`)→ 拆 `.confidence-bar`(灰轨)+ `.confidence-fill`(填充层,正绿反红),删失效 `::after` + - ② 状态 select 改选不生效(v-model 绑只读 computed 致写失败、回弹并写回旧 status)→ 弃 v-model,改 `:value` + `@change` 读 `event.target.value` + - ③ 删 `filterIdeas` 空函数及其 `@input` 绑定(filteredIdeas 本是 computed 自动响应) + - ④ 删除想法加 `confirm()` 二次确认 + - ⑥ 对抗评估标题加「启发式」tag(诚实标注当前为启发式非 LLM,避免「对抗辩论」措辞误导) +- **决策**:死代码/双套抽象组(⑦⑧⑨⑩)缓到另一会话,届时重新审核问题真实性;B 类(⑤⑥根治⑪⑫)方案未定,待定方向后再推。 + +**代码变更**:`src/views/Ideas.vue`(单文件 9 处:模板 5 + script 3 + CSS 2 段) + +**验证**:`vue-tsc --noEmit` ✓(零错误零输出) + +**遗留问题 / 下一步**: +1. **B 类(有必要但方案未定)**:⑤ 立项引导(三种改法改变产品流程语义:放宽显示条件 / 保持 approved 门槛加提示 / 评估达标自动 approved)/ ⑥ 对抗评估接 df-ai LlmProvider 根治(大工程,单独立项;当前 UI「启发式」标记为临时绕过)/ ⑪ 状态常量集中(跨 Rust+TS 双侧,IdeaStatus 枚举已有但 IdeaRecord 用 String 落库)/ ⑫ 实体↔Record 映射(评估为合理分层,建议不改) +2. **缓组(另会话重审真实性后再动)**:⑦ df-ideas 死代码空壳(capture/graph/evaluator/promotion;IdeaGraph 关联图是已知下一步增强需求,删/留需定)/ ⑧ 两套 Recommendation 枚举 + 两套评估入口并存(adversarial 6 变体实用 vs evaluator 5 变体仅死代码引用)/ ⑨ PromotionPolicy 三档死枚举 / ⑩ Idea 实体构造分散(record_to_idea 与 IdeaCapture::capture 字段重叠) +3. **实测验证**:tauri dev 看置信度条彩色填充 / 状态切换生效 / 删除弹确认 / 评估区「启发式」标 + +### [Sprint 13] 2026-06-13 — 文档记录治理体系(路由规范 + decision-record skill + 降频自检 hook) + +**背景**:功能决策/需求散落于对话、PROGRESS、模块文档,缺统一真相源与触发机制。建立「单一真相源 + 自动自检」治理体系。 + +**工作内容**: +- **文档记录规范**(`docs/02-架构设计/文档记录规范.md` 新建):核心原则(SSOT / 不复制只引用 / 决策与流水分离 / 先主后辅)+ 文档职责矩阵(每类信息唯一主文档)+ 内容路由表 + 更新顺序 + 唯一性记录与检测(记前 grep 查重 / 单向引用 / 定期扫描)+ 与 skill 关系 + 治理体系实现决策(hook 设计取舍) +- **功能决策记录扩展**(`docs/02-架构设计/功能决策记录.md`):约定节加需求条目说明;末尾新增「需求与待办」总览(📋 待做需求表 + 需求澄清)——散落各决策条目 📐/🚧 状态的待办汇集为单一清单 +- **decision-record skill 增强**(`~/.claude/skills/decision-record/SKILL.md`):除「为什么这么定」决策,增记需求细节(待做/规格/澄清);维护规则(记前查重 / 演进 `→` 标注 / 定期压缩防爆炸);原则补「更新优先于新增」 +- **降频自检 Stop hook**(`~/.claude/hooks/dr-check.sh` + `settings.json` Stop 配置):累计 ≥10 轮 或 (>1 轮 且 距上次 ≥10 分钟) 才注入自检提示触发 skill;纯脚本计数无 API(替代每轮自检,省 ~90% token);`stop_hook_active` guard 防循环;状态按项目隔离(`~/.claude/.dr-state/`,不污染 git)。设计取舍详见 [文档记录规范 - 治理体系实现决策](docs/02-架构设计/文档记录规范.md) +- **修 `docs/INDEX.md` 误登记**:PROGRESS 实际在根级,移除 `07-项目管理/` 树下的重复登记行 + +**代码变更**:非产品代码——`docs/02-架构设计/{文档记录规范.md,功能决策记录.md,INDEX.md}`、`~/.claude/{skills/decision-record/SKILL.md,hooks/dr-check.sh,settings.json}` + +**验证**:hook 逻辑实测(首战静默初始化 / 2-9 轮静默计数 / 第 10 轮或 ≥10 分钟触发 / 防循环 guard);无 jq 依赖纯脚本 + +**遗留问题 / 下一步**: +1. hook 实际长期触发频率待观察(10 轮 / 10 分钟阈值是否合理,按需调) +2. 决策爆炸后执行首次压缩(> 300 行 / 老 Sprint > 3 / 单域 > 10 条触发归档或拆域) + +### [Sprint 14] 2026-06-13 — 流式 token 用量记录 + Provider 默认态持久化 + +**工作内容**: + +**A. 流式 Token 用量获取 + 记录 + 展示 + 设置开关**(跨 df-ai/df-storage/src-tauri/前端,11 文件)— `stream_llm` 全程丢弃 token 计数、`AiCompleted.total_tokens` 恒 0 的旧疾根治(Sprint 6 P1 token 控制中「usage 采集用户叫停」项重启落地): +- **Provider 层 usage 解析**:`StreamChunk` 加 `usage: Option`;OpenAI 开 `stream_options:{include_usage:true}` 末 chunk 取累计 usage;Anthropic `message_start`/`message_delta` 累积(output_tokens 当累计值覆盖,非增量) +- **累加落库**:`ai_conversations` 加 `prompt_tokens`/`completion_tokens` 列(V5 迁移);`save_conversation` upsert 走累加模式(读旧值叠加,非覆盖——保审批暂停→恢复跨 loop 实例总用量正确);`run_agentic_loop` 局部累加器每轮叠加 +- **前端展示 + 开关**:store 双状态 `lastTokenUsage`(单次)+ `convTokenTotal`(对话累计,切对话从 DB 加载);AI 气泡底部 token 条(默认关闭,`df-show-token-usage` 开关放 Settings) +- **model 记录**:消息级 model + 对话级 models 数组(C+B),取 `default_model` 配置值补填不覆盖 + +**B. Provider 默认态持久化(is_default 重启丢失 bug 修复)** — 「设为默认」重启后丢失、两条记录都显示「设为默认」按钮的 bug 根治: +- 根因:`is_default` 字段恒写 `false` + `ai_set_provider` 只改内存 session 不落库 → 重启 session 清零默认全丢 +- 修复:`ai_set_provider` 互斥落库(目标 true/其余 false,仅写变化记录);`ai_save_provider` 新建首个自动默认;`ai_list_providers` 直返 DB 值;`active_provider_id` 降为运行时缓存 + +**代码变更**:A — df-ai/{provider,openai_compat,anthropic_compat}.rs、df-storage/{models,crud,migrations}.rs、src-tauri/commands/ai.rs、前端 {types,stores/ai,components/AiChat,views/Settings}(11 文件);B — src-tauri/commands/ai.rs(3 函数) + +**验证**:cargo check --workspace ✓(0 error,6 既有 warning)、vue-tsc --noEmit ✓。**未 tauri dev 运行时实测** + +**遗留问题 / 下一步**: +1. tauri dev 实测:token 落库 / 切换对话总量加载 / 重启保默认 / 首个自动默认 +2. `ai_save_provider` get_by_id 双调用(review 🟡①,冗余往返,待合并) +3. Settings 页面 UI 重构(Part D,用户说稍后) +4. model UI 展示(暂缓) + +### [Sprint 15] 2026-06-13 — 知识库 Tier 1 全栈打通 + Phase 5.5 向量检索 + +**工作内容**: + +**Tier 1 知识库双向闭环(AI Chat ↔ 知识库,全栈打通)** — candidate→published 状态机 + AI 提炼 + 手动录入 + 知识注入: + +**A. 存储层(df-storage,V7/V8 迁移)**: +- V7 建表 `knowledges`(id/kind/title/content/tags/status/confidence/reuse_count/verified/source_project/source_ref/created_at/updated_at),3 索引 +- V8 幂等补列 `embedding BLOB`(PRAGMA table_info 探测,同 v4/v5/v6 模式) +- `KnowledgeRecord`(models.rs),含 `embedding: Option>` +- `KnowledgeRepo`(crud.rs):`search()`(LIKE 双分支,按 kind 可选过滤)/ `list_by_status()`(CASE WHEN confidence 语义排序)/ `increment_reuse_count()`(SQL 原子 +1)/ `top_used()`;向量扩展:`set_embedding()` / `search_vector()`(纯 Rust 余弦批量比较,skip 维度不匹配);工具函数:`f32s_to_blob` / `blob_to_f32s` / `cosine_similarity` + +**B. 领域层(df-evolve)**: +- `KnowledgeStatus` 枚举(Candidate/PendingReview/Published/Archived,snake_case,`from_str`/`as_str`) +- `Confidence` 枚举(High/Medium/Low,同上) +- `Knowledge` struct:移除 `effectiveness` 字段,时间字段统一 String 毫秒(对齐全部既有 model) +- `pattern.rs`:清理死参数 `result_quality: f32` + +**C. IPC 层(src-tauri/commands/knowledge.rs 新建,11 个 Command)**: +- `knowledge_{list,get,search,create,update_status,record_reuse,list_candidates,archive,get_config,save_config,extract_now}` +- 状态转换合法矩阵硬编码:candidate→{pending_review|published|archived}; pending_review→{published|archived}; published→{archived} +- `knowledge_archive`(软删除,匹配 `ai_conversation_archive` 语义) +- 配置读写:`knowledge_{get,save}_config`(读写 `AppState.knowledge_config: Arc>`) +- 手动提炼:`knowledge_extract_now` + +**D. AI 集成(ai.rs,知识注入 + 提炼)**: +- `build_knowledge_context()`:`auto_inject` 开关 → `hybrid_search()` top-3 → `increment_reuse_count` fire-and-forget → 注入 system prompt 头部 +- `extract_knowledge_from_conversation()`:后台 spawn,min_messages≥4 守卫 → 取最后 6 条消息 → LLM JSON 提炼 → 逐条写 candidate;提炼失败仅 warn 不阻断 +- `maybe_spawn_extraction()`:agentic loop 两处正常退出统一调用(`OnComplete` 默认模式) + +**Phase 5.5 — 向量检索(Settings 开关,默认关闭)**: +- `LlmProvider::embed()` trait 方法:默认返回 Err,openai_compat 实现 POST `/v1/embeddings`(按 index 排序) +- `KnowledgeConfig` 加三字段:`vector_enabled`(默认 false)/ `embedding_provider_id`(排除 anthropic)/ `embedding_model` +- `generate_embedding()`:截断 8000 字 → build_provider → embed() +- `spawn_embedding_for_knowledge()`:知识发布时 fire-and-forget 生成嵌入(不在 candidate 阶段浪费调用) +- `hybrid_search()`:三层降级链——开关关→LIKE; embed 失败→LIKE; 正常→双信号排序(同时命中>仅 LIKE>仅向量 cos≥0.3);不引入 sqlite-vec(规避 Windows MSVC C 扩展编译风险) +- `Settings.vue`:语义检索开关 + v-if 联动 embedding provider 下拉 + 模型名输入 + +**E. 前端(TypeScript + Vue)**: +- `types.ts`:`KnowledgeRecord` / `CreateKnowledgeInput` / `KnowledgeConfig`(含 vector 三字段) +- `api/knowledge.ts`(新建,11 个 invoke 封装) +- `stores/knowledge.ts`(完全重写,getter 模式,消除全部 15 条 mock + localStorage) +- `Knowledge.vue`(完全重写:知识库/审核收件箱双 Tab + 状态徽章 + confidence 指示器 + 新增对话框 + 候选 badge) +- `App.vue`:侧栏候选 badge(`candidateCount` ref,store loadCandidates 后更新) + +**F. 配置设计**(AppState 内存 + IPC,非 Tauri config 插件): +- `KnowledgeConfig`(state.rs):auto_extract / trigger_mode / min_messages / idle_timeout_ms / auto_inject / vector_enabled / embedding_provider_id / embedding_model + +**代码变更**:df-storage(models.rs/crud.rs/migrations.rs)/ df-evolve(knowledge.rs/pattern.rs)/ src-tauri(state.rs + commands/knowledge.rs 新建 + commands/ai.rs + lib.rs)/ 前端(api/types.ts + api/knowledge.ts 新建 + api/index.ts + stores/knowledge.ts + views/Knowledge.vue + views/Settings.vue + App.vue)。约 1,400 行 Rust + 700 行 TS。 + +**验证**:cargo check 0 error ✓,vue-tsc 0 error ✓。**未 tauri dev 运行时实测** + +**遗留问题 / 下一步**: +1. tauri dev 实测 10 项功能验证清单(手动录入 / 审核流 / 归档 / 检索注入 / reuse_count / AI 提炼 OnComplete / AI 提炼 Manual / 配置修改 / 注入开关 / 矛盾场景) +2. 向量检索待实测(需配 embedding provider + 开启开关 + 发布 ≥1 条知识后验证混合排序) +3. Tier 2 待做:ai_node prompt 注入 / 工作流 NodeFailed→pitfall / 决策记录→architecture_pattern(前置 traceability 持久化) +4. 知识库 API 对外暴露(MCP Shell 封装,外部工具 Claude Code/CodeX/Cursor 使用) + +### [Sprint 16] 2026-06-13 — AI Chat LLM 优化收尾(Part C/D + Review 修复) + AI Node 单测 + +**工作内容**: +- **Part C LLM 并发控制**:`state.rs` 新增 `LlmConcurrency`(双层 `Arc>>` — 全局默认 3 / 单对话默认 2,运行时替换内层 Arc 重建);`ai.rs` 3 个叶子 LLM 调用点(stream_llm / generate_title / extract_knowledge)加 permit,acquire 顺序固定 global→per_conv 防死锁,stream 后显式 `drop` 不占工具执行槽;新增 `ai_set_concurrency_config` command(运行时调并发上限);`lib.rs` 注册 +- **Part D Settings UI**:`Settings.vue` 通用设置区加「全局最大并发 / 单对话并发」两 setting-row + 前端 clamp(1-10 / 1-5)+ debounce 300ms + localStorage 持久化 + onMounted 启动同步 + onUnmounted 清 timer +- **Review 两轮 8 修复**:首轮 6(① per_conv 单例语义注释 / ② save-extract 并发注释改正 / ③ permit 显式 drop / ④ Settings clamp / ⑤ 合并 to_chat_messages 进 all_messages_clone / ⑥ system prompt 超 budget warn)+ 复审 2(后端 set_concurrency 补 clamp 边界对齐前端 / onUnmounted 清 debounce timer) +- **AI Node 单测**:`ai_node.rs` 重构抽 `parse_params` 纯函数(execute 参数解析剥离,单测不 mock LLM 零网络)+ 7 单测(缺参×3 / prompt 上游优先回退 / config 取值 / 可选默认值 / 显式参数解析) +- **任务收尾**:#43/#44 → completed(代码 + 单测全绿,GUI 实测转跟踪);新增 #54(GUI 动态实测跟踪任务) + +**代码变更**: +- `crates/df-ai/src/context.rs`(Review ⑤⑥ — 合并 to_chat_messages / system prompt 超 budget warn) +- `src-tauri/src/state.rs`(LlmConcurrency 双层 Semaphore + per_conv 单例语义注释) +- `src-tauri/src/commands/ai.rs`(permit×3 + drop / process_tool_calls join_all 并行 / save 后台 spawn / ai_set_concurrency_config command + clamp) +- `src-tauri/src/lib.rs`(注册 ai_set_concurrency_config) +- `src/views/Settings.vue`(并发 UI + clamp + debounce + onUnmounted) +- `crates/df-nodes/src/ai_node.rs`(parse_params 重构 + 7 单测) +- `docs/02-架构设计/功能决策记录.md`(新增「AI Node」域记 parse_params 决策 + per_conv 退化条目) + +**验证**:cargo check 0 error ✓ / df-ai context 5 单测 ✓ / df-nodes ai_node 7 单测 ✓ / vue-tsc 0 error ✓。**未 tauri dev 运行时实测**(转 #54 跟踪) + +**遗留问题 / 下一步**: +1. **#54 GUI 动态实测**(用户跑 tauri dev,Claude 核对日志):长对话裁剪(context_trimmed 日志 + DB 全量)/ 多工具并行(Low join_all 时序)/ save 异步不阻塞 Completed / 双层 Semaphore 限流(多对话撞 RPM)/ Settings 并发即时生效 + 刷新保持 / DAG ai 节点真调 LLM +2. ~~AI Node 可补 GLM 集成测试~~ ✅ 已补并通过:`glm_live_complete`(`#[ignore]` + env var)真调 GLM glm-4-flash 返回"通过",usage 解析正确(provider 调用层验证;GUI DAG 触发仍属 #54) + +### [Sprint 17] 2026-06-13 — 模块文档↔代码一致性审查(4 文档纠错补全) + +**工作内容**: +- **审查范围**:docs/03 模块文档(df-ai / df-storage / df-workflow)+ docs/01 技术文档(SQLite-CRUD 模式)共 4 份 vs 对应 crate 代码,三代理并行审查 +- **🔴 硬伤 28 全修**:df-ai(LlmProvider trait 签名 / 删 OpenAI 虚构 idle-timeout + finished_received / ContextManager 方法名 replace_tool_result_content + build_for_request 返回元组 / Token 公式 / Anthropic usage take() / 工具归属)/ df-storage(V1 表名 / Repo 表名 releases·workflow_executions / search published 过滤 / increment_reuse_count updated_at / cosine epsilon / blob 截断)/ SQLite-CRUD 整篇过时(表 6→11 / CRUD 待→已 / 时间 INTEGER→TEXT / 写返回 ()→String·bool / 连接池→单连接 / schema.rs→migrations.rs)/ df-workflow(Node trait 按值 + 默认 false + node_type / 文件名 conditions·eventbus / 虚构事件区分已发未发) +- **⚪ 虚构 7 全清**:df-workflow(阻塞节点「等待人工操作」标未实现 / 持久化快照删改仅内存态 / 断点续跑补无暂停恢复机制)/ df-ai(conditions.rs 不存在,清状态表 + 决策缺口表虚构引用 / AiToolRegistry「12 工具」夸大改基础设施) +- **🟡 核心 8 + 碎琐 10 补全**:df-ai(apply_openai_sse / apply_anthropic_event 纯函数 / ContextManager 6 pub 方法 / Anthropic 事件全集 / 文件结构 router·stream / URL 三分支 / finish=length / max_tokens 4096 / PROTECT_COUNT / budget 公式)/ df-workflow(NodeRegistry / DagDef / StateMachine / Dag API / NodeContext·NodeOutput 字段 / EventBus API / 条件引擎默认放行 warn)/ df-storage(list_non_archived / set_archived 例外对照) + +**代码变更**:无(纯文档纠错,代码未动) + +**验证**:每项修正均经代理 Read 代码复核签名/SQL/行为后改;git diff 4 文档落地(df-ai 443 / df-storage 134 / SQLite-CRUD 56 / df-workflow 122 行变动) + +**遗留问题 / 下一步**: +1. 4 文档已与代码实质一致;后续若改 df-ai/df-storage/df-workflow 公开接口,需同步对应模块文档(文档治理:模块文档随代码变) +2. df-evolve(knowledge.rs / pattern.rs)本轮未审 — docs/03 无对应模块文档,若补可另起审查 + +### [Sprint 18] 2026-06-13 — 知识生命线(详情页 + 溯源 + 引用 + 生命周期) + +**工作内容**: +- **根因**:知识库 candidate 卡片点不开详情、不能编辑、看不到"为什么产生";reasoning 字段被 prompt 要求但写库丢弃(bug);increment_reuse_count 只 +1 不记去向;无状态变更历史。用户要求呈现知识完整生命周期:产生→审核→引用→归档,且约束异步不阻塞、代码简洁易扩展。 + +**A. 存储层(df-storage,V10 迁移)**: +- V10:幂等补 `knowledges.reasoning TEXT` 列 + 新建 `knowledge_events` 审计表(id/knowledge_id/event_type/source_ref/context_json/timestamp + 3 索引) +- `KnowledgeRecord` 加 `reasoning: Option`;新增 `KnowledgeEventRecord`(6 字段) +- `KnowledgeEventsRepo`(impl_repo! 宏 7 方法 + 扩展 `list_by_knowledge`/`list_by_knowledge_type`);5 处显式 SELECT 列列举 + from_row + insert/update body 全线接 reasoning + +**B. 后端埋点(src-tauri)**: +- **新建 `commands/knowledge_timeline.rs`** — `KnowledgeTimeline` 独立记录器(持 Arc),便捷方法 record_extracted/referenced/status_change/created,内部 `fire()` 吞错误 fire-and-forget。各业务流程只调一行,不内联写 SQL(代码简洁约束) +- `extract_knowledge_from_conversation`:回填 reasoning(修 bug)+ 查对话标题 + insert 后记 extracted 事件 +- `build_knowledge_context`:加 conv_id 参数 + spawn 块内 increment_reuse_count 后记 referenced 事件(conv_id+query);调用点上移 conv_id 快照 +- `knowledge_create/update_status`:记 created/status_changed 事件(归档复用 status_changed,删 dead code EVENT_ARCHIVED/record_archived) +- 3 新 IPC command:`knowledge_get_detail`(聚合 knowledge+events)/`knowledge_update`(部分更新编辑)/`knowledge_events`(查生命线);AppState 加 `knowledge_events` Repo;lib.rs 注册 + +**C. 前端**: +- types.ts:KnowledgeRecord 加 reasoning + 新增 KnowledgeEventRecord/KnowledgeDetailPayload/UpdateKnowledgeInput +- api/store:各加 getDetail/update/getEvents +- **Knowledge.vue 全面重构**:Ideas 式左右分栏(左卡片列表 @click 选中 / 右详情四分区 — ①基本信息+编辑 ②溯源(产生方式/来源对话/AI reasoning) ③引用记录(复用次数+最近 N 条) ④生命周期时间线)。可编辑 title/content/tags/confidence + +**关键决策**(详见 docs/02-架构设计/功能决策记录.md):① 独立 events 表非 JSON 嵌主表(引用数百次防行膨胀);② 独立 KnowledgeTimeline 记录器(不散落埋点);③ 归档复用 status_changed(防冗余);④ reasoning 回填(修 prompt 要求但丢弃的 bug) + +**代码变更**:df-storage(migrations/models/crud)+ src-tauri(state/knowledge_timeline 新建/ai/knowledge/lib/mod)+ 前端(types/api/knowledge.ts/stores/knowledge.ts/Knowledge.vue)= 12 文件 + +**验证**:cargo check --workspace ✓(仅既有警告)/ vue-tsc --noEmit ✓ 零错误 / cargo test -p df-storage ✓ 21 单测全过。**未 tauri dev 运行时实测** + +**遗留问题 / 下一步**: +1. **#54 GUI 实测**:AI 对话结束→提炼→收件箱→点详情看 reasoning+来源;发布→生命线 status_changed;新对话注入→referenced 事件;编辑 candidate→保存 +2. 老库历史 candidate 无 reasoning/events,详情页降级展示(手动录入/无溯源)——属预期兼容 +3. knowledge_events 纯追加表,长期可清理 >90 天 referenced 事件(本次不做) + +### [Sprint 19] 2026-06-14 — list_directory 防爆 + localStorage→SQLite 统一持久化 + token/发送 bug 修复 + +**工作内容**:用户报多个 AiChat bug + 提需求:list_directory 扫出 13782 项塞爆对话;token 显示"0 in";发送卡住/留框;LLM 并发上限太小;localStorage 散落要统一入 SQLite。同时用户并行主导 ai.rs→ai/ + ai.ts→composables 模块拆分(我观察配合,不接管)。 + +**A. list_directory 递归防爆(Part A)**: +- `list_dir_recursive`(ai/tool_registry.rs)加噪音目录剪枝(`.git`/`node_modules`/`target`/`dist`/`build`/`.next`/`.cache`/`__pycache__`/`.venv`/`venv`/`.idea`)——**列出但不深入内部**;硬上限 1000 条 + `truncated` 标志;默认 max_depth 3→2;加 `skip_noise_dirs` 参数(默认 true,AI 看编译产物 dist/target 时传 false) +- 根因:AI 广扫项目根传 `recursive:true`,噪音目录铺平致 13782 项塞进对话 message(UI 卡 + token 爆) + +**B. localStorage → SQLite 统一持久化(Part B)**: +- 新建 `app_settings` KV 表(V13 迁移)+ 手写 `SettingsRepo`(get/set/get_all/delete,不走 `impl_repo!` 宏因 KV 无固定 schema);4 IPC(settings_get/set/get_all/delete);AppState 加 settings 字段 +- 前端 `appSettings` store(reactive 缓存 + 300ms debounce + `useSetting` ref)+ api 封装 +- **11 key 迁移**:敏感 `df-connections` + UI 偏好(theme/language/ai-width/ai-ui/token/concurrency)+ `df-ai-active-conv`;**例外 `df-ai-gen`/`df-ai-text`**(流式临时快照,高频写留 localStorage)。App.vue onMounted 一次性迁移旧 localStorage + +**C. Bug 修复**: +- **token prompt=0**:GLM 流式 usage 带 completion 不带 prompt → 加估算兜底(provider 返回 0 时按请求消息 `TokenEstimator::estimate_message` 求和) +- **发送卡住/留框**:`sendMessage` IPC 失败时无 catch → streaming 永真(光标卡死)+ handleSend 回填输入(消息已显示);改 try/catch 回滚 streaming + 清空气泡占位 +- **LLM 并发上限**:三层(模板 max / JS Math.min / 后端 .clamp)全去上限,保留 min=1 + `per-conv ≤ global` 约束 + +**D. token 暴增诊断 + 清理**: +- 查 SQLite 实数据:对话 e4365d47 累积 in=115万 / out=2万 / 消息体 1.6MB +- 根因:**单条 list_directory 工具结果 1.2MB**(13782 项,Part A 修复前产生)落库后每轮重发 + save 累加 → 暴增;token 估算准,膨胀是真(非估算锅) +- 删除污染对话(`ai_conversations` 1 行 + `ai_tool_executions` 123 行) + +**E. 模块拆分(用户主导,本会话发生,我配合不接管)**: +- `ai.rs`(2651 行)→ `ai/` 11 子模块(agentic/audit/commands/conversation/knowledge_inject/mod/prompt/skills/stream_recv/title/tool_registry),旧 ai.rs 删 +- `stores/ai.ts`(749 行)→ 薄壳 + `composables/ai/*`(events/stream/send/conversations/window/panel) +- `df-evolve` crate 移除(workspace/代码零引用) +- 踩坑(glob 重导出命令宏符号 / super 路径失效 / 0 字节残留触发 E0761)已记入经验记录.md + +**关键决策**(详见功能决策记录 / 经验记录):① list_directory 剪枝 + skip_noise 保留访问能力(定点查/强制递归两路通);② KV 表统一持久化复用 SettingsRepo(兑现经验记录预言);③ token 估算兜底(provider 不报时按消息估);④ 发送失败回滚 streaming;⑤ 并发去上限保 per-conv≤global + +**代码变更**:src-tauri(`commands/ai/` 新模块 + `commands/settings.rs` + state.rs + lib.rs)+ crates/df-storage(migrations V13 + crud SettingsRepo)+ 前端(api/settings.ts + stores/appSettings.ts + App.vue/Settings.vue/composables/ai/* + i18n)≈ 25 文件 + +**验证**:cargo check ✓ / vue-tsc ✓ 零错 / tauri dev 启动 ✓ 运行;token/发送/并发/list_directory 已实测 + +**遗留 / 下一步**: +1. **Fix 1(待用户最终定)**:工具结果入库前截断 50KB(堵 `read_file` 1MB 洞 + 防御);Fix 2(放宽旧工具结果裁剪)因 Part A 已杀主源,延后不做 +2. Settings.vue(1037 行)拆 panel 子组件——早前建议,用户已做 ai.rs,此件待评估 +3. 诊断日志清理(handleEvent/approveToolCall 早期调试 console.log 残留) + +### [Sprint 20] 2026-06-14 — 项目管理模块代码审查 + 全局核对(3-agent review 验证) + +**工作内容**: 应用户要求审查「项目管理 / 项目导入 / 生命周期可用性」(后端 commands/project.rs + ai/tool_registry.rs + df-project/scan.rs + 前端 Projects.vue / ProjectDetail.vue)。先 3-agent 并行 review 出 15 项发现,剔幻觉 2 项(read_readme 命名误判 / conflict.name null 过度防御)后剩 13 项;再应要求**全局核对**(grep 全仓库 6 维度)确认每项发现是孤例还是系统性,保证全局成立有效。**本次仅审查 + 核对,未改代码。** + +**验证结论**: 13/13 发现**事实全部成立**,无幻觉。4 处行号偏差(① 249→263 / ⑤ 113→136 / ⑫ 130→145 / ③ 未标→271);2 处范围需补(⑧ 实 8 处非 3 处 / ③ 漏 relocate:212)。 + +**🔴 必须修复(3)**: +1. **AI delete_project 硬删绕过软删**(tool_registry.rs:263 `repo.delete` 物理 DELETE vs IPC project.rs:120 `soft_delete` 进回收站)—— 同一操作两种后果,AI 对话删 = 数据丢失不可恢复,UI 删 = 可恢复。**数据安全缺口** +2. **AI 缺回收站生命周期工具**(restore/purge/list_deleted 全无)—— ProjectRepo 有完整方法(crud.rs:572/591/610/634)+ IPC 全注册(project.rs:125-145),AI 层零覆盖,割裂工作流 +3. **collect_sample 同步 IO 阻塞 async**(project.rs:271 在 async fn 内直接同步调 df-project::scan)—— 对比 scan_project_stack(:182)用了 spawn_blocking,不一致 + +**🟡 建议改进(6)**: ④ normalize_path 两份重复(project.rs:154 + tool_registry.rs:180)/ ⑤ update 白名单双份不同步(tool_registry:136 硬编码 vs crud.rs:287 allowed_columns,idea_id 字段不同步)/ ⑥ AI 扫描 + 目录状态文案硬编码未走 $t(Projects.vue:33/195 + ProjectDetail.vue:93)/ ⑦ parseStack 两份重复(Projects.vue:143 + ProjectDetail.vue:321)/ ⑧ 原生 alert/confirm(全局 8 处非仅 ProjectDetail 3 处,Ideas.vue:378/391 同病)/ ⑨ collect_sample 无文件大小预检(scan.rs:151/205 read 全文后截断) + +**⚪ 可选(3)**: ⑩ LLM 无超时(project.rs:297 占 llm_concurrency permit 无限 await)/ ⑪ JSON 提取取首尾花括号(project.rs:368-369 多 JSON 场景错)/ ⑫ create_project 无 path 参数(tool_registry:145 需 create+bind 两步) + +**全局核对关键定性**(6 维度 grep 全仓库): +- **① 是 project 特例但最该修**: soft_delete/回收站机制**全仓库仅 project 一张表有**(task/idea/branch/release/workflow 本就物理删,无语义不一致);正因 project 唯一设计回收站,AI 硬删直接击穿其数据安全设计 +- **⑧ 系统性**: 全仓库 8 处原生弹窗(ProjectDetail 4 + Projects 2 + Ideas 2),非局部 +- **③ 范围补报**: 不只 :271,relocate_project_path:212 同步 detect_stack 同样阻塞,3 处未包 spawn_blocking vs 1 处包了 +- **src-tauri 层自身干净**: `std::fs` 同步调用零匹配,阻塞全在跨 crate 调同步的 df-project::scan +- **④⑤⑦ 精准**: 全仓库确为两份/一处,无更多副本;validate_path/resolve_workspace_path 是安全校验,职责不同非 normalize 重复 + +**未改代码**(待用户定推进批次) + +**遗留 / 下一步(修复批次建议)**: +- **批1 数据安全**: ①(AI delete 改 soft_delete)+ ②(补 restore/purge/list_trash 三工具,复用 ProjectRepo 对应方法) +- **批2 阻塞 + 去重**: ③(:270/:271/:212 三处统一 spawn_blocking)+ ④(抽公共 normalize_path 到 df-project/df-core)+ ⑤(tool_registry 复用 allowed_columns_for) +- **批3 前端**: ⑥(补 i18n key)+ ⑦(抽 src/utils/project.ts)+ ⑧(全局 8 处 alert/confirm 换 Arco Modal) +- ⚪ 可选: ⑨⑩⑪⑫ 按需 + +--- + +### [设计] 2026-06-14 — B-03 人工审批响应机制设计 + +**工作内容**:df-workflow HumanNode 审批响应机制设计(B-260614-03)。勘察确认链路基础设施 90% 已通(EventBus broadcast / `HumanApprovalResponse` 事件 / `approve_human_approval` IPC / 前端 store 已接通),唯一缺口为 `human_node.rs:55` 发完请求直接 return "同意"。设计:订阅时序铁律(`subscribe()` 先于 `send(Request)`)+ `tokio::select!` 循环(响应 / 超时 / 取消)+ execution_id+node_id 双键过滤 + Lagged 容忍。澄清 B-06/B-07 前置真实影响(B-06=并发隔离,非单流功能前置;B-07=取消必要非充分),拆 **B-03a**(响应等待 + 超时)/ **B-03b**(取消机制:`set_cancelled` + cancel IPC + 前端按钮)。 + +**产出**:完整设计文档 [B-03-人工审批响应机制.md](./docs/02-架构设计/B-03-人工审批响应机制.md);功能决策记录加「工作流人工审批节点(B-03)」摘要章节。 + +**下一步**:B-06(execution_id 下沉)/ B-07(共享 StateMachine)前置修复 → 落 B-03a 实现 → B-03b 取消机制补全。 + -
+ +
@@ -95,17 +97,39 @@ + + + +
⚠ {{ errorMsg }}
+
+ + diff --git a/src/components/ToolCard.vue b/src/components/ToolCard.vue new file mode 100644 index 0000000..ebfa440 --- /dev/null +++ b/src/components/ToolCard.vue @@ -0,0 +1,678 @@ + + + + + + + diff --git a/src/components/ToolCardList.vue b/src/components/ToolCardList.vue new file mode 100644 index 0000000..fdccf91 --- /dev/null +++ b/src/components/ToolCardList.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/composables/ai/useAiConversations.ts b/src/composables/ai/useAiConversations.ts new file mode 100644 index 0000000..65013c1 --- /dev/null +++ b/src/composables/ai/useAiConversations.ts @@ -0,0 +1,178 @@ +//! 对话管理 — load/list/new/switch/delete/rename/archive + 侧栏折叠态 +//! +//! 耦合: +//! - 多处调 useAiEvents.notifyConversationChanged +//! - newConversation/switchConversation 写 appSettings 持久化活跃会话 id + +import { aiApi } from '../../api' +import { useAppSettingsStore } from '../../stores/appSettings' +import { state } from '../../stores/ai' +import { notifyConversationChanged } from './useAiEvents' +import { persistUiState } from './useAiPanel' +import type { AiToolCallInfo } from '../../api/types' + +const appSettings = useAppSettingsStore() + +/** 拉取会话列表;首次加载时若 appSettings 仍有有效 active-conv 且当前无活跃对话则恢复 */ +async function loadConversations() { + try { + state.conversations = await aiApi.listConversations() + // 恢复上次活跃对话(仅在首次加载、ID 仍有效且当前无活跃对话时) + const pending = appSettings.get('df-ai-active-conv', null) + if (pending && !state.activeConversationId && state.conversations.some(c => c.id === pending)) { + await switchConversation(pending) + } + } catch { + // 静默失败,不影响主流程 + } +} + +/** 新建空对话并切过去 */ +async function newConversation() { + const result = await aiApi.createConversation() + state.activeConversationId = result.id + void appSettings.set('df-ai-active-conv', result.id) + state.messages = [] + state.currentText = '' + state.pendingApprovals = [] + state.streaming = false + await loadConversations() + notifyConversationChanged() +} + +/** 切换到指定会话:加载历史消息(含 tool_calls 回填 + tool_result 映射 + pending 审批恢复) */ +export async function switchConversation(id: string) { + // 允许生成中切换:后台继续生成,事件按 conversation_id 路由不污染当前视图 + const detail = await aiApi.switchConversation(id) + state.activeConversationId = id + void appSettings.set('df-ai-active-conv', id) + + try { + const rawMsgs = typeof detail.messages === 'string' + ? JSON.parse(detail.messages) + : detail.messages + + // 构建 tool_call_id → tool_result 映射,用于回填工具执行结果 + const toolResultMap = new Map() + for (const m of rawMsgs) { + if (m.role === 'tool' && m.tool_call_id) { + toolResultMap.set(m.tool_call_id, m.content || '') + } + } + + state.messages = rawMsgs + .filter((m: any) => m.role !== 'tool') + .map((m: any, i: number) => ({ + id: `loaded-${i}`, + role: m.role, + content: m.content || '', + model: m.model, + timestamp: Date.now(), + toolCalls: m.tool_calls?.map((tc: any) => ({ + id: tc.id, + name: tc.function?.name || '', + args: typeof tc.function?.arguments === 'string' + ? JSON.parse(tc.function.arguments || '{}') + : tc.function?.arguments || {}, + status: 'completed' as const, + result: toolResultMap.get(tc.id), + })), + })) + } catch { + state.messages = [] + } + + // 加载历史对话 token 总量(来自 DB summary);切换对话清空实时值 + const conv = state.conversations.find(c => c.id === id) + if (conv && conv.prompt_tokens != null && conv.completion_tokens != null) { + state.convTokenTotal = { + prompt: conv.prompt_tokens, + completion: conv.completion_tokens, + total: conv.prompt_tokens + conv.completion_tokens, + } + } else { + state.convTokenTotal = null + } + state.lastTokenUsage = null + + state.currentText = '' + // 恢复该对话积压的待审批:重启后后端从审计表重建了 pending_approvals, + // 此处查回并把对应 toolCard.status 置 pending_approval,使审批卡片重新可见 + try { + const pending = await aiApi.pendingToolCalls(id) + if (pending.length) { + const pendingIds = new Set(pending.map(p => p.tool_call_id)) + const restored: AiToolCallInfo[] = [] + for (const m of state.messages) { + for (const tc of (m.toolCalls || [])) { + if (pendingIds.has(tc.id)) { + tc.status = 'pending_approval' + restored.push({ id: tc.id, name: tc.name, args: tc.args, status: 'pending_approval' }) + } + } + } + state.pendingApprovals = restored + } else { + state.pendingApprovals = [] + } + } catch { + state.pendingApprovals = [] + } +} + +/** 删除会话;若删的是当前活跃会话则清空消息+移除活跃 id 持久化 */ +async function deleteConversation(id: string) { + await aiApi.deleteConversation(id) + if (state.activeConversationId === id) { + state.activeConversationId = null + void appSettings.remove('df-ai-active-conv') + state.messages = [] + } + await loadConversations() + notifyConversationChanged() +} + +/** 重命名会话(后端 + 本地侧栏摘要同步) */ +async function renameConversation(id: string, title: string) { + await aiApi.renameConversation(id, title) + // 本地同步更新侧栏摘要标题 + const conv = state.conversations.find(c => c.id === id) + if (conv) conv.title = title + notifyConversationChanged() +} + +/** 归档/取消归档(后端 + 本地侧栏分组同步) */ +async function archiveConversation(id: string, archived: boolean) { + await aiApi.archiveConversation(id, archived) + // 本地同步归档态(侧栏立即移入/移出归档分组) + const conv = state.conversations.find(c => c.id === id) + if (conv) conv.archived = archived + notifyConversationChanged() +} + +/** 折叠/展开归档分组 */ +function toggleArchivedFold() { + state.archivedCollapsed = !state.archivedCollapsed + persistUiState() +} + +/** 展开/收起侧栏(会话列表) */ +function toggleSidebar() { + state.sidebarOpen = !state.sidebarOpen + persistUiState() +} + +export function useAiConversations() { + return { + loadConversations, + newConversation, + switchConversation, + deleteConversation, + renameConversation, + archiveConversation, + toggleArchivedFold, + toggleSidebar, + } +} + +export { loadConversations } diff --git a/src/composables/ai/useAiEvents.ts b/src/composables/ai/useAiEvents.ts new file mode 100644 index 0000000..896b0bf --- /dev/null +++ b/src/composables/ai/useAiEvents.ts @@ -0,0 +1,257 @@ +//! AI 事件监听与分发 — startListener/stopListener/handleEvent 及其辅助函数 +//! +//! 模块级私有状态(不进 reactive): +//! - _unlistenAiEvent / _unlistenConvChanged: 已注册的 unlistener +//! - _startPromise: startListener 并发去重(防 onMounted 与 sendMessage 首发竞态重复注册) +//! - _msgCounter: 客户端消息自增 id(全局唯一,多个 composable 共用 nextMsgId) +//! +//! 耦合: +//! - handleEvent 调 useAiConversations.loadConversations、useAiSend.drainQueue、 +//! useAiStream.{resetStreamWatchdog,clearStreamWatchdog}、本模块 flushCurrentText/findToolCall/notifyConversationChanged +//! - onStreamTimeout(useAiStream) 调本模块 nextMsgId + +import { listen, emit } from '@tauri-apps/api/event' +import { aiApi } from '../../api' +import { useAppSettingsStore } from '../../stores/appSettings' +import { state } from '../../stores/ai' +import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream' +import { drainQueue } from './useAiSend' +import { loadConversations } from './useAiConversations' +import type { AiChatEvent, AiToolCallInfo } from '../../api/types' + +let _unlistenAiEvent: (() => void) | null = null +let _unlistenConvChanged: (() => void) | null = null +// startListener 并发去重:防 onMounted 与 sendMessage 首发竞态下重复注册 listener +// (两回调写同一 state.currentText → 流式文字双倍) +let _startPromise: Promise | null = null +let _msgCounter = 0 + +const appSettings = useAppSettingsStore() + +/** 全局消息 id 自增(供 events/stream/send/window 各 composable 共享同一计数器) */ +export function nextMsgId(): number { + return ++_msgCounter +} + +/** 通知会话列表发生变化(供 newConversation/deleteConversation/rename/archive 等触发刷新侧栏) */ +export function notifyConversationChanged() { + emit('ai-conversation-changed', {}) +} + +/** 后端原始错误转用户友好提示 */ +export function friendlyError(raw: string): string { + if (/404|not\s*found/i.test(raw)) return '调用失败:接口地址或模型不存在,请检查 Provider 配置' + if (/401|403|unauthorized|api[_\s-]?key/i.test(raw)) return '调用失败:API Key 无效或无权限' + if (/timeout|超时/i.test(raw)) return '响应超时,请重试' + if (/network|connection|ECONN|网络|连接/i.test(raw)) return '网络连接失败,请检查网络' + return raw +} + +/** 把流式累积的 currentText 回填到最后一条 assistant 消息(AiAgentRound/AiCompleted 收尾共用) */ +export function flushCurrentText() { + if (!state.currentText) return + const last = state.messages[state.messages.length - 1] + if (last && last.role === 'assistant') last.content = state.currentText +} + +/** 在全部消息中查找指定 id 的工具调用卡片(用于状态流转) */ +export function findToolCall(id: string): AiToolCallInfo | undefined { + for (const msg of state.messages) { + if (msg.toolCalls) { + const tc = msg.toolCalls.find(t => t.id === id) + if (tc) return tc + } + } + return undefined +} + +/** token 用量展示开关(读 appSettings,与 Settings.vue 共享 key `df-show-token-usage`) */ +function isShowTokenUsage(): boolean { + return appSettings.get('df-show-token-usage', false) +} + +/** 后端事件分发:按 conversation_id 路由,流式累积文本,工具状态流转,看门狗联动 */ +export function handleEvent(event: AiChatEvent) { + const convId = event.conversation_id + // 首次收到事件时同步当前对话 id(后端自动建对话的场景) + // 同步写 appSettings(SQLite):刷新页面后 loadConversations 据此恢复上次会话 + if (convId && !state.activeConversationId) { + state.activeConversationId = convId + void appSettings.set('df-ai-active-conv', convId) + } + // 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误时刷新侧边栏 + const isCurrent = !convId || convId === state.activeConversationId + if (!isCurrent) { + if (event.type === 'AiCompleted' || event.type === 'AiError') { + state.generatingConvId = null + void loadConversations() + } + return + } + // 标记正在生成的对话(完成/错误事件除外) + if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError') { + state.generatingConvId = convId + } + // 流式看门狗:活跃事件(delta/工具/新轮/审批结果)重置;审批等待/完成/错误在 case 内 clear + if (!['AiApprovalRequired', 'AiCompleted', 'AiError'].includes(event.type)) { + resetStreamWatchdog() + } + switch (event.type) { + case 'AiTextDelta': + state.currentText += event.delta + break + + case 'AiAgentRound': { + // Agent 循环新一轮:保存当前文本到上一条 assistant 消息,新建空 assistant 消息 + flushCurrentText() + state.currentText = '' + state.messages.push({ + id: `ai-${nextMsgId()}`, + role: 'assistant', + content: '', + timestamp: Date.now(), + }) + break + } + + case 'AiToolCallStarted': { + const info: AiToolCallInfo = { + id: event.id, + name: event.name, + args: event.args, + status: 'running', + } + const lastMsg = state.messages[state.messages.length - 1] + if (lastMsg && lastMsg.role === 'assistant') { + lastMsg.toolCalls = lastMsg.toolCalls || [] + lastMsg.toolCalls.push(info) + } + break + } + + case 'AiToolCallCompleted': { + const tc = findToolCall(event.id) + if (tc) { + tc.status = 'completed' + tc.result = event.result + } + state.pendingApprovals = state.pendingApprovals.filter(p => p.id !== event.id) + break + } + + case 'AiApprovalRequired': { + clearStreamWatchdog() // 等用户审批,不计超时 + const info: AiToolCallInfo = { + id: event.id, + name: event.name, + args: event.args, + status: 'pending_approval', + } + state.pendingApprovals.push(info) + const tc = findToolCall(event.id) + if (tc) { + tc.status = 'pending_approval' + tc.reason = event.reason + } + break + } + + case 'AiApprovalResult': { + if (!event.approved) { + const tc = findToolCall(event.id) + if (tc) { + tc.status = 'rejected' + // 与后端落库一致:拒绝结果回填卡片,供 UI 展示拒绝原因 + tc.result = '用户拒绝了此操作' + } + state.pendingApprovals = state.pendingApprovals.filter(p => p.id !== event.id) + } + break + } + + case 'AiCompleted': { + clearStreamWatchdog() + flushCurrentText() + state.currentText = '' + state.streaming = false + state.generatingConvId = null + // token 用量记录(开关开时):lastTokenUsage 供当前回复展示,convTokenTotal 累加对话总量 + if (isShowTokenUsage()) { + state.lastTokenUsage = { + prompt: event.prompt_tokens, + completion: event.completion_tokens, + total: event.total_tokens, + } + if (state.convTokenTotal) { + state.convTokenTotal.prompt += event.prompt_tokens + state.convTokenTotal.completion += event.completion_tokens + state.convTokenTotal.total += event.total_tokens + } else { + state.convTokenTotal = { prompt: event.prompt_tokens, completion: event.completion_tokens, total: event.total_tokens } + } + } + // 清理分离窗口生成态快照 + localStorage.removeItem('df-ai-gen') + localStorage.removeItem('df-ai-text') + void loadConversations() + notifyConversationChanged() + // 队列续发:当前完成后自动发下一条(后端 generating 已复位,不会被"正在生成中"拒绝) + drainQueue() + break + } + + case 'AiError': { + clearStreamWatchdog() + state.streaming = false + state.generatingConvId = null + state.currentText = '' + localStorage.removeItem('df-ai-gen') + localStorage.removeItem('df-ai-text') + state.messages.push({ + id: `err-${nextMsgId()}`, + role: 'assistant', + content: friendlyError(event.error), + isError: true, + timestamp: Date.now(), + }) + break + } + } +} + +/** 启动事件监听(幂等 + 并发去重,onMounted 与 sendMessage 首发竞态不会重复注册) */ +export async function startListener() { + // 已注册 → 直接复用(幂等;sendMessage 每次调用不重复注册) + if (_unlistenAiEvent && _unlistenConvChanged) return + // 并发去重:防 onMounted 与 sendMessage 首发竞态重复注册 listener → 文字双倍 + if (_startPromise) return _startPromise + _startPromise = (async () => { + _unlistenAiEvent = await aiApi.onEvent(handleEvent) + _unlistenConvChanged = await listen('ai-conversation-changed', () => { void loadConversations() }) + })() + try { + await _startPromise + } finally { + _startPromise = null + } +} + +/** 停止事件监听(卸载时调用,释放后端 listener) */ +function stopListener() { + _unlistenAiEvent?.() + _unlistenConvChanged?.() + _unlistenAiEvent = null + _unlistenConvChanged = null +} + +export function useAiEvents() { + return { + startListener, + stopListener, + handleEvent, + flushCurrentText, + findToolCall, + friendlyError, + notifyConversationChanged, + } +} diff --git a/src/composables/ai/useAiPanel.ts b/src/composables/ai/useAiPanel.ts new file mode 100644 index 0000000..4cf8b93 --- /dev/null +++ b/src/composables/ai/useAiPanel.ts @@ -0,0 +1,107 @@ +//! 面板与持久化 — togglePanel/toggleMaximize + UI 持久化(restoreUiState/persistUiState) +//! + token 开关 + providers/skills/clearChat +//! +//! 模块级私有: +//! - 启动恢复:模块加载时 restoreUiState()(从 appSettings 读上次 UI 布局) +//! - watch appSettings.cache['df-ai-ui']:loadAll() 异步填充后再次应用 UI 布局 +//! +//! 耦合: +//! - togglePanel/toggleMaximize/toggleArchivedFold/toggleSidebar 调 persistUiState + +import { watch } from 'vue' +import { aiApi } from '../../api' +import { useAppSettingsStore } from '../../stores/appSettings' +import { state } from '../../stores/ai' + +const appSettings = useAppSettingsStore() + +// 启动恢复:从 appSettings(SQLite)读取上次的 UI 布局(模块级单例 state) +// detached/docked 不恢复 — 重启后分离窗口必然不存在,必须回 false +export function restoreUiState() { + const s = appSettings.get('df-ai-ui', null) + if (!s) return + if (typeof s.panelOpen === 'boolean') state.panelOpen = s.panelOpen + if (typeof s.maximized === 'boolean') state.maximized = s.maximized + if (typeof s.sidebarOpen === 'boolean') state.sidebarOpen = s.sidebarOpen + if (typeof s.archivedCollapsed === 'boolean') state.archivedCollapsed = s.archivedCollapsed +} +restoreUiState() + +// loadAll() 异步填充缓存后,再次应用 UI 布局(模块级 state,watch 仅触发一次) +watch( + () => appSettings.cache['df-ai-ui'], + (v) => { + if (!v) return + const s = v as any + if (typeof s.panelOpen === 'boolean') state.panelOpen = s.panelOpen + if (typeof s.maximized === 'boolean') state.maximized = s.maximized + if (typeof s.sidebarOpen === 'boolean') state.sidebarOpen = s.sidebarOpen + if (typeof s.archivedCollapsed === 'boolean') state.archivedCollapsed = s.archivedCollapsed + }, +) + +/** 持久化 UI 布局到 appSettings(debounced 写 SQLite) */ +export function persistUiState() { + void appSettings.set('df-ai-ui', { + panelOpen: state.panelOpen, + maximized: state.maximized, + sidebarOpen: state.sidebarOpen, + archivedCollapsed: state.archivedCollapsed, + }) +} + +/** 切换面板展开/收起(收起时同步退出最大化,避免 v-show 卡死 main 交互) */ +function togglePanel() { + state.panelOpen = !state.panelOpen + // 关闭面板时同步退出最大化,避免
被 v-show 隐藏导致其他页面无法交互 + if (!state.panelOpen) state.maximized = false + persistUiState() +} + +/** 最大化/还原侧栏宽度 */ +function toggleMaximize() { + state.maximized = !state.maximized + persistUiState() +} + +// ── Providers / Skills / 清屏 ── + +/** 拉取 provider 列表(用于顶部 bar 展示与切换) */ +async function loadProviders() { + state.providers = await aiApi.listProviders() +} + +/** 拉取本机技能列表(用于 `/` 联想,静默失败) */ +async function loadSkills() { + try { + state.skills = await aiApi.listSkills() + } catch { + // 静默失败,不影响主流程 + } +} + +/** 切换活跃 provider(后端 + 本地 state) */ +async function setProvider(providerId: string) { + await aiApi.setProvider(providerId) + state.activeProvider = providerId +} + +/** 清空当前会话消息(后端 + 本地 state) */ +async function clearChat() { + await aiApi.clearChat() + state.messages = [] + state.currentText = '' + state.pendingApprovals = [] + state.streaming = false +} + +export function useAiPanel() { + return { + togglePanel, + toggleMaximize, + loadProviders, + loadSkills, + setProvider, + clearChat, + } +} diff --git a/src/composables/ai/useAiSend.ts b/src/composables/ai/useAiSend.ts new file mode 100644 index 0000000..d62c127 --- /dev/null +++ b/src/composables/ai/useAiSend.ts @@ -0,0 +1,124 @@ +//! 发送与队列管理 — sendMessage/approveToolCall + 队列(drain/cancel/clear) + stopChat +//! +//! 模块级私有: +//! - 无(队列存在 state.queue 中,看门狗/审批状态机依赖 events 与 stream) +//! +//! 耦合: +//! - sendMessage 调 useAiEvents.startListener(useAiEvents 导出但不在解构集中暴露给组件, +//! 故 startListener 同时经 useAiEvents 导出,sendMessage 直接 import 调用) +//! - sendMessage 调 useAiStream.resetStreamWatchdog +//! - drainQueue 在 sendMessage 完成后由 handleEvent(AiCompleted) 调用 — 故 drainQueue 必须为模块级 export + +import { aiApi } from '../../api' +import { useAppSettingsStore } from '../../stores/appSettings' +import { state } from '../../stores/ai' +import { resetStreamWatchdog } from './useAiStream' +import { startListener } from './useAiEvents' +import { nextMsgId } from './useAiEvents' + +const appSettings = useAppSettingsStore() + +/// 待发送队列上限(超过抛错提示用户) +const QUEUE_LIMIT = 10 + +/** 取出队首并发送(AiCompleted 触发,此时 streaming 已 false) */ +export function drainQueue() { + if (state.queue.length === 0) return + const next = state.queue.shift()! + void sendMessage(next.text, next.skill) +} + +/** 发送消息:生成中入队,否则推送 user+air 气泡并触发后端流式 */ +async function sendMessage(text: string, skill?: string) { + if (!text.trim()) return + + // 生成中:进入待发送队列,当前对话完成后(AiCompleted)由 drainQueue 自动续发 + if (state.streaming) { + if (state.queue.length >= QUEUE_LIMIT) { + throw new Error(`待发送队列已满(最多 ${QUEUE_LIMIT} 条)`) + } + state.queue.push({ text: text.trim(), skill: skill || undefined }) + return + } + + state.messages.push({ + id: `user-${nextMsgId()}`, + role: 'user', + content: text.trim(), + timestamp: Date.now(), + }) + + const aiMsgId = `ai-${nextMsgId()}` + state.messages.push({ + id: aiMsgId, + role: 'assistant', + content: '', + timestamp: Date.now(), + }) + + state.streaming = true + state.currentText = '' + resetStreamWatchdog() // 启动流式看门狗,无数据超时兜底 + + await startListener() + const raw = appSettings.get('df-ai-language', 'auto') + const lang = raw === 'auto' + ? appSettings.get('df-language', 'zh-CN') + : raw + try { + await aiApi.sendMessage(text.trim(), lang, skill) + } catch (e) { + // IPC 失败(spawn 前/provider 配置错等):回滚 streaming 防光标卡死 + 移除空气泡占位; + // 重新抛出由 handleSend 回填输入框,用户可重试 + state.streaming = false + state.messages = state.messages.filter(m => m.id !== aiMsgId) + throw e + } +} + +/** 工具审批:乐观置 running,IPC 失败时改 completed+错误文案(不回滚避免卡按钮) */ +async function approveToolCall(toolCallId: string, approved: boolean) { + // 乐观置运行中,禁用审批按钮防重复点击(后端事件回来后转 completed/rejected) + const tc = state.messages + .flatMap(m => m.toolCalls || []) + .find(t => t.id === toolCallId) + if (tc) tc.status = 'running' + try { + await aiApi.approve(toolCallId, approved) + } catch (e) { + // 仅 IPC 真失败(审批已处理/网络断)走到这里:后端工具失败已改走 emit completed,不进此分支 + // 不回滚 pending_approval(会卡死按钮),改设 completed + 错误提示,让用户知晓失败 + console.error('[AI] 审批操作未送达后端:', e) + if (tc) { + tc.status = 'completed' + tc.result = `审批操作未送达后端:${e instanceof Error ? e.message : String(e)}` + } + throw e + } +} + +/** 取消队列中指定位置的消息 */ +function cancelQueued(index: number) { + state.queue.splice(index, 1) +} + +/** 清空整个待发送队列 */ +function clearQueue() { + state.queue = [] +} + +/** 停止当前生成:仅发停止信号,streaming 状态由后端 AiCompleted 事件收尾 */ +async function stopChat() { + await aiApi.stopChat() +} + +export function useAiSend() { + return { + sendMessage, + approveToolCall, + drainQueue, + cancelQueued, + clearQueue, + stopChat, + } +} diff --git a/src/composables/ai/useAiStream.ts b/src/composables/ai/useAiStream.ts new file mode 100644 index 0000000..07f5528 --- /dev/null +++ b/src/composables/ai/useAiStream.ts @@ -0,0 +1,52 @@ +//! 流式看门狗 — 无数据超时兜底 +//! +//! 后端断连/异常不发 AiCompleted/AiError 时,前端自动收尾+提示, +//! 避免 streaming 永久 true 卡死对话(用户报"流式文字停在中途"即此类)。 +//! +//! 耦合说明: +//! - sendMessage 启动生成时 resetStreamWatchdog() 启动计时 +//! - handleEvent 每个活跃事件(delta/工具/新轮/审批结果)重置;审批等待/完成/错误 clear +//! - 超时回调 onStreamTimeout 直接改 state 并补一条错误消息 + +import { state } from '../../stores/ai' +import { nextMsgId } from './useAiEvents' + +/// ≥ 后端 STREAM_IDLE_TIMEOUT(120s, ai.rs:848)+余量; +/// 前端若短于后端,慢首 token 会被前端先误杀 +export const STREAM_TIMEOUT_MS = 130000 + +let _streamWatchdog: ReturnType | null = null + +/** 看门狗超时回调:收尾 streaming 态并补错误消息 */ +export function onStreamTimeout() { + state.streaming = false + state.generatingConvId = null + state.currentText = '' + clearStreamWatchdog() + state.messages.push({ + id: `timeout-${nextMsgId()}`, + role: 'assistant', + content: '⚠ 响应中断(长时间无数据流)。可能是网络断连或后端异常,请重试。', + isError: true, + timestamp: Date.now(), + }) +} + +/** 启动/重置看门狗(活跃事件或 sendMessage 时调用) */ +export function resetStreamWatchdog() { + if (_streamWatchdog) clearTimeout(_streamWatchdog) + _streamWatchdog = setTimeout(onStreamTimeout, STREAM_TIMEOUT_MS) +} + +/** 清除看门狗(完成/错误/审批等待时调用) */ +export function clearStreamWatchdog() { + if (_streamWatchdog) { clearTimeout(_streamWatchdog); _streamWatchdog = null } +} + +export function useAiStream() { + return { + onStreamTimeout, + resetStreamWatchdog, + clearStreamWatchdog, + } +} diff --git a/src/composables/ai/useAiWindow.ts b/src/composables/ai/useAiWindow.ts new file mode 100644 index 0000000..db35b07 --- /dev/null +++ b/src/composables/ai/useAiWindow.ts @@ -0,0 +1,167 @@ +//! 窗口分离模式 — detach/reattach/resumeInDetached/closeDetachedWindow/dock/syncToMain/startFollowMain/stopFollowMain +//! +//! 模块级私有(不进 reactive): +//! - _unlistenMove / _unlistenResize: 主窗口 move/resize 跟随的 unlistener +//! +//! 耦合: +//! - detachPanel/resumeInDetached 调 useAiConversations.switchConversation +//! - resumeInDetached 调 useAiEvents.nextMsgId 占位 ai 气泡 +//! - reattachPanel/detachPanel 调 useAiPanel.persistUiState + +import { state } from '../../stores/ai' +import { nextMsgId } from './useAiEvents' +import { switchConversation } from './useAiConversations' +import { persistUiState } from './useAiPanel' + +// ── 主窗口事件跟随 unlistener ── +let _unlistenMove: (() => void) | null = null +let _unlistenResize: (() => void) | null = null + +/** 分离 AI 面板到独立窗口(若已存在则聚焦);快照当前生成态供分离窗口接管 */ +async function detachPanel() { + const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow') + const existing = await WebviewWindow.getByLabel('ai-detached') + if (existing) { + await existing.setFocus() + return + } + // 快照当前生成态,供分离窗口接管(保持正在进行的对话) + if (state.streaming && state.generatingConvId) { + localStorage.setItem('df-ai-gen', state.generatingConvId) + localStorage.setItem('df-ai-text', state.currentText) + } + const convId = state.activeConversationId + const sep = convId ? `?conv=${encodeURIComponent(convId)}` : '' + const url = window.location.origin + window.location.pathname + '#/ai-detached' + sep + const win = new WebviewWindow('ai-detached', { + url, + title: 'DevFlow AI', + width: 520, + height: 700, + minWidth: 360, + minHeight: 400, + center: true, + decorations: true, + }) + win.once('tauri://created', () => { + state.panelOpen = false + persistUiState() + }) + win.once('tauri://error', (e) => { + console.error('[AI] 窗口创建失败:', e) + state.detached = false + }) + win.once('tauri://destroyed', () => { + state.detached = false + state.docked = false + stopFollowMain() + }) + state.detached = true +} + +/** 关闭分离窗口并回到内嵌面板(主窗口侧调用) */ +async function reattachPanel() { + stopFollowMain() + const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow') + const win = await WebviewWindow.getByLabel('ai-detached') + if (win) { + await win.close() + } + state.detached = false + state.docked = false + state.panelOpen = true + persistUiState() + localStorage.removeItem('df-ai-gen') + localStorage.removeItem('df-ai-text') +} + +/** 分离窗口挂载时接管主窗口当前对话(含生成中态) */ +async function resumeInDetached(convId: string | null) { + const id = convId || state.activeConversationId + if (id) await switchConversation(id) + const gen = localStorage.getItem('df-ai-gen') + if (gen) { + // 接管正在生成的对话:恢复已生成文本并补占位,后续 delta 继续追加 + state.currentText = localStorage.getItem('df-ai-text') || '' + state.messages.push({ + id: `ai-${nextMsgId()}`, + role: 'assistant', + content: '', + timestamp: Date.now(), + }) + state.streaming = true + state.generatingConvId = gen + } +} + +/** 关闭分离窗口自身(分离窗口侧调用) */ +async function closeDetachedWindow() { + state.docked = false + stopFollowMain() + localStorage.removeItem('df-ai-gen') + localStorage.removeItem('df-ai-text') + const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow') + await getCurrentWebviewWindow().close() +} + +/** 吸附/取消吸附:AI 窗口跟随主窗口右侧 */ +async function dockDetached() { + // 取消吸附 + if (state.docked) { + state.docked = false + stopFollowMain() + return + } + + try { + await syncToMain() + state.docked = true + startFollowMain() + } catch (e) { + console.error('[AI] 吸附失败:', e) + } +} + +/** 同步 AI 窗口位置到主窗口右侧(保留 4px 间隙) */ +async function syncToMain() { + const { WebviewWindow, getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow') + const { PhysicalPosition, PhysicalSize } = await import('@tauri-apps/api/dpi') + const mainWin = await WebviewWindow.getByLabel('main') + const aiWin = getCurrentWebviewWindow() + if (!mainWin) return + const pos = await mainWin.outerPosition() + const size = await mainWin.innerSize() + await aiWin.setPosition(new PhysicalPosition(pos.x + size.width + 4, pos.y)) + await aiWin.setSize(new PhysicalSize(600, size.height)) +} + +/** 启动主窗口 move/resize 跟随(吸附时调用) */ +async function startFollowMain() { + stopFollowMain() + const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow') + const mainWin = await WebviewWindow.getByLabel('main') + if (!mainWin) return + _unlistenMove = await mainWin.onMoved(() => { void syncToMain() }) + _unlistenResize = await mainWin.onResized(() => { void syncToMain() }) +} + +/** 停止主窗口跟随 */ +function stopFollowMain() { + _unlistenMove?.() + _unlistenResize?.() + _unlistenMove = null + _unlistenResize = null +} + +export function useAiWindow() { + return { + detachPanel, + reattachPanel, + resumeInDetached, + closeDetachedWindow, + dockDetached, + syncToMain, + startFollowMain, + stopFollowMain, + } +} diff --git a/src/constants/project.ts b/src/constants/project.ts new file mode 100644 index 0000000..66123d8 --- /dev/null +++ b/src/constants/project.ts @@ -0,0 +1,101 @@ +//! 项目 & 任务状态映射常量 +//! +//! 统一各视图(Projects/ProjectDetail/Tasks/Dashboard)的 label / badge / 阶段进度, +//! 根治此前三套状态映射互相矛盾、任务文案两处不一致的问题。 + +// ── 项目状态 ── + +// 值为 i18n key,实际文案走 $t('projects.status.')(见 src/i18n/{zh-CN,en}/projects.ts) +export const PROJECT_STATUS_LABELS: Record = { + planning: 'projects.status.planning', + in_progress: 'projects.status.in_progress', + paused: 'projects.status.paused', + completed: 'projects.status.completed', + cancelled: 'projects.status.cancelled', + active: 'projects.status.active', +} + +/** 项目状态 → 卡片 badge 样式 class */ +export const PROJECT_STATUS_BADGE_CLASS: Record = { + planning: 'stage-design', + in_progress: 'stage-coding', + paused: 'stage-testing', + completed: 'stage-release', + cancelled: 'stage-design', +} + +/** 项目状态 → 阶段/进度信息(详情页 pipeline + Dashboard 进度条共用) */ +export interface ProjectStageInfo { + /** pipeline 步骤索引 0-4: idea/requirement/coding/testing/release */ + stepIndex: number + /** 进度百分比 */ + progress: number + /** Dashboard dot/chip 颜色档: coding/testing/release */ + stage: string +} + +export const PROJECT_STAGE_INFO: Record = { + planning: { stepIndex: 0, progress: 20, stage: 'coding' }, + in_progress: { stepIndex: 2, progress: 55, stage: 'coding' }, + paused: { stepIndex: 2, progress: 40, stage: 'testing' }, + completed: { stepIndex: 4, progress: 100, stage: 'release' }, + cancelled: { stepIndex: 0, progress: 0, stage: 'testing' }, +} + +export function projectStatusLabel(status: string): string { + return PROJECT_STATUS_LABELS[status] ?? status +} + +export function projectBadgeClass(status: string): string { + return PROJECT_STATUS_BADGE_CLASS[status] ?? 'stage-design' +} + +export function projectStageInfo(status: string): ProjectStageInfo { + return PROJECT_STAGE_INFO[status] ?? PROJECT_STAGE_INFO.planning +} + +// ── 任务状态 ── + +// 值为 i18n key,实际文案走 $t('tasks.status.')(见 src/i18n/{zh-CN,en}/tasks.ts) +export const TASK_STATUS_LABELS: Record = { + todo: 'tasks.status.todo', + in_progress: 'tasks.status.in_progress', + review_ready: 'tasks.status.review_ready', + merged: 'tasks.status.merged', + abandoned: 'tasks.status.abandoned', +} + +export const TASK_STATUS_CLASS: Record = { + todo: 'status-todo', + in_progress: 'status-progress', + review_ready: 'status-review', + merged: 'status-done', + abandoned: 'status-abandoned', +} + +export function taskStatusLabel(status: string): string { + return TASK_STATUS_LABELS[status] ?? status +} + +export function taskStatusClass(status: string): string { + return TASK_STATUS_CLASS[status] ?? 'status-todo' +} + +// ── 任务优先级 ── + +export const PRIORITY_LABELS: Record = { 0: 'P0', 1: 'P1', 2: 'P2', 3: 'P3' } + +export const PRIORITY_CLASSES: Record = { + 0: 'priority-critical', + 1: 'priority-high', + 2: 'priority-medium', + 3: 'priority-low', +} + +export function priorityLabel(p: number): string { + return PRIORITY_LABELS[p] ?? `P${p}` +} + +export function priorityClass(p: number): string { + return PRIORITY_CLASSES[p] ?? 'priority-low' +} diff --git a/src/i18n/en/ai.ts b/src/i18n/en/ai.ts new file mode 100644 index 0000000..034b64f --- /dev/null +++ b/src/i18n/en/ai.ts @@ -0,0 +1,10 @@ +export default { + ai: { + assistant: 'AI Assistant', + toolName: { + readFile: 'Read File', + listDirectory: 'List Directory', + writeFile: 'Write File', + }, + }, +} diff --git a/src/i18n/en/aiChat.ts b/src/i18n/en/aiChat.ts new file mode 100644 index 0000000..797680d --- /dev/null +++ b/src/i18n/en/aiChat.ts @@ -0,0 +1,64 @@ +export default { + aiChat: { + // ── Conversation sidebar ── + sidebarTitle: 'Chats', + newConversation: 'New chat', + doubleClickToRename: 'Double-click to rename', + archive: 'Archive', + unarchive: 'Unarchive', + archived: 'Archived', + archivedCount: 'Archived ({n})', + emptyConversation: 'No conversations', + + // ── Time grouping (sidebar) ── + today: 'Today', + yesterday: 'Yesterday', + earlier: 'Earlier', + + // ── Header buttons ── + conversationList: 'Chat list', + maximize: 'Maximize', + restoreSidebar: 'Restore sidebar', + detachWindow: 'Detach window', + closePanel: 'Close panel', + dockRight: 'Dock to main window right', + undock: 'Undock', + pinOnTop: 'Pin on top', + unpin: 'Unpin', + closeWindow: 'Close window', + + // ── Provider status ── + clickToSwitchProvider: 'Click to switch provider', + providerNotConfigured: 'No AI provider configured. Please add one in settings.', + notConfigured: 'Not configured', + + // ── Empty states ── + emptyNoProvider: 'No AI provider configured', + emptyNoProviderHint: 'Please add a provider in settings to start chatting', + emptyTitle: 'Chat with AI assistant', + emptyHint: 'Try "create a project" or "list all tasks"', + + // ── Back to bottom ── + backToBottom: 'Back to bottom', + + // ── Pending queue ── + queueTitle: 'Queued {n}/10', + clearQueue: 'Clear', + + // ── Skill ── + clearSkill: 'Clear skill', + placeholderWithSkill: 'Skill selected. Type arguments and press Enter', + placeholderDefault: 'Type a message... (/ for skills)', + skillNoMatch: 'No matching skill', + skillNotLoaded: 'No local skills loaded', + + // ── Send ── + stopGenerating: 'Stop generating', + + // ── Input hint ── + inputHint: 'Enter to send · Shift+Enter for newline', + + // ── Confirm dialog ── + confirmDeleteConversation: 'Delete chat "{title}"? This cannot be undone.', + }, +} diff --git a/src/i18n/en/aiTool.ts b/src/i18n/en/aiTool.ts new file mode 100644 index 0000000..b323473 --- /dev/null +++ b/src/i18n/en/aiTool.ts @@ -0,0 +1,31 @@ +export default { + aiTool: { + running: 'Running…', + approve: 'Approve', + reject: 'Reject', + rejectedHint: 'User rejected this action', + collapse: 'Collapse', + expandAll: 'Expand all', + written: 'written', + items: 'items', + lines: 'lines', + + readPrefix: 'Read', + readFallback: 'Read File', + dirPrefix: 'Dir', + dirFallback: 'List Directory', + writePrefix: 'Write', + writeFallback: 'Write File', + + taskCount: '{n} tasks', + projectCount: '{n} projects', + ideaCount: '{n} ideas', + createdWithName: 'Created: {name}', + created: 'Created', + updatedField: 'Updated {field}', + updated: 'Updated', + deleted: 'Deleted', + deleteIneffective: 'Delete ineffective', + workflowHint: 'Run it on the workflow page', + }, +} diff --git a/src/i18n/en/common.ts b/src/i18n/en/common.ts new file mode 100644 index 0000000..1b4a0fd --- /dev/null +++ b/src/i18n/en/common.ts @@ -0,0 +1,17 @@ +export default { + common: { + justNow: 'just now', + minutesAgo: '{n}m ago', + hoursAgo: '{n}h ago', + dayAgo: '{n}d ago', + yesterday: 'Yesterday', + ago: '{time} ago', + cancel: 'Cancel', + confirm: 'OK', + delete: 'Delete', + save: 'Save', + edit: 'Edit', + close: 'Close', + loading: 'Loading…', + }, +} diff --git a/src/i18n/en.ts b/src/i18n/en/dashboard.ts similarity index 65% rename from src/i18n/en.ts rename to src/i18n/en/dashboard.ts index e41ddd8..e6948ca 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en/dashboard.ts @@ -1,17 +1,4 @@ export default { - nav: { - overview: 'Overview', - ideas: 'Ideas', - projects: 'Projects', - tasks: 'Tasks', - knowledge: 'Knowledge', - decisions: 'Decisions', - settings: 'Settings', - workspace: 'Workspace', - traceability: 'Traceability', - systemReady: 'System Ready', - aiPanel: 'AI Panel', - }, dashboard: { title: 'Overview', subtitle: 'All systems operational', @@ -59,20 +46,4 @@ export default { rejected: 'Rejected', }, }, - ai: { - assistant: 'AI Assistant', - toolName: { - readFile: 'Read File', - listDirectory: 'List Directory', - writeFile: 'Write File', - }, - }, - common: { - justNow: 'just now', - minutesAgo: '{n}m ago', - hoursAgo: '{n}h ago', - dayAgo: '{n}d ago', - yesterday: 'Yesterday', - ago: '{time} ago', - }, } diff --git a/src/i18n/en/ideas.ts b/src/i18n/en/ideas.ts new file mode 100644 index 0000000..6e67d3d --- /dev/null +++ b/src/i18n/en/ideas.ts @@ -0,0 +1,79 @@ +export default { + ideas: { + // Page header + title: '💡 Ideas', + refresh: '🔄 Refresh', + capture: '✨ Capture Idea', + + // Filter bar + searchPlaceholder: 'Search ideas...', + filter: { + all: 'All', + hot: 'Hot', + pending: 'Pending', + promoted: 'Promoted', + }, + + // Empty state + emptyState: 'Select an idea to view details', + + // Adversarial evaluation + adversarialTitle: '⚖️ Adversarial Evaluation', + evalModeHeuristic: 'Heuristic', + positive: '📈 Pro', + negative: '📉 Con', + confidence: '{n}% confidence', + analystTitle: '🧠 AI Analyst Conclusion', + finalScore: 'Overall score: {score}/10', + netSentiment: 'Overall sentiment: {tone} ({n})', + sentimentPositive: 'Positive', + sentimentNegative: 'Cautious', + actionTitle: '💡 Action Items', + evaluating: '⏳ Evaluating…', + startEval: '🔍 Start Adversarial Eval', + + // Assessment badges + assessment: { + 'immediate action': '🚀 Act immediately', + soon: '📅 Act soon', + 'with resources': '📦 Act with resources', + 'research more': '🔍 Needs more research', + monitor: '👁️ Keep monitoring', + cancel: '❌ Cancel idea', + }, + + // Multi-dimensional score + multiScoreTitle: '📊 Multi-dim Score', + noEval: 'No evaluation yet', + + // Tags + tagsTitle: '🏷️ Tags', + noTags: 'No tags yet', + + // Status management + statusTitle: '📋 Status', + status: { + draft: '📝 Draft', + pending_review: '⏳ Pending', + approved: '✅ Approved', + promoted: '🚀 Promoted', + rejected: '❌ Rejected', + }, + + // Action buttons + promoteToProject: '🚀 Promote to Project', + deleteIdea: '🗑️ Delete Idea', + + // Capture modal + captureTitle: '✨ Capture New Idea', + fieldTitle: 'Title', + fieldDesc: 'Description', + titlePlaceholder: 'Describe your idea in one sentence...', + descPlaceholder: 'More details (optional)...', + + // Native dialog text (confirm / alert) + confirmDelete: 'Delete idea "{title}"? This cannot be undone.', + promoteFailed: 'Promotion failed', + evalFailed: 'Evaluation failed', + }, +} diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts new file mode 100644 index 0000000..d9eea91 --- /dev/null +++ b/src/i18n/en/index.ts @@ -0,0 +1,14 @@ +// 自动聚合本目录所有模块文件 —— 新增模块文件即生效,无需改此文件。 +// 排除 index 自身。每个模块文件 export default 一个对象,顶层 key 合并进 messages。 +const modules = import.meta.glob(['./*.ts'], { eager: true, import: 'default' }) as Record< + string, + Record +> + +const messages: Record = {} +for (const [path, mod] of Object.entries(modules)) { + if (path.endsWith('index.ts')) continue + Object.assign(messages, mod) +} + +export default messages diff --git a/src/i18n/en/knowledge.ts b/src/i18n/en/knowledge.ts new file mode 100644 index 0000000..e77cf65 --- /dev/null +++ b/src/i18n/en/knowledge.ts @@ -0,0 +1,78 @@ +export default { + knowledge: { + title: '📚 Knowledge Base', + add: '+ New Knowledge', + tab: { + library: 'Library', + inbox: 'Inbox', + }, + searchPlaceholder: 'Search title, tags, content...', + categoryAll: 'All', + emptyInbox: '✅ No pending items', + emptyLibrary: '📭 No matching knowledge', + edit: '✏️ Edit', + publish: 'Publish', + reject: 'Reject', + archive: 'Archive', + reuseCount: '🔄 Reused {n} times', + confidenceBadge: 'Confidence: {label}', + contentLabel: 'Content', + tagsLabel: 'Tags', + tagsNone: 'None', + tagsPlaceholder: 'comma separated', + traceTitle: '🔍 Provenance', + traceMethod: 'Origin', + traceSource: 'Source conversation', + traceTime: 'Created at', + originExtracted: '🤖 Extracted by AI', + originManual: '✍️ Manually added', + originUnknown: '—', + reasoningLabel: '🤖 AI Reasoning', + refTitle: '🔄 References ({n})', + refEmpty: 'Not referenced yet', + loadMore: 'Load more ({n} left)', + lifecycleTitle: '📅 Lifecycle', + detailEmptyHint: 'Select a knowledge on the left to view its full lifecycle', + createTitle: 'New Knowledge', + kindLabel: 'Type', + titleLabel: 'Title', + titlePlaceholder: 'Short title', + contentPlaceholder: 'Reusable content', + tagsFieldLabel: 'Tags (comma separated)', + tagsFieldPlaceholder: 'Go, concurrency, context', + confidenceLabel: 'Confidence', + confidenceNone: 'Not set', + create: 'Create', + timeline: { + extracted: 'AI extracted', + created: 'Manually added', + published: 'Approved', + archived: 'Archived', + enterReview: 'Entered review', + referenced: 'Referenced', + statusChanged: 'Status → {to}', + summarySource: 'Source: {title}', + summaryConv: 'Conversation: {title}', + }, + status: { + candidate: 'Pending', + pending_review: 'Reviewing', + published: 'Published', + archived: 'Archived', + }, + confidence: { + high: 'High', + medium: 'Medium', + low: 'Low', + }, + kind: { + review_rule: 'Review Rule', + prompt_template: 'Prompt Template', + pitfall: 'Pitfall', + architecture_pattern: 'Architecture Pattern', + diagnosis: 'Diagnosis', + deployment_note: 'Deployment Note', + workflow_optimization: 'Workflow Optimization', + }, + }, +} diff --git a/src/i18n/en/nav.ts b/src/i18n/en/nav.ts new file mode 100644 index 0000000..85de63a --- /dev/null +++ b/src/i18n/en/nav.ts @@ -0,0 +1,15 @@ +export default { + nav: { + overview: 'Overview', + ideas: 'Ideas', + projects: 'Projects', + tasks: 'Tasks', + knowledge: 'Knowledge', + decisions: 'Decisions', + settings: 'Settings', + workspace: 'Workspace', + traceability: 'Traceability', + systemReady: 'System Ready', + aiPanel: 'AI Panel', + }, +} diff --git a/src/i18n/en/projectDetail.ts b/src/i18n/en/projectDetail.ts new file mode 100644 index 0000000..5b559b9 --- /dev/null +++ b/src/i18n/en/projectDetail.ts @@ -0,0 +1,54 @@ +export default { + projectDetail: { + // Header + backToList: '← Projects', + sync: '🔄 Sync', + delete: '🗑 Delete', + newTask: '+ New Task', + // Stage pipeline + stageIdea: '💡 Idea', + stageRequirement: '📋 Requirement', + stageCoding: '💻 Coding', + stageTesting: '🧪 Testing', + stageRelease: '🚀 Release', + // New task modal + newTaskTitle: 'New Task', + taskTitleLabel: 'Task Title', + taskTitlePlaceholder: 'Enter task title', + descLabel: 'Description', + descPlaceholder: 'Brief description', + branchLabel: 'Branch Name', + confirmCreate: 'Create', + // Project info panel + infoTitle: '📋 Project Info', + sourceIdea: 'Source Idea', + viewIdea: 'View idea details', + ideaDeleted: 'Original idea deleted', + createdAt: 'Created', + updatedAt: 'Updated', + description: 'Description', + projectStatus: 'Status', + // Task list panel + taskListTitle: '🔀 Tasks', + taskCount: '{n} tasks', + emptyTasks: 'No tasks yet', + // Workflow log panel + workflowLogTitle: '📋 Workflow Log', + runDemoWorkflow: '▶ Run Demo Workflow', + emptyWorkflowLog: 'Click "Run Demo Workflow" to view live logs', + // Approval dialog + approvalTitle: 'Manual Approval Required', + approvalHint: 'Choose an action:', + approvalLater: 'Later', + // Delete confirm + confirmDelete: 'Delete project "{name}"? It will be moved to trash and can be restored.', + // Project directory + codeDirLabel: 'Code Directory', + noDirBound: 'Not bound', + relocateDir: 'Relocate', + bindDir: 'Bind Directory', + dirStatusLabel: 'Directory Status', + // Tech stack + techStackLabel: 'Tech Stack', + }, +} diff --git a/src/i18n/en/projects.ts b/src/i18n/en/projects.ts new file mode 100644 index 0000000..885a7b4 --- /dev/null +++ b/src/i18n/en/projects.ts @@ -0,0 +1,42 @@ +export default { + projects: { + title: '📂 Projects', + // Header actions + create: '+ New Project', + trash: '🗑 Trash', + // New project modal + createTitle: 'New Project', + nameLabel: 'Project Name', + namePlaceholder: 'Enter project name', + descLabel: 'Project Description', + descPlaceholder: 'Briefly describe the project goal', + confirmCreate: 'Create', + // Trash modal + trashTitle: '🗑 Trash', + trashEmpty: 'Trash is empty', + movedIn: 'moved in', + restore: '↩ Restore', + purge: 'Delete Permanently', + // Project card + deleteTitle: 'Delete', + // Empty state + empty: 'No projects yet, click the top-right button to create your first project', + // Confirm prompts + confirmDelete: 'Delete project "{name}"? It will be moved to trash and can be restored.', + confirmPurge: 'Permanently delete project "{name}"? Tasks/branches/releases will be physically deleted and cannot be recovered!', + // New fields + codeDirLabel: 'Code Directory', + codeDirPlaceholder: 'Click the button on the right to select project directory', + selectDir: 'Select Directory', + clearDir: 'Clear', + // Status labels (PROJECT_STATUS_LABELS values in constants/project.ts use these keys) + status: { + planning: '📐 Planning', + in_progress: '💻 In Progress', + paused: '⏸ Paused', + completed: '✅ Completed', + cancelled: '❌ Cancelled', + active: '🚀 In Progress', + }, + }, +} diff --git a/src/i18n/en/settings.ts b/src/i18n/en/settings.ts new file mode 100644 index 0000000..0b6dae1 --- /dev/null +++ b/src/i18n/en/settings.ts @@ -0,0 +1,118 @@ +export default { + settings: { + // ===== Page title ===== + title: '⚙️ Settings', + + // ===== AI Provider panel ===== + panelAiProvider: '🤖 AI Model Config', + addProvider: '+ Add Provider', + editProviderTitle: 'Edit Provider', + addProviderTitle: 'Add Provider', + emptyProvider: 'No AI provider. Click above to add.', + badgeDefault: 'Default', + actionSetDefault: 'Set as default', + detailBaseUrl: 'Base URL', + detailModel: 'Model', + detailApiKey: 'API Key', + + // Provider form + labelName: 'Name', + labelProviderType: 'Protocol', + labelBaseUrl: 'Base URL', + labelApiKey: 'API Key', + labelDefaultModel: 'Default', + phProviderName: 'e.g. DeepSeek, GLM-4', + phBaseUrl: 'https://api.deepseek.com', + phApiKey: 'sk-...', + phDefaultModel: 'deepseek-chat', + providerTypeOpenaiCompat: 'OpenAI-compatible (DeepSeek / GLM v4 / OpenAI)', + providerTypeAnthropic: 'Anthropic-compatible (GLM subscription / Claude)', + saving: 'Saving...', + + // ===== Connection panel ===== + panelConnection: '🔗 Connections', + addConnection: '+ Add connection', + editConnectionTitle: 'Edit connection', + addConnectionTitle: 'Add connection', + emptyConnection: 'No connection yet. Click the button above to add one.', + labelType: 'Type', + labelHost: 'Host', + labelPort: 'Port', + labelUser: 'Username', + phConnName: 'flux_dev', + phHost: '127.0.0.1', + phPort: '3306', + phUser: 'root', + + // ===== General settings panel ===== + panelGeneral: '🎨 General', + labelTheme: 'Theme', + descTheme: 'Dark / light', + themeDark: '🌙 Dark', + themeLight: '☀️ Light', + themeSystem: '💻 System', + + labelLanguage: 'Language', + descLanguage: 'Interface language', + + labelAiLanguage: 'AI language', + descAiLanguage: 'Default AI reply language', + aiLanguageAuto: '🔄 Follow interface language', + aiLanguageZh: '🇨🇳 简体中文', + aiLanguageEn: '🇺🇸 English', + + labelAutoExecute: 'Auto-execute', + descAutoExecute: 'Let AI auto-execute low-risk actions', + + labelLogLevel: 'Log level', + descLogLevel: 'Verbosity of workflow logs', + + labelShowTokenUsage: 'Show token usage', + descShowTokenUsage: 'Show token consumption of each reply in conversations', + + labelGlobalConcurrency: 'Global max concurrency', + descGlobalConcurrency: 'Shared cap on simultaneous LLM requests across all conversations', + + labelPerConvConcurrency: 'Per-conversation concurrency', + descPerConvConcurrency: 'Concurrent LLM calls within one conversation (main loop / title / distill shared)', + + // ===== Knowledge base panel ===== + panelKnowledge: '📚 Knowledge base', + labelAutoExtract: 'Auto-distill knowledge', + descAutoExtract: 'Automatically distill reusable knowledge from conversations after they finish (AI proposes candidates, manual review required)', + labelTriggerMode: 'Distill trigger', + descTriggerMode: 'When auto-distillation is triggered', + triggerOnComplete: 'On conversation complete', + triggerOnIdle: 'On conversation idle', + triggerManualOnly: 'Manual only', + labelMinMessages: 'Min message count', + descMinMessages: 'Distillation triggers only after this many messages (filters out small talk noise)', + labelIdleTimeout: 'Idle timeout (s)', + descIdleTimeout: 'Seconds to wait in idle-trigger mode', + labelAutoInject: 'Auto-inject related knowledge', + descAutoInject: 'Retrieve related knowledge and inject it into the conversation context during chat', + labelVectorEnabled: 'Semantic retrieval (vector)', + descVectorEnabled: 'Use Embedding vectors for semantic matching — close meanings surface even with different wording; when off, pure keyword matching with zero external calls', + labelEmbeddingProvider: 'Embedding provider', + descEmbeddingProvider: 'AI provider used for vectorization (OpenAI-compatible only; switching requires re-embedding all knowledge)', + labelEmbeddingModel: 'Embedding model', + descEmbeddingModel: 'e.g. embedding-3 (Zhipu) / text-embedding-v4 (Alibaba) / text-embedding-3-small (OpenAI)', + embeddingProviderNone: 'Not selected', + phEmbeddingModel: 'embedding-3', + + // ===== Toast ===== + toastLoadProviderFail: 'Failed to load providers', + toastSaveIncomplete: 'Please fill in all fields (Name / Base URL / API Key / Model)', + toastSaved: 'Saved', + toastSaveFail: 'Save failed: {msg}', + toastDeleted: 'Deleted', + toastDeleteFail: 'Delete failed: {msg}', + toastSetDefaultOk: 'Set as default', + toastSetDefaultFail: 'Failed to set default: {msg}', + toastConnIncomplete: 'Please fill in Name and Host', + + // ===== Confirm dialogs ===== + confirmDeleteProvider: 'Delete provider "{name}"? This cannot be undone.', + confirmDeleteConn: 'Delete connection "{name}"?', + }, +} diff --git a/src/i18n/en/tasks.ts b/src/i18n/en/tasks.ts new file mode 100644 index 0000000..9a3fc17 --- /dev/null +++ b/src/i18n/en/tasks.ts @@ -0,0 +1,56 @@ +export default { + tasks: { + // Header + title: '🔀 Task Queue', + refresh: '🔄 Refresh', + create: '+ New Task', + + // Filter bar + filter: { + project: 'Project: ', + status: 'Status: ', + all: 'All', + }, + // Status filters (with icons) + statusFilter: { + all: 'All', + todo: 'Todo', + in_progress: 'In Progress', + review_ready: 'Review Ready', + merged: 'Merged', + abandoned: 'Abandoned', + }, + + // Groups + group: { + taskCount: '{n} tasks', + unknownProject: 'Unknown Project', + }, + + // New task modal + modal: { + title: 'New Task', + project: 'Project', + titleField: 'Title', + desc: 'Description', + branch: 'Branch', + priority: 'Priority', + titlePlaceholder: 'Enter task title...', + descPlaceholder: 'Brief description (optional)', + branchPlaceholder: 'feature/xxx (optional)', + // Priority options + priorityCritical: 'P0 Critical', + priorityHigh: 'P1 High', + priorityMedium: 'P2 Medium', + priorityLow: 'P3 Low', + }, + // Status labels (TASK_STATUS_LABELS values in constants/project.ts use these keys) + status: { + todo: '📋 Todo', + in_progress: '🔄 In Progress', + review_ready: '👀 Review Ready', + merged: '✅ Merged', + abandoned: '🗑️ Abandoned', + }, + }, +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index b8490b5..f13ecfa 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,16 +1,26 @@ import { createI18n } from 'vue-i18n' import zhCN from './zh-CN' import en from './en' +import { useAppSettingsStore } from '../stores/appSettings' + +// 界面语言:模块加载时 appSettings 缓存尚未填充(loadAll 在 App.vue onMounted 异步执行), +// 故这里 get() 拿到默认 'zh-CN';真实用户偏好由 App.vue 在 loadAll 完成后回填到 +// i18n.global.locale.value,初帧渲染短暂 zh-CN 不影响功能。 +const getInitialLocale = () => { + if (typeof window !== 'undefined') { + return useAppSettingsStore().get('df-language', 'zh-CN') + } + return 'zh-CN' +} -// 界面语言:跟随 localStorage 中用户选择,默认简体中文 const i18n = createI18n({ legacy: false, globalInjection: true, // 模板中可直接用 $t / $tc - locale: localStorage.getItem('df-language') || 'zh-CN', + locale: getInitialLocale(), fallbackLocale: 'en', messages: { - 'zh-CN': zhCN, - en, + 'zh-CN': zhCN as Record, + en: en as Record, }, }) diff --git a/src/i18n/zh-CN/ai.ts b/src/i18n/zh-CN/ai.ts new file mode 100644 index 0000000..96d1917 --- /dev/null +++ b/src/i18n/zh-CN/ai.ts @@ -0,0 +1,10 @@ +export default { + ai: { + assistant: 'AI 助手', + toolName: { + readFile: '读取文件', + listDirectory: '查看目录', + writeFile: '写入文件', + }, + }, +} diff --git a/src/i18n/zh-CN/aiChat.ts b/src/i18n/zh-CN/aiChat.ts new file mode 100644 index 0000000..bccb57f --- /dev/null +++ b/src/i18n/zh-CN/aiChat.ts @@ -0,0 +1,64 @@ +export default { + aiChat: { + // ── 对话侧栏 ── + sidebarTitle: '对话', + newConversation: '新对话', + doubleClickToRename: '双击重命名', + archive: '归档', + unarchive: '取消归档', + archived: '已归档', + archivedCount: '已归档 ({n})', + emptyConversation: '暂无对话', + + // ── 时间分组(侧栏) ── + today: '今天', + yesterday: '昨天', + earlier: '更早', + + // ── Header 按钮 ── + conversationList: '对话列表', + maximize: '最大化', + restoreSidebar: '还原侧栏', + detachWindow: '分离窗口', + closePanel: '关闭面板', + dockRight: '吸附到主窗口右侧', + undock: '取消吸附', + pinOnTop: '置顶', + unpin: '取消置顶', + closeWindow: '关闭窗口', + + // ── Provider 状态 ── + clickToSwitchProvider: '点击切换 Provider', + providerNotConfigured: '未配置 AI 提供商,请在设置中添加', + notConfigured: '未配置', + + // ── 空状态 ── + emptyNoProvider: '未配置 AI 提供商', + emptyNoProviderHint: '请在设置中添加 Provider 后开始对话', + emptyTitle: '与 AI 助手对话', + emptyHint: '可以说"帮我创建一个项目"或"列出所有任务"', + + // ── 回到底部 ── + backToBottom: '回到底部', + + // ── 待发送队列 ── + queueTitle: '待发送 {n}/10', + clearQueue: '清空', + + // ── 技能 ── + clearSkill: '取消技能', + placeholderWithSkill: '已选技能,输入参数后回车', + placeholderDefault: '输入消息...(/ 触发技能)', + skillNoMatch: '无匹配技能', + skillNotLoaded: '未读取到本机技能', + + // ── 发送 ── + stopGenerating: '停止生成', + + // ── 输入提示 ── + inputHint: 'Enter 发送 · Shift+Enter 换行', + + // ── 确认弹层 ── + confirmDeleteConversation: '确定删除对话「{title}」?该操作不可恢复。', + }, +} diff --git a/src/i18n/zh-CN/aiTool.ts b/src/i18n/zh-CN/aiTool.ts new file mode 100644 index 0000000..41a5c73 --- /dev/null +++ b/src/i18n/zh-CN/aiTool.ts @@ -0,0 +1,33 @@ +export default { + aiTool: { + running: '执行中…', + approve: '批准执行', + reject: '拒绝', + rejectedHint: '用户拒绝了此操作', + collapse: '收起', + expandAll: '展开全部', + written: '已写入', + items: '项', + lines: '行', + + // 工具显示名(含路径摘要前缀,fallback 兜底) + readPrefix: '读取', + readFallback: '读取文件', + dirPrefix: '目录', + dirFallback: '列出目录', + writePrefix: '写入', + writeFallback: '写入文件', + + // completed 结果摘要(header 折叠态可见) + taskCount: '{n} 项任务', + projectCount: '{n} 个项目', + ideaCount: '{n} 条想法', + createdWithName: '已创建:{name}', + created: '已创建', + updatedField: '已更新 {field}', + updated: '已更新', + deleted: '已删除', + deleteIneffective: '删除未生效', + workflowHint: '请到工作流页面运行', + }, +} diff --git a/src/i18n/zh-CN/common.ts b/src/i18n/zh-CN/common.ts new file mode 100644 index 0000000..3718206 --- /dev/null +++ b/src/i18n/zh-CN/common.ts @@ -0,0 +1,17 @@ +export default { + common: { + justNow: '刚刚', + minutesAgo: '{n} 分钟前', + hoursAgo: '{n} 小时前', + dayAgo: '{n} 天前', + yesterday: '昨天', + ago: '{time}前', + cancel: '取消', + confirm: '确定', + delete: '删除', + save: '保存', + edit: '编辑', + close: '关闭', + loading: '加载中…', + }, +} diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN/dashboard.ts similarity index 66% rename from src/i18n/zh-CN.ts rename to src/i18n/zh-CN/dashboard.ts index 1df7114..6e2eb3f 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN/dashboard.ts @@ -1,17 +1,4 @@ export default { - nav: { - overview: '总览', - ideas: '灵感', - projects: '项目', - tasks: '任务', - knowledge: '知识库', - decisions: '决策', - settings: '设置', - workspace: '工作区', - traceability: '追溯', - systemReady: '系统就绪', - aiPanel: 'AI 面板', - }, dashboard: { title: '总览', subtitle: '所有系统运行正常', @@ -59,20 +46,4 @@ export default { rejected: '已驳回', }, }, - ai: { - assistant: 'AI 助手', - toolName: { - readFile: '读取文件', - listDirectory: '查看目录', - writeFile: '写入文件', - }, - }, - common: { - justNow: '刚刚', - minutesAgo: '{n} 分钟前', - hoursAgo: '{n} 小时前', - dayAgo: '{n} 天前', - yesterday: '昨天', - ago: '{time}前', - }, } diff --git a/src/i18n/zh-CN/ideas.ts b/src/i18n/zh-CN/ideas.ts new file mode 100644 index 0000000..f61b36f --- /dev/null +++ b/src/i18n/zh-CN/ideas.ts @@ -0,0 +1,79 @@ +export default { + ideas: { + // 页头 + title: '💡 灵感', + refresh: '🔄 刷新', + capture: '✨ 捕捉灵感', + + // 筛选栏 + searchPlaceholder: '搜索灵感...', + filter: { + all: '全部', + hot: '热门', + pending: '待评估', + promoted: '已立项', + }, + + // 空状态 + emptyState: '选择一个想法查看详情', + + // 对抗式评估 + adversarialTitle: '⚖️ 对抗式评估', + evalModeHeuristic: '启发式', + positive: '📈 正方观点', + negative: '📉 反方观点', + confidence: '{n}% 置信度', + analystTitle: '🧠 AI 分析师结论', + finalScore: '综合评分: {score}/10', + netSentiment: '整体倾向: {tone} ({n})', + sentimentPositive: '积极', + sentimentNegative: '谨慎', + actionTitle: '💡 行动建议', + evaluating: '⏳ 评估中…', + startEval: '🔍 开始对抗评估', + + // 评估结论徽章 + assessment: { + 'immediate action': '🚀 立即行动', + soon: '📅 尽快行动', + 'with resources': '📦 配置资源后行动', + 'research more': '🔍 需要更多研究', + monitor: '👁️ 持续监控', + cancel: '❌ 取消想法', + }, + + // 多维评分 + multiScoreTitle: '📊 多维评分', + noEval: '暂无评估', + + // 标签 + tagsTitle: '🏷️ 标签', + noTags: '暂无标签', + + // 状态管理 + statusTitle: '📋 状态管理', + status: { + draft: '📝 草稿', + pending_review: '⏳ 待评估', + approved: '✅ 已批准', + promoted: '🚀 已立项', + rejected: '❌ 已拒绝', + }, + + // 操作按钮 + promoteToProject: '🚀 立项为项目', + deleteIdea: '🗑️ 删除想法', + + // 捕捉模态框 + captureTitle: '✨ 捕捉新想法', + fieldTitle: '标题', + fieldDesc: '描述', + titlePlaceholder: '一句话描述你的想法...', + descPlaceholder: '详细说明(可选)...', + + // 原生对话框文案(confirm / alert) + confirmDelete: '确定删除想法「{title}」?此操作不可撤销。', + promoteFailed: '立项失败', + evalFailed: '评估失败', + }, +} diff --git a/src/i18n/zh-CN/index.ts b/src/i18n/zh-CN/index.ts new file mode 100644 index 0000000..d9eea91 --- /dev/null +++ b/src/i18n/zh-CN/index.ts @@ -0,0 +1,14 @@ +// 自动聚合本目录所有模块文件 —— 新增模块文件即生效,无需改此文件。 +// 排除 index 自身。每个模块文件 export default 一个对象,顶层 key 合并进 messages。 +const modules = import.meta.glob(['./*.ts'], { eager: true, import: 'default' }) as Record< + string, + Record +> + +const messages: Record = {} +for (const [path, mod] of Object.entries(modules)) { + if (path.endsWith('index.ts')) continue + Object.assign(messages, mod) +} + +export default messages diff --git a/src/i18n/zh-CN/knowledge.ts b/src/i18n/zh-CN/knowledge.ts new file mode 100644 index 0000000..2e2d0c7 --- /dev/null +++ b/src/i18n/zh-CN/knowledge.ts @@ -0,0 +1,94 @@ +export default { + knowledge: { + title: '📚 知识库', + add: '+ 新增知识', + // 顶层 Tab + tab: { + library: '知识库', + inbox: '审核收件箱', + }, + // 搜索 + searchPlaceholder: '搜索标题、标签、内容...', + // 分类 + categoryAll: '全部', + // 列表空状态 + emptyInbox: '✅ 暂无待审核条目', + emptyLibrary: '📭 没有匹配的知识', + // 详情头部操作 + edit: '✏️ 编辑', + publish: '发布', + reject: '拒绝', + archive: '归档', + // 详情徽章 + reuseCount: '🔄 复用 {n} 次', + confidenceBadge: '置信度: {label}', + // 详情字段 + contentLabel: '内容', + tagsLabel: '标签', + tagsNone: '无', + tagsPlaceholder: '逗号分隔', + // 溯源 + traceTitle: '🔍 溯源', + traceMethod: '产生方式', + traceSource: '来源对话', + traceTime: '产生时间', + originExtracted: '🤖 AI 从对话中提炼', + originManual: '✍️ 手动录入', + originUnknown: '—', + reasoningLabel: '🤖 AI 判断依据', + // 引用 + refTitle: '🔄 引用记录({n} 次)', + refEmpty: '尚未被引用', + loadMore: '加载更多(剩 {n} 条)', + // 生命周期 + lifecycleTitle: '📅 生命周期', + // 详情空状态 + detailEmptyHint: '选择左侧知识查看完整生命线', + // 新增对话框 + createTitle: '新增知识', + kindLabel: '类型', + titleLabel: '标题', + titlePlaceholder: '简短标题', + contentPlaceholder: '完整可复用内容', + tagsFieldLabel: '标签(逗号分隔)', + tagsFieldPlaceholder: 'Go, 并发, context', + confidenceLabel: '置信度', + confidenceNone: '不设置', + create: '创建', + // 时间线节点 + timeline: { + extracted: 'AI 提炼产生', + created: '手动录入', + published: '审核通过', + archived: '归档', + enterReview: '进入审核', + referenced: '被引用', + statusChanged: '状态 → {to}', + summarySource: '来源: {title}', + summaryConv: '对话: {title}', + }, + // 状态标签 + status: { + candidate: '待审核', + pending_review: '审核中', + published: '已发布', + archived: '已归档', + }, + // 置信度标签 + confidence: { + high: '高', + medium: '中', + low: '低', + }, + // 知识类型(与 store KNOWLEDGE_KINDS key 对齐) + kind: { + review_rule: '审查规则', + prompt_template: 'Prompt模板', + pitfall: '踩坑经验', + architecture_pattern: '架构模式', + diagnosis: '诊断知识', + deployment_note: '部署经验', + workflow_optimization: '工作流优化', + }, + }, +} diff --git a/src/i18n/zh-CN/nav.ts b/src/i18n/zh-CN/nav.ts new file mode 100644 index 0000000..d03a081 --- /dev/null +++ b/src/i18n/zh-CN/nav.ts @@ -0,0 +1,15 @@ +export default { + nav: { + overview: '总览', + ideas: '灵感', + projects: '项目', + tasks: '任务', + knowledge: '知识库', + decisions: '决策', + settings: '设置', + workspace: '工作区', + traceability: '追溯', + systemReady: '系统就绪', + aiPanel: 'AI 面板', + }, +} diff --git a/src/i18n/zh-CN/projectDetail.ts b/src/i18n/zh-CN/projectDetail.ts new file mode 100644 index 0000000..2d1cfd1 --- /dev/null +++ b/src/i18n/zh-CN/projectDetail.ts @@ -0,0 +1,54 @@ +export default { + projectDetail: { + // 头部 + backToList: '← 项目列表', + sync: '🔄 同步', + delete: '🗑 删除', + newTask: '+ 新任务', + // 阶段 pipeline + stageIdea: '💡 想法', + stageRequirement: '📋 需求', + stageCoding: '💻 编码', + stageTesting: '🧪 测试', + stageRelease: '🚀 发布', + // 新建任务模态框 + newTaskTitle: '新建任务', + taskTitleLabel: '任务标题', + taskTitlePlaceholder: '输入任务标题', + descLabel: '描述', + descPlaceholder: '简要描述', + branchLabel: '分支名称', + confirmCreate: '确认创建', + // 项目信息面板 + infoTitle: '📋 项目信息', + sourceIdea: '来源想法', + viewIdea: '查看想法详情', + ideaDeleted: '原始想法已删除', + createdAt: '创建时间', + updatedAt: '更新时间', + description: '描述', + projectStatus: '项目状态', + // 任务列表面板 + taskListTitle: '🔀 任务列表', + taskCount: '{n} 个任务', + emptyTasks: '暂无任务', + // 工作流日志面板 + workflowLogTitle: '📋 工作流日志', + runDemoWorkflow: '▶ 运行测试工作流', + emptyWorkflowLog: '点击"运行测试工作流"查看实时日志', + // 审批对话框 + approvalTitle: '需要人工审批', + approvalHint: '请选择操作:', + approvalLater: '稍后处理', + // 删除确认 + confirmDelete: '确定删除项目「{name}」?将移入回收站,可恢复。', + // 项目目录 + codeDirLabel: '代码目录', + noDirBound: '未绑定', + relocateDir: '重定位', + bindDir: '绑定目录', + dirStatusLabel: '目录状态', + // 技术栈 + techStackLabel: '技术栈', + }, +} diff --git a/src/i18n/zh-CN/projects.ts b/src/i18n/zh-CN/projects.ts new file mode 100644 index 0000000..6d765bf --- /dev/null +++ b/src/i18n/zh-CN/projects.ts @@ -0,0 +1,41 @@ +export default { + projects: { + title: '📂 项目列表', + // 头部操作 + create: '+ 新建项目', + trash: '🗑 回收站', + // 新建项目模态框 + createTitle: '新建项目', + nameLabel: '项目名称', + namePlaceholder: '输入项目名称', + descLabel: '项目描述', + descPlaceholder: '简要描述项目目标', + codeDirLabel: '代码目录', + codeDirPlaceholder: '点击右侧选择项目所在目录', + selectDir: '选择目录', + clearDir: '清除', + confirmCreate: '确认创建', + // 回收站模态框 + trashTitle: '🗑 回收站', + trashEmpty: '回收站为空', + movedIn: '移入', + restore: '↩ 恢复', + purge: '彻底删除', + // 项目卡片 + deleteTitle: '删除', + // 空状态 + empty: '暂无项目,点击右上角创建第一个项目', + // 确认文案 + confirmDelete: '确定删除项目「{name}」?将移入回收站,可恢复。', + confirmPurge: '彻底删除项目「{name}」?连带任务/分支/发布一并物理删除,不可恢复!', + // 状态文案(constants/project.ts 的 PROJECT_STATUS_LABELS 值走此 key) + status: { + planning: '📐 规划中', + in_progress: '💻 进行中', + paused: '⏸ 已暂停', + completed: '✅ 已完成', + cancelled: '❌ 已取消', + active: '🚀 进行中', + }, + }, +} diff --git a/src/i18n/zh-CN/settings.ts b/src/i18n/zh-CN/settings.ts new file mode 100644 index 0000000..1fc31e8 --- /dev/null +++ b/src/i18n/zh-CN/settings.ts @@ -0,0 +1,118 @@ +export default { + settings: { + // ===== 页面标题 ===== + title: '⚙️ 设置', + + // ===== AI 模型配置面板 ===== + panelAiProvider: '🤖 AI 模型配置', + addProvider: '+ 添加 Provider', + editProviderTitle: '编辑 Provider', + addProviderTitle: '添加 Provider', + emptyProvider: '暂无 AI 提供商,点击上方按钮添加', + badgeDefault: '默认', + actionSetDefault: '设为默认', + detailBaseUrl: 'Base URL', + detailModel: '模型', + detailApiKey: 'API Key', + + // Provider 表单 + labelName: '名称', + labelProviderType: '协议类型', + labelBaseUrl: 'Base URL', + labelApiKey: 'API Key', + labelDefaultModel: '默认模型', + phProviderName: '如 DeepSeek、GLM-4', + phBaseUrl: 'https://api.deepseek.com', + phApiKey: 'sk-...', + phDefaultModel: 'deepseek-chat', + providerTypeOpenaiCompat: 'OpenAI 兼容(DeepSeek / GLM v4 / OpenAI)', + providerTypeAnthropic: 'Anthropic 兼容(GLM 订阅 / Claude)', + saving: '保存中...', + + // ===== 连接管理面板 ===== + panelConnection: '🔗 连接管理', + addConnection: '+ 添加连接', + editConnectionTitle: '编辑连接', + addConnectionTitle: '添加连接', + emptyConnection: '暂无连接,点击上方按钮添加', + labelType: '类型', + labelHost: 'Host', + labelPort: 'Port', + labelUser: '用户名', + phConnName: 'flux_dev', + phHost: '127.0.0.1', + phPort: '3306', + phUser: 'root', + + // ===== 通用设置面板 ===== + panelGeneral: '🎨 通用设置', + labelTheme: '主题', + descTheme: '深色 / 浅色模式', + themeDark: '🌙 深色模式', + themeLight: '☀️ 浅色模式', + themeSystem: '💻 跟随系统', + + labelLanguage: '语言', + descLanguage: '界面显示语言', + + labelAiLanguage: 'AI 对话语言', + descAiLanguage: 'AI 回复使用的默认语言', + aiLanguageAuto: '🔄 跟随界面语言', + aiLanguageZh: '🇨🇳 简体中文', + aiLanguageEn: '🇺🇸 English', + + labelAutoExecute: 'AI 自动执行', + descAutoExecute: 'AI 可自动执行低风险操作', + + labelLogLevel: '日志级别', + descLogLevel: '工作流日志详细程度', + + labelShowTokenUsage: '显示 Token 用量', + descShowTokenUsage: '在对话中展示每次回复的 token 消耗', + + labelGlobalConcurrency: '全局最大并发', + descGlobalConcurrency: '所有对话共享的 LLM 同时请求数上限', + + labelPerConvConcurrency: '单对话并发上限', + descPerConvConcurrency: '单个对话内同时进行的 LLM 调用数(主循环 / 标题 / 提炼共享)', + + // ===== 知识库面板 ===== + panelKnowledge: '📚 知识库', + labelAutoExtract: '自动提炼知识', + descAutoExtract: '对话完成后自动从内容中提炼可复用知识(AI 提炼产候选,需人工审核)', + labelTriggerMode: '提炼触发方式', + descTriggerMode: '自动提炼的触发时机', + triggerOnComplete: '对话完成后', + triggerOnIdle: '对话闲置后', + triggerManualOnly: '仅手动', + labelMinMessages: '最少消息数', + descMinMessages: '对话达到此消息数才触发提炼(防闲聊噪音)', + labelIdleTimeout: '闲置超时(秒)', + descIdleTimeout: '闲置触发模式下等待的秒数', + labelAutoInject: '自动注入相关知识', + descAutoInject: '聊天时自动检索相关知识注入对话上下文', + labelVectorEnabled: '语义检索(向量)', + descVectorEnabled: '用 Embedding 向量做语义匹配,意思相近用词不同也能搜到;关闭时纯关键词匹配,零外部调用', + labelEmbeddingProvider: 'Embedding Provider', + descEmbeddingProvider: '向量化用的 AI 提供商(仅 OpenAI 兼容类型;选定后更换需重新嵌入全部知识)', + labelEmbeddingModel: 'Embedding 模型', + descEmbeddingModel: '如 embedding-3(智谱) / text-embedding-v4(阿里) / text-embedding-3-small(OpenAI)', + embeddingProviderNone: '未选择', + phEmbeddingModel: 'embedding-3', + + // ===== Toast 提示 ===== + toastLoadProviderFail: '加载提供商失败', + toastSaveIncomplete: '请填写完整(名称 / Base URL / API Key / 模型)', + toastSaved: '已保存', + toastSaveFail: '保存失败:{msg}', + toastDeleted: '已删除', + toastDeleteFail: '删除失败:{msg}', + toastSetDefaultOk: '已设为默认', + toastSetDefaultFail: '设置默认失败:{msg}', + toastConnIncomplete: '请填写名称和 Host', + + // ===== 确认弹层 ===== + confirmDeleteProvider: '确定删除提供商「{name}」?该操作不可恢复。', + confirmDeleteConn: '确定删除连接「{name}」?', + }, +} diff --git a/src/i18n/zh-CN/tasks.ts b/src/i18n/zh-CN/tasks.ts new file mode 100644 index 0000000..ea18b00 --- /dev/null +++ b/src/i18n/zh-CN/tasks.ts @@ -0,0 +1,56 @@ +export default { + tasks: { + // 页头 + title: '🔀 任务队列', + refresh: '🔄 刷新', + create: '+ 新建任务', + + // 筛选栏 + filter: { + project: '项目:', + status: '状态:', + all: '全部', + }, + // 状态筛选(含图标) + statusFilter: { + all: '全部', + todo: '待开始', + in_progress: '进行中', + review_ready: '待审查', + merged: '已合并', + abandoned: '已放弃', + }, + + // 分组 + group: { + taskCount: '{n} 个任务', + unknownProject: '未知项目', + }, + + // 新建任务模态 + modal: { + title: '新建任务', + project: '项目', + titleField: '标题', + desc: '描述', + branch: '分支', + priority: '优先级', + titlePlaceholder: '输入任务标题...', + descPlaceholder: '简要描述(可选)', + branchPlaceholder: 'feature/xxx(可选)', + // 优先级选项 + priorityCritical: 'P0 紧急', + priorityHigh: 'P1 高', + priorityMedium: 'P2 中', + priorityLow: 'P3 低', + }, + // 状态文案(constants/project.ts 的 TASK_STATUS_LABELS 值走此 key) + status: { + todo: '📋 待开始', + in_progress: '🔄 进行中', + review_ready: '👀 待审查', + merged: '✅ 已合并', + abandoned: '🗑️ 已放弃', + }, + }, +} diff --git a/src/main.ts b/src/main.ts index b4ebbef..2a37f21 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,6 @@ import "./styles/global.css"; createApp(App).use(router).use(i18n).mount("#app"); -// 启动计时:从 index.html 解析到 Vue 挂载完成 +// 启动计时:从 index.html 解析到 Vue 挂载完成(debug 级,空白屏复现时开 verbose 看) const t0 = (window as any).__APP_T0 ?? 0; -console.log(`[启动] Vue mount 完成: ${(performance.now() - t0).toFixed(0)}ms`); +console.debug(`[启动] Vue mount 完成: ${(performance.now() - t0).toFixed(0)}ms`); diff --git a/src/stores/ai.ts b/src/stores/ai.ts index 90957a1..a1869c2 100644 --- a/src/stores/ai.ts +++ b/src/stores/ai.ts @@ -1,15 +1,35 @@ -//! AI 聊天 Store — 管理对话状态、流式渲染、工具审批、对话管理 +//! AI 聊天 Store — 模块级单例 state + useAiStore() 统一入口 +//! +//! 架构(路线A:state 单例 + 逻辑外移到 composables): +//! - 本文件只保留模块级单例 reactive state(19 字段)+ useAiStore() 统一入口 +//! - 所有方法搬到 src/composables/ai/*,通过 ...useAiXxx(state) 展开到 useAiStore() 返回值 +//! - 组件用法不变:`const { state, sendMessage, ... } = useAiStore()` 零改动 +//! - composables 之间通过模块级 import 直接互调(events↔send↔stream 耦合) +//! +//! composables 职责: +//! - useAiEvents startListener/stopListener/handleEvent/flushCurrentText/findToolCall/friendlyError/notifyConversationChanged +//! - useAiStream onStreamTimeout/resetStreamWatchdog/clearStreamWatchdog + STREAM_TIMEOUT_MS +//! - useAiSend sendMessage/approveToolCall/drainQueue/cancelQueued/clearQueue/stopChat +//! - useAiConversations loadConversations/newConversation/switchConversation/deleteConversation/renameConversation/archiveConversation/toggleArchivedFold/toggleSidebar +//! - useAiWindow detachPanel/reattachPanel/resumeInDetached/closeDetachedWindow/dockDetached/syncToMain/startFollowMain/stopFollowMain +//! - useAiPanel togglePanel/toggleMaximize/loadProviders/loadSkills/setProvider/clearChat + restoreUiState/persistUiState +//! +//! 注意: +//! - 各 composable 模块加载时会执行其顶层副作用(useAiPanel.restoreUiState + watch appSettings), +//! 故 useAiStore() 之外无需手动 init;以下 import 即触发恢复逻辑 +//! - state 为模块级单例,全应用共享同一引用 import { reactive } from 'vue' -import { listen, emit } from '@tauri-apps/api/event' -import { aiApi } from '../api' -import type { AiChatEvent, AiConversationSummary, AiMessage, AiProviderConfig, AiToolCallInfo } from '../api/types' +import type { AiChatEvent, AiConversationSummary, AiMessage, AiProviderConfig, AiToolCallInfo, SkillInfo } from '../api/types' +import { useAiEvents } from '../composables/ai/useAiEvents' +import { useAiStream } from '../composables/ai/useAiStream' +import { useAiSend } from '../composables/ai/useAiSend' +import { useAiConversations } from '../composables/ai/useAiConversations' +import { useAiWindow } from '../composables/ai/useAiWindow' +import { useAiPanel } from '../composables/ai/useAiPanel' -let _unlistenAiEvent: (() => void) | null = null -let _unlistenConvChanged: (() => void) | null = null -let _msgCounter = 0 - -const state = reactive({ +/// 模块级单例 state — 全应用共享(composables 通过 `import { state } from '../../stores/ai'` 取用) +export const state = reactive({ messages: [] as AiMessage[], streaming: false, currentText: '', @@ -29,481 +49,35 @@ const state = reactive({ detached: false, // 吸附跟随中 docked: false, + // 本机技能(`/` 联想) + skills: [] as SkillInfo[], + // 待发送队列(生成中发的消息排队,AiCompleted 后自动续发) + queue: [] as { text: string; skill?: string }[], + // 归档分组折叠态(默认折叠) + archivedCollapsed: true, + // token 用量展示(受 df-show-token-usage 开关控制;lastTokenUsage=最近一条回复,convTokenTotal=当前对话累计) + lastTokenUsage: null as { prompt: number; completion: number; total: number } | null, + convTokenTotal: null as { prompt: number; completion: number; total: number } | null, }) +// 旁注:此处不再保留 type-only 导出(AiChatEvent 等),因组件直接从 api/types import。 +// 若有外部模块仍从本文件 import 这些类型,下方 re-export 兜底: +export type { AiChatEvent, AiConversationSummary, AiMessage, AiProviderConfig, AiToolCallInfo, SkillInfo } + +/** + * AI Store 统一入口 — 返回单例 state + 全部方法。 + * + * 组件零改动:解构 shape 与拆分前完全一致。每次调用都重新展开 composable 方法 + * (composable 内部全部是模块级单例实现,展开的只是引用,无重复实例化开销)。 + */ export function useAiStore() { - async function startListener() { - if (_unlistenAiEvent) return - _unlistenAiEvent = await aiApi.onEvent(handleEvent) - _unlistenConvChanged = await listen('ai-conversation-changed', () => { - loadConversations() - }) - } - - function stopListener() { - _unlistenAiEvent?.() - _unlistenConvChanged?.() - _unlistenAiEvent = null - _unlistenConvChanged = null - } - - function notifyConversationChanged() { - emit('ai-conversation-changed', {}) - } - - /** 后端原始错误转用户友好提示 */ - function friendlyError(raw: string): string { - if (/404|not\s*found/i.test(raw)) return '调用失败:接口地址或模型不存在,请检查 Provider 配置' - if (/401|403|unauthorized|api[_\s-]?key/i.test(raw)) return '调用失败:API Key 无效或无权限' - if (/timeout|超时/i.test(raw)) return '响应超时,请重试' - if (/network|connection|ECONN|网络|连接/i.test(raw)) return '网络连接失败,请检查网络' - return raw - } - - function handleEvent(event: AiChatEvent) { - const convId = event.conversation_id - // 首次收到事件时同步当前对话 id(后端自动建对话的场景) - if (convId && !state.activeConversationId) state.activeConversationId = convId - // 事件不属于当前展示对话(生成中切走了)→ 不污染当前视图,仅完成/错误时刷新侧边栏 - const isCurrent = !convId || convId === state.activeConversationId - if (!isCurrent) { - if (event.type === 'AiCompleted' || event.type === 'AiError') { - state.generatingConvId = null - loadConversations() - } - return - } - // 标记正在生成的对话(完成/错误事件除外) - if (convId && event.type !== 'AiCompleted' && event.type !== 'AiError') { - state.generatingConvId = convId - } - switch (event.type) { - case 'AiTextDelta': - state.currentText += event.delta - break - - case 'AiAgentRound': { - // Agent 循环新一轮:保存当前文本到上一条 assistant 消息,新建空 assistant 消息 - if (state.currentText) { - const lastMsg = state.messages[state.messages.length - 1] - if (lastMsg && lastMsg.role === 'assistant') { - lastMsg.content = state.currentText - } - } - state.currentText = '' - state.messages.push({ - id: `ai-${++_msgCounter}`, - role: 'assistant', - content: '', - timestamp: Date.now(), - }) - break - } - - case 'AiToolCallStarted': { - const info: AiToolCallInfo = { - id: event.id, - name: event.name, - args: event.args, - status: 'running', - } - const lastMsg = state.messages[state.messages.length - 1] - if (lastMsg && lastMsg.role === 'assistant') { - lastMsg.toolCalls = lastMsg.toolCalls || [] - lastMsg.toolCalls.push(info) - } - break - } - - case 'AiToolCallCompleted': { - const tc = findToolCall(event.id) - if (tc) { - tc.status = 'completed' - tc.result = event.result - } - state.pendingApprovals = state.pendingApprovals.filter(p => p.id !== event.id) - break - } - - case 'AiApprovalRequired': { - const info: AiToolCallInfo = { - id: event.id, - name: event.name, - args: event.args, - status: 'pending_approval', - } - state.pendingApprovals.push(info) - const tc = findToolCall(event.id) - if (tc) tc.status = 'pending_approval' - break - } - - case 'AiApprovalResult': { - if (!event.approved) { - const tc = findToolCall(event.id) - if (tc) tc.status = 'rejected' - state.pendingApprovals = state.pendingApprovals.filter(p => p.id !== event.id) - } - break - } - - case 'AiCompleted': { - if (state.currentText) { - const lastMsg = state.messages[state.messages.length - 1] - if (lastMsg && lastMsg.role === 'assistant') { - lastMsg.content = state.currentText - } - } - state.currentText = '' - state.streaming = false - state.generatingConvId = null - // 清理分离窗口生成态快照 - localStorage.removeItem('df-ai-gen') - localStorage.removeItem('df-ai-text') - loadConversations() - notifyConversationChanged() - break - } - - case 'AiError': { - state.streaming = false - state.generatingConvId = null - state.currentText = '' - localStorage.removeItem('df-ai-gen') - localStorage.removeItem('df-ai-text') - state.messages.push({ - id: `err-${++_msgCounter}`, - role: 'assistant', - content: friendlyError(event.error), - isError: true, - timestamp: Date.now(), - }) - break - } - } - } - - function findToolCall(id: string): AiToolCallInfo | undefined { - for (const msg of state.messages) { - if (msg.toolCalls) { - const tc = msg.toolCalls.find(t => t.id === id) - if (tc) return tc - } - } - return undefined - } - - async function sendMessage(text: string) { - if (!text.trim() || state.streaming) return - - state.messages.push({ - id: `user-${++_msgCounter}`, - role: 'user', - content: text.trim(), - timestamp: Date.now(), - }) - - state.messages.push({ - id: `ai-${++_msgCounter}`, - role: 'assistant', - content: '', - timestamp: Date.now(), - }) - - state.streaming = true - state.currentText = '' - - await startListener() - const raw = localStorage.getItem('df-ai-language') || 'auto' - const lang = raw === 'auto' - ? (localStorage.getItem('df-language') || 'zh-CN') - : raw - await aiApi.sendMessage(text.trim(), lang) - } - - async function approveToolCall(toolCallId: string, approved: boolean) { - await aiApi.approve(toolCallId, approved) - } - - async function loadProviders() { - state.providers = await aiApi.listProviders() - } - - async function setProvider(providerId: string) { - await aiApi.setProvider(providerId) - state.activeProvider = providerId - } - - async function clearChat() { - await aiApi.clearChat() - state.messages = [] - state.currentText = '' - state.pendingApprovals = [] - state.streaming = false - } - - /** 停止当前生成:仅发停止信号,streaming 状态由后端 AiCompleted 事件收尾 */ - async function stopChat() { - await aiApi.stopChat() - } - - function togglePanel() { - state.panelOpen = !state.panelOpen - } - - function toggleMaximize() { - state.maximized = !state.maximized - } - - // ══════════════════════════════════════ - // 对话管理 - // ══════════════════════════════════════ - - async function loadConversations() { - try { - state.conversations = await aiApi.listConversations() - } catch { - // 静默失败,不影响主流程 - } - } - - async function newConversation() { - const result = await aiApi.createConversation() - state.activeConversationId = result.id - state.messages = [] - state.currentText = '' - state.pendingApprovals = [] - state.streaming = false - await loadConversations() - notifyConversationChanged() - } - - async function switchConversation(id: string) { - // 允许生成中切换:后台继续生成,事件按 conversation_id 路由不污染当前视图 - const detail = await aiApi.switchConversation(id) - state.activeConversationId = id - - try { - const rawMsgs = typeof detail.messages === 'string' - ? JSON.parse(detail.messages) - : detail.messages - - // 构建 tool_call_id → tool_result 映射,用于回填工具执行结果 - const toolResultMap = new Map() - for (const m of rawMsgs) { - if (m.role === 'tool' && m.tool_call_id) { - toolResultMap.set(m.tool_call_id, m.content || '') - } - } - - state.messages = rawMsgs - .filter((m: any) => m.role !== 'tool') - .map((m: any, i: number) => ({ - id: `loaded-${i}`, - role: m.role, - content: m.content || '', - timestamp: Date.now(), - toolCalls: m.tool_calls?.map((tc: any) => ({ - id: tc.id, - name: tc.function?.name || '', - args: typeof tc.function?.arguments === 'string' - ? JSON.parse(tc.function.arguments || '{}') - : tc.function?.arguments || {}, - status: 'completed' as const, - result: toolResultMap.get(tc.id), - })), - })) - } catch { - state.messages = [] - } - - state.currentText = '' - state.pendingApprovals = [] - } - - async function deleteConversation(id: string) { - await aiApi.deleteConversation(id) - if (state.activeConversationId === id) { - state.activeConversationId = null - state.messages = [] - } - await loadConversations() - notifyConversationChanged() - } - - async function renameConversation(id: string, title: string) { - await aiApi.renameConversation(id, title) - // 本地同步更新侧栏摘要标题 - const conv = state.conversations.find(c => c.id === id) - if (conv) conv.title = title - notifyConversationChanged() - } - - function toggleSidebar() { - state.sidebarOpen = !state.sidebarOpen - } - - // ══════════════════════════════════════ - // 窗口模式 - // ══════════════════════════════════════ - - async function detachPanel() { - const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow') - const existing = await WebviewWindow.getByLabel('ai-detached') - if (existing) { - await existing.setFocus() - return - } - // 快照当前生成态,供分离窗口接管(保持正在进行的对话) - if (state.streaming && state.generatingConvId) { - localStorage.setItem('df-ai-gen', state.generatingConvId) - localStorage.setItem('df-ai-text', state.currentText) - } - const convId = state.activeConversationId - const sep = convId ? `?conv=${encodeURIComponent(convId)}` : '' - const url = window.location.origin + window.location.pathname + '#/ai-detached' + sep - const win = new WebviewWindow('ai-detached', { - url, - title: 'DevFlow AI', - width: 520, - height: 700, - minWidth: 360, - minHeight: 400, - center: true, - decorations: true, - }) - win.once('tauri://created', () => { - state.panelOpen = false - }) - win.once('tauri://error', (e) => { - console.error('[AI] 窗口创建失败:', e) - state.detached = false - }) - win.once('tauri://destroyed', () => { - state.detached = false - state.docked = false - stopFollowMain() - }) - state.detached = true - } - - async function reattachPanel() { - stopFollowMain() - const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow') - const win = await WebviewWindow.getByLabel('ai-detached') - if (win) { - await win.close() - } - state.detached = false - state.docked = false - state.panelOpen = true - localStorage.removeItem('df-ai-gen') - localStorage.removeItem('df-ai-text') - } - - /** 分离窗口挂载时接管主窗口当前对话(含生成中态) */ - async function resumeInDetached(convId: string | null) { - const id = convId || state.activeConversationId - if (id) await switchConversation(id) - const gen = localStorage.getItem('df-ai-gen') - if (gen) { - // 接管正在生成的对话:恢复已生成文本并补占位,后续 delta 继续追加 - state.currentText = localStorage.getItem('df-ai-text') || '' - state.messages.push({ - id: `ai-${++_msgCounter}`, - role: 'assistant', - content: '', - timestamp: Date.now(), - }) - state.streaming = true - state.generatingConvId = gen - } - } - - async function closeDetachedWindow() { - state.docked = false - stopFollowMain() - localStorage.removeItem('df-ai-gen') - localStorage.removeItem('df-ai-text') - const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow') - await getCurrentWebviewWindow().close() - } - - /** 吸附/取消吸附:AI 窗口跟随主窗口右侧 */ - async function dockDetached() { - // 取消吸附 - if (state.docked) { - state.docked = false - stopFollowMain() - return - } - - try { - await syncToMain() - state.docked = true - startFollowMain() - } catch (e) { - console.error('[AI] 吸附失败:', e) - } - } - - /** 同步 AI 窗口位置到主窗口右侧(保留 4px 间隙) */ - async function syncToMain() { - const { WebviewWindow, getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow') - const { PhysicalPosition, PhysicalSize } = await import('@tauri-apps/api/dpi') - const mainWin = await WebviewWindow.getByLabel('main') - const aiWin = getCurrentWebviewWindow() - if (!mainWin) return - const pos = await mainWin.outerPosition() - const size = await mainWin.innerSize() - await aiWin.setPosition(new PhysicalPosition(pos.x + size.width + 4, pos.y)) - await aiWin.setSize(new PhysicalSize(600, size.height)) - } - - // ── 主窗口事件跟随 ── - let _unlistenMove: (() => void) | null = null - let _unlistenResize: (() => void) | null = null - - async function startFollowMain() { - stopFollowMain() - const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow') - const mainWin = await WebviewWindow.getByLabel('main') - if (!mainWin) return - _unlistenMove = await mainWin.onMoved(() => { syncToMain() }) - _unlistenResize = await mainWin.onResized(() => { syncToMain() }) - } - - function stopFollowMain() { - _unlistenMove?.() - _unlistenResize?.() - _unlistenMove = null - _unlistenResize = null - } - return { state, - sendMessage, - approveToolCall, - loadProviders, - setProvider, - clearChat, - stopChat, - togglePanel, - toggleMaximize, - startListener, - stopListener, - // 对话管理 - loadConversations, - newConversation, - switchConversation, - deleteConversation, - renameConversation, - toggleSidebar, - // 窗口模式 - detachPanel, - reattachPanel, - closeDetachedWindow, - dockDetached, - resumeInDetached, + ...useAiEvents(), + ...useAiStream(), + ...useAiSend(), + ...useAiConversations(), + ...useAiWindow(), + ...useAiPanel(), } } diff --git a/src/stores/appSettings.ts b/src/stores/appSettings.ts new file mode 100644 index 0000000..d80aae1 --- /dev/null +++ b/src/stores/appSettings.ts @@ -0,0 +1,138 @@ +//! 应用设置 KV Store — localStorage → SQLite 迁移的统一持久化层 +//! +//! 后端 `app_settings` 表的 value 永远是 JSON 字符串。本层职责: +//! - 缓存已解析(反序列化)值,按 key 索引,组件同步读 +//! - 写操作立即更新缓存(乐观),并按 key debounce ~300ms 合并连续快速写 +//! - 组合式 `useSetting` 让组件像用 ref 一样用 v-model 绑定某个 key +//! +//! 注意:此为「KV 偏好持久化」层,与已有的 `stores/settings.ts`(mock 的 AI 提供商/ +//! 连接/通用配置列表)是两件事 —— 那个文件承载静态展示数据,本文件承载运行时偏好, +//! 故单独成文,互不干扰。 + +import { reactive, ref, watch, type Ref } from 'vue' +import { settingsApi } from '../api/settings' + +/// 按 key 合并连续写的 debounce 时长(毫秒) +const SET_DEBOUNCE_MS = 300 + +/// 已解析值的响应式缓存 —— 用 reactive 对象而非 Map,使 `useSetting` 的 watch 能 +/// 在 `loadAll` / `set` / `remove` 写入时被触发,从而同步刷新组件视图 +const cache = reactive>({}) + +/// 按 key 的 pending 写定时器:连续 set 在窗口内只发最后一次到 SQLite +const pendingTimers = new Map>() + +/// 载荷队列:debounce 触发时取最新值(而非定时器创建时的快照) +const pendingValues = new Map() + +/** 启动时一次性拉回全部 key/value 并解析填充缓存(App.vue 调用) */ +async function loadAll(): Promise { + const all = await settingsApi.getAll() + for (const [k, raw] of Object.entries(all)) { + cache[k] = parse(raw) + } +} + +/** 同步取已解析值,未命中或缓存未加载时返回 defaultValue */ +function get(key: string, defaultValue: T): T { + const v = cache[key] + return v === undefined ? defaultValue : (v as T) +} + +/** + * 写入某 key:立即更新缓存(乐观,视图无延迟),再 debounce 后落库。 + * 同 key 连续快速写(如拖拽/输入)在窗口内合并,只把最后一次值 JSON.stringify 发后端。 + */ +async function set(key: string, value: unknown): Promise { + cache[key] = value + pendingValues.set(key, value) + + const existing = pendingTimers.get(key) + if (existing) clearTimeout(existing) + + const timer = setTimeout(() => { + pendingTimers.delete(key) + const latest = pendingValues.get(key) + pendingValues.delete(key) + // 落库失败仅记日志:缓存已是最新值,降级为内存态,避免回滚造成 UI 与库不一致 + settingsApi + .set(key, JSON.stringify(latest)) + .catch((e) => console.error(`[appSettings] 写入 ${key} 失败:`, e)) + }, SET_DEBOUNCE_MS) + pendingTimers.set(key, timer) +} + +/** 删除某 key:清缓存 + 取消 pending 写 + 调后端删除 */ +async function remove(key: string): Promise { + delete cache[key] + pendingValues.delete(key) + const t = pendingTimers.get(key) + if (t) { + clearTimeout(t) + pendingTimers.delete(key) + } + await settingsApi.delete(key) +} + +/** + * 组合式:把某 key 绑定到一个 ref,组件可直接 v-model。 + * 读走缓存(同步、响应式),写触发 debounced set。 + * + * 实现说明:用普通 `ref` 持有当前值,`watch(cache[key])` 单向同步缓存 → ref, + * 这样 `loadAll` 异步填充缓存后能正确刷新组件视图(customRef 的 trigger 不会被 + * reactive cache 的异步 mutation 自动唤起,故改用 watch 显式同步)。 + * + * @example + * const theme = useSetting('df-theme', 'dark') + * // 模板里 —— 改动自动落库 + */ +function useSetting(key: string, defaultValue: T): Ref { + const r = ref(get(key, defaultValue)) as Ref + + // 缓存 → ref:loadAll/set/remove 改 cache[key] 时同步到 ref + watch( + () => cache[key], + (v) => { + // 写回值与当前 ref 不同时才更新,避免组件 set 后被 watch 回写造成抖动 + const next = v === undefined ? defaultValue : (v as T) + if (!Object.is(next, r.value)) r.value = next + }, + { deep: true }, + ) + + // ref → 缓存(及落库):组件改 .value 时触发 debounced set + watch( + r, + (v) => { + // 仅当与缓存当前值不同时才写,避免 watch 回写触发循环 + if (!Object.is(v, cache[key])) void set(key, v) + }, + { deep: true }, + ) + + return r +} + +/** + * 解析后端原始值:JSON.parse 失败时回退为原字符串(兼容历史非 JSON 数据)。 + * null/空串直接返回,避免 `JSON.parse('')` 抛错。 + */ +function parse(raw: string): unknown { + if (raw === null || raw === '') return raw + try { + return JSON.parse(raw) + } catch { + return raw + } +} + +export function useAppSettingsStore() { + return { + cache, + loadAll, + get, + set, + remove, + useSetting, + } +} diff --git a/src/stores/index.ts b/src/stores/index.ts index 860306e..878d23a 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,8 +1,9 @@ export { useProjectStore } from './project' export { useKnowledgeStore } from './knowledge' -export type { KnowledgeItem } from './knowledge' export { useSettingsStore } from './settings' export type { AIProvider, Connection, GeneralSettings } from './settings' + +export { useAppSettingsStore } from './appSettings' diff --git a/src/stores/knowledge.ts b/src/stores/knowledge.ts index f24ad67..aa1c6e6 100644 --- a/src/stores/knowledge.ts +++ b/src/stores/knowledge.ts @@ -1,79 +1,156 @@ import { reactive, computed } from 'vue' +import { knowledgeApi } from '../api' +import type { + KnowledgeRecord, + KnowledgeDetailPayload, + KnowledgeEventRecord, + CreateKnowledgeInput, + UpdateKnowledgeInput, + KnowledgeConfig, +} from '../api/types' -export interface KnowledgeItem { - id: number - title: string - description: string - category: string - tags: string[] - reuseCount: number - score: number - updatedAt: string -} +// ── 全局响应式状态(单例) ── const state = reactive({ - items: [ - // 审查规则 - { id: 1, title: 'SQL 注入防护检查', description: '所有 SQL 拼接必须使用参数化查询,禁止字符串拼接用户输入', category: 'review', tags: ['安全', 'SQL', 'Go'], reuseCount: 23, score: 95, updatedAt: '3 天前' }, - { id: 2, title: '错误处理规范', description: '禁止吞掉 error,必须向上传播或记录日志', category: 'review', tags: ['Go', '规范', '错误处理'], reuseCount: 18, score: 88, updatedAt: '1 周前' }, - { id: 3, title: 'API 响应格式一致性', description: '统一使用 { code, message, data } 结构,HTTP 状态码语义正确', category: 'review', tags: ['API', '规范'], reuseCount: 15, score: 82, updatedAt: '5 天前' }, - // Prompt 模板 - { id: 4, title: 'SQL 查询生成 Prompt', description: '根据自然语言生成 SQL,包含表结构上下文和示例输出格式', category: 'prompt', tags: ['SQL', 'LLM', '模板'], reuseCount: 142, score: 91, updatedAt: '昨天' }, - { id: 5, title: '代码审查 Prompt', description: 'AI 代码审查专用 Prompt,涵盖安全、性能、可维护性维度', category: 'prompt', tags: ['代码审查', 'LLM'], reuseCount: 87, score: 85, updatedAt: '3 天前' }, - { id: 6, title: 'API 文档生成 Prompt', description: '从 Handler 代码自动生成 API 文档的 Prompt 模板', category: 'prompt', tags: ['文档', 'LLM', '自动生成'], reuseCount: 34, score: 78, updatedAt: '1 周前' }, - // 踩坑经验 - { id: 7, title: 'Go context 传递陷阱', description: 'goroutine 中必须传递 context 而非创建新的,否则超时控制失效', category: 'pitfall', tags: ['Go', '并发', 'context'], reuseCount: 12, score: 92, updatedAt: '2 天前' }, - { id: 8, title: 'Vue 3 作用域坑', description: 'v-for 内 ref 绑定不会自动响应式,需使用数组形式', category: 'pitfall', tags: ['Vue', '前端', '响应式'], reuseCount: 8, score: 75, updatedAt: '1 周前' }, - { id: 9, title: 'Docker 网络模式选择', description: 'host 模式在 Mac/Win 上无效,必须用端口映射', category: 'pitfall', tags: ['Docker', '网络'], reuseCount: 6, score: 70, updatedAt: '2 周前' }, - // 诊断知识 - { id: 10, title: 'MySQL 慢查询诊断流程', description: 'EXPLAIN → 索引检查 → 慢查询日志分析 → 优化建议', category: 'diagnosis', tags: ['MySQL', '性能', '诊断'], reuseCount: 31, score: 89, updatedAt: '4 天前' }, - { id: 11, title: 'Go 内存泄漏排查', description: 'pprof heap 分析 → goroutine 泄漏检查 → GC 调优', category: 'diagnosis', tags: ['Go', '内存', 'pprof'], reuseCount: 9, score: 83, updatedAt: '1 周前' }, - { id: 12, title: '前端白屏诊断', description: 'Console 错误 → 网络请求 → 路由配置 → 构建产物检查', category: 'diagnosis', tags: ['前端', '调试', 'Vue'], reuseCount: 14, score: 80, updatedAt: '5 天前' }, - // 部署经验 - { id: 13, title: 'Go 二进制热更新', description: 'kill + mv + nohup 启动,无需 systemd 的轻量部署方案', category: 'deploy', tags: ['Go', '部署', 'Linux'], reuseCount: 19, score: 86, updatedAt: '昨天' }, - { id: 14, title: 'SCP 上传最佳实践', description: '使用 ssh-proxy upload 代替 scp,支持配置化管理', category: 'deploy', tags: ['SCP', '部署', '工具'], reuseCount: 11, score: 72, updatedAt: '3 天前' }, - { id: 15, title: 'Nginx 反向代理配置', description: 'u-ask 的 Nginx 配置模板,含 WebSocket 支持和 gzip', category: 'deploy', tags: ['Nginx', '配置', '反向代理'], reuseCount: 7, score: 77, updatedAt: '1 周前' }, - ] as KnowledgeItem[], - - categories: [ - { key: 'all', label: '全部', icon: '📦' }, - { key: 'review', label: '审查规则', icon: '🔍' }, - { key: 'prompt', label: 'Prompt模板', icon: '💬' }, - { key: 'pitfall', label: '踩坑经验', icon: '⚠️' }, - { key: 'diagnosis', label: '诊断知识', icon: '🩺' }, - { key: 'deploy', label: '部署经验', icon: '🚀' }, - ], + items: [] as KnowledgeRecord[], + candidates: [] as KnowledgeRecord[], + config: null as KnowledgeConfig | null, + loading: false, + error: null as string | null, }) -export function useKnowledgeStore() { - const items = computed(() => state.items) +// 7 种知识类型(snake_case,与后端 KnowledgeKind 对齐) +// 仅存 key + icon;展示文案(label)走 i18n: $t('knowledge.kind.') +export const KNOWLEDGE_KINDS = [ + { key: 'review_rule', icon: '🔍' }, + { key: 'prompt_template', icon: '💬' }, + { key: 'pitfall', icon: '⚠️' }, + { key: 'architecture_pattern', icon: '🏛️' }, + { key: 'diagnosis', icon: '🩺' }, + { key: 'deployment_note', icon: '🚀' }, + { key: 'workflow_optimization', icon: '⚡' }, +] as const - const getByCategory = (category: string) => - computed(() => - category === 'all' - ? state.items - : state.items.filter(i => i.category === category) - ) - - const search = (query: string) => - computed(() => { - if (!query.trim()) return state.items - const q = query.toLowerCase() - return state.items.filter(i => - i.title.toLowerCase().includes(q) || - i.tags.some(t => t.toLowerCase().includes(q)) || - i.description.toLowerCase().includes(q) - ) - }) - - const getCategoryCount = (key: string) => - key === 'all' ? state.items.length : state.items.filter(i => i.category === key).length - - return { - items, - categories: computed(() => state.categories), - getByCategory, - search, - getCategoryCount, +/** 解析 tags JSON 字符串为 string[] */ +export function parseTags(tags: string | null): string[] { + if (!tags) return [] + try { + const arr = JSON.parse(tags) + return Array.isArray(arr) ? arr : [] + } catch { + return [] } } + +export function useKnowledgeStore() { + // ── 加载列表(默认排除 archived) ── + async function loadList(status?: string) { + state.loading = true + state.error = null + try { + state.items = await knowledgeApi.list(status ?? null) + } catch (e: any) { + state.error = e?.toString() ?? '加载知识库失败' + } finally { + state.loading = false + } + } + + // ── 加载审核收件箱(候选列表) ── + async function loadCandidates() { + try { + state.candidates = await knowledgeApi.listCandidates() + } catch (e: any) { + state.error = e?.toString() ?? '加载收件箱失败' + } + } + + async function search(query: string, kind?: string) { + state.loading = true + state.error = null + try { + state.items = await knowledgeApi.search({ query, kind }) + } catch (e: any) { + state.error = e?.toString() ?? '检索失败' + } finally { + state.loading = false + } + } + + async function create(input: CreateKnowledgeInput) { + const record = await knowledgeApi.create(input) + // 手动录入默认 candidate,刷新收件箱 + await loadCandidates() + return record + } + + async function updateStatus(id: string, status: string) { + await knowledgeApi.updateStatus(id, status) + // 从 items 和 candidates 中同步移除 + state.items = state.items.filter(k => k.id !== id) + state.candidates = state.candidates.filter(k => k.id !== id) + } + + async function archive(id: string) { + await knowledgeApi.archive(id) + state.items = state.items.filter(k => k.id !== id) + state.candidates = state.candidates.filter(k => k.id !== id) + } + + // ── 配置 ── + async function loadConfig() { + try { + state.config = await knowledgeApi.getConfig() + } catch (e: any) { + state.error = e?.toString() ?? '加载配置失败' + } + } + + async function saveConfig(config: KnowledgeConfig) { + await knowledgeApi.saveConfig(config) + state.config = config + } + + async function extractNow() { + await knowledgeApi.extractNow() + await loadCandidates() + } + + // ── 生命线:详情 / 编辑 / 事件查询(按需调用,不入全局 state) ── + async function getDetail(id: string): Promise { + return knowledgeApi.getDetail(id) + } + + async function update(id: string, input: UpdateKnowledgeInput): Promise { + const updated = await knowledgeApi.update(id, input) + // 同步刷新内存中的列表/收件箱条目 + const patch = (arr: KnowledgeRecord[]) => { + const idx = arr.findIndex(k => k.id === id) + if (idx >= 0) arr[idx] = { ...arr[idx], ...updated } + } + patch(state.items) + patch(state.candidates) + return updated + } + + async function getEvents(knowledgeId: string, eventType?: string, limit?: number): Promise { + return knowledgeApi.events(knowledgeId, eventType, limit) + } + + // 候选数量(供侧栏 badge 用) + const candidateCount = computed(() => state.candidates.length) + + return reactive({ + // 状态用 getter 实时读 state(防快照不跟随,同 project store 模式) + get items() { return state.items }, + get candidates() { return state.candidates }, + get config() { return state.config }, + get loading() { return state.loading }, + get error() { return state.error }, + candidateCount, + // actions + loadList, loadCandidates, search, create, updateStatus, archive, + loadConfig, saveConfig, extractNow, + getDetail, update, getEvents, + }) +} diff --git a/src/stores/project.ts b/src/stores/project.ts index 8378867..1030c0f 100644 --- a/src/stores/project.ts +++ b/src/stores/project.ts @@ -7,6 +7,7 @@ import type { ProjectRecord, TaskRecord, IdeaRecord, WorkflowRecord, WorkflowEve const state = reactive({ projects: [] as ProjectRecord[], + deletedProjects: [] as ProjectRecord[], tasks: [] as TaskRecord[], ideas: [] as IdeaRecord[], workflowExecutions: [] as WorkflowRecord[], @@ -24,7 +25,7 @@ const state = reactive({ let _eventUnlisten: (() => void) | null = null -export function useProjectStore() { +function createStore() { // ── 项目 CRUD ── async function loadProjects() { state.loading = true @@ -38,10 +39,18 @@ export function useProjectStore() { } } - async function createProject(name: string, description = '', ideaId?: string) { - const record = await projectApi.create({ name, description, idea_id: ideaId }) - state.projects.push(record) - return record + // 清除错误状态(供 toast 显示后重置,允许连续同值错误再次触发 watch) + function clearError() { state.error = null } + + async function createProject(name: string, description = '', ideaId?: string, path?: string, stack?: string) { + try { + const record = await projectApi.create({ name, description, idea_id: ideaId, path, stack }) + state.projects.push(record) + return record + } catch (e: any) { + state.error = e?.toString() ?? '创建项目失败' + return null + } } async function updateProject(id: string, field: string, value: string) { @@ -52,9 +61,48 @@ export function useProjectStore() { } } + /** 重定位项目目录(后端重探测 stack,返回最新记录并更新本地状态) */ + async function relocateProjectPath(id: string, newPath: string) { + const record = await projectApi.relocatePath(id, newPath) + const idx = state.projects.findIndex(p => p.id === id) + if (idx >= 0) state.projects[idx] = record + return record + } + async function deleteProject(id: string) { - await projectApi.delete(id) - state.projects = state.projects.filter(p => p.id !== id) + try { + await projectApi.delete(id) // 软删 → 回收站(可恢复) + state.projects = state.projects.filter(p => p.id !== id) + } catch (e: any) { + state.error = e?.toString() ?? '删除项目失败' + } + } + + async function loadDeletedProjects() { + try { + state.deletedProjects = await projectApi.listDeleted() + } catch (e: any) { + state.error = e?.toString() ?? '加载回收站失败' + } + } + + async function restoreProject(id: string) { + try { + await projectApi.restore(id) + state.deletedProjects = state.deletedProjects.filter(p => p.id !== id) + await loadProjects() // 恢复后刷新活跃列表 + } catch (e: any) { + state.error = e?.toString() ?? '恢复项目失败' + } + } + + async function purgeProject(id: string) { + try { + await projectApi.purge(id) // 彻底删(级联物理删,不可恢复) + state.deletedProjects = state.deletedProjects.filter(p => p.id !== id) + } catch (e: any) { + state.error = e?.toString() ?? '彻底删除失败' + } } // ── 任务 CRUD ── @@ -67,9 +115,14 @@ export function useProjectStore() { } async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string }) { - const record = await taskApi.create(input) - state.tasks.push(record) - return record + try { + const record = await taskApi.create(input) + state.tasks.push(record) + return record + } catch (e: any) { + state.error = e?.toString() ?? '创建任务失败' + return null + } } async function updateTask(id: string, field: string, value: string) { @@ -81,8 +134,12 @@ export function useProjectStore() { } async function deleteTask(id: string) { - await taskApi.delete(id) - state.tasks = state.tasks.filter(t => t.id !== id) + try { + await taskApi.delete(id) + state.tasks = state.tasks.filter(t => t.id !== id) + } catch (e: any) { + state.error = e?.toString() ?? '删除任务失败' + } } // ── 想法 CRUD ── @@ -95,9 +152,14 @@ export function useProjectStore() { } async function createIdea(input: { title: string; description?: string; priority?: number; tags?: string; source?: string }) { - const record = await ideaApi.create(input) - state.ideas.push(record) - return record + try { + const record = await ideaApi.create(input) + state.ideas.push(record) + return record + } catch (e: any) { + state.error = e?.toString() ?? '创建想法失败' + return null + } } async function updateIdea(id: string, field: string, value: string) { @@ -109,8 +171,25 @@ export function useProjectStore() { } async function deleteIdea(id: string) { - await ideaApi.delete(id) - state.ideas = state.ideas.filter(i => i.id !== id) + try { + await ideaApi.delete(id) + state.ideas = state.ideas.filter(i => i.id !== id) + } catch (e: any) { + state.error = e?.toString() ?? '删除想法失败' + } + } + + async function evaluateIdea(id: string) { + const record = await ideaApi.evaluate(id) + const idx = state.ideas.findIndex(i => i.id === id) + if (idx >= 0) state.ideas[idx] = record + return record + } + + async function promoteIdea(id: string) { + const res = await ideaApi.promote(id) + await loadIdeas() // 后端已回写 status=promoted/promoted_to,刷新列表 + return res } // ── 工作流 ── @@ -129,11 +208,14 @@ export function useProjectStore() { async function startEventListener() { if (_eventUnlisten) return _eventUnlisten _eventUnlisten = await workflowApi.onEvent((payload) => { - state.liveEvents.push(payload) - - // 处理人工审批请求 - if (payload.event.type === 'HumanApprovalRequest') { - state.pendingApproval = payload.event.data as typeof state.pendingApproval + try { + state.liveEvents.push(payload) + // 处理人工审批请求 + if (payload.event?.type === 'HumanApprovalRequest') { + state.pendingApproval = payload.event.data as typeof state.pendingApproval + } + } catch (e) { + console.error('处理工作流事件失败:', e, payload) } }) return _eventUnlisten @@ -182,20 +264,24 @@ export function useProjectStore() { })) return reactive({ - // reactive state — 直接引用 state 属性,已经是响应式的 - projects: state.projects, - tasks: state.tasks, - ideas: state.ideas, - workflowExecutions: state.workflowExecutions, - liveEvents: state.liveEvents, - loading: state.loading, - error: state.error, + // 状态用 getter 实时读 state —— 直接 ideas: state.ideas 会快照引用, + // loadIdeas 等重新赋值 state.ideas 时返回对象的 ideas 属性不跟随,刷新后视图为空 + get projects() { return state.projects }, + get tasks() { return state.tasks }, + get ideas() { return state.ideas }, + get workflowExecutions() { return state.workflowExecutions }, + get liveEvents() { return state.liveEvents }, + get loading() { return state.loading }, + get error() { return state.error }, + clearError, + get deletedProjects() { return state.deletedProjects }, // project actions - loadProjects, createProject, updateProject, deleteProject, + loadProjects, createProject, updateProject, deleteProject, relocateProjectPath, + loadDeletedProjects, restoreProject, purgeProject, // task actions loadTasks, createTask, updateTask, deleteTask, // idea actions - loadIdeas, createIdea, updateIdea, deleteIdea, + loadIdeas, createIdea, updateIdea, deleteIdea, evaluateIdea, promoteIdea, // workflow actions runWorkflow, loadWorkflowExecutions, startEventListener, stopEventListener, clearLiveEvents, approveHumanApproval, pendingApproval: computed(() => state.pendingApproval), @@ -203,3 +289,13 @@ export function useProjectStore() { stats, }) } + +type ProjectStore = ReturnType +let _storeInstance: ProjectStore | null = null + +/** 项目/任务/想法/工作流 全局状态(单例,多组件复用同一 reactive 包装) */ +export function useProjectStore(): ProjectStore { + if (_storeInstance) return _storeInstance + _storeInstance = createStore() + return _storeInstance +} diff --git a/src/styles/global.css b/src/styles/global.css index 733298d..c240daa 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -55,6 +55,7 @@ --df-gap-grid: 12px; /* 卡片/网格 gap */ --df-gap-head: 10px; /* 面板标题→内容 */ --df-pad-panel: 14px 16px; /* 面板/卡片内边距 */ + --df-msg-max-width: 90%; /* AI 气泡/工具卡片最大宽度(对齐不撑满) */ /* — Typography: 仅 400/500 字重 — */ --df-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; diff --git a/src/views/Ideas.vue b/src/views/Ideas.vue index c1687e5..265531c 100644 --- a/src/views/Ideas.vue +++ b/src/views/Ideas.vue @@ -1,10 +1,10 @@