commit 98393b49081e89fd5bea9f8de7803eb590bce729 Author: 绝尘 <237809796@qq.com> Date: Fri Jun 12 01:31:05 2026 +0800 新增: 初始化 DevFlow 项目仓库 Tauri 2 + Vue 3 + Vite 6 桌面应用,Rust workspace 含 13 个 crate (df-ai / df-storage / df-workflow / df-core / df-execute 等)。 核心能力:AI 聊天 agentic 循环(工具调用+人工审批)、工作流引擎、 任务/想法/项目/阶段管理、可追溯性,及配套前端组件。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ba5a4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependencies +node_modules/ + +# Build output +dist/ +dist-ssr/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Rust / Cargo +target/ +!Cargo.lock + +# Tauri +src-tauri/target/ +src-tauri/Cargo.lock + +# Tauri 自动生成的 schema(每次构建变,可重建) +src-tauri/gen/schemas/ + +# 本地工具缓存(Claude Code 会话/锁) +.claude/ + +# OS 缓存 +.DS_Store +Thumbs.db + +# TS 增量构建 + 测试覆盖率 +*.tsbuildinfo +coverage/ + +# 本地环境覆盖 +*.local +.env +.env.* +!.env.example diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..eca2276 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,542 @@ +# DevFlow — 产研全流程工作流平台 + +> 版本: v0.1.0 | 创建: 2026-06-10 | 状态: 设计阶段 + +## 一、项目定位 + +**AI 原生的产研操作系统**,从想法到上线的全流程编排。 + +### 核心特性 + +- **想法池 (Idea Pool)**:持续捕捉、AI 评估、漏斗筛选、晋升立项 +- **多项目并行**:多项目同时推进,共享 AI/执行资源 +- **多任务/分支并行**:同一项目内多任务同时开发,各绑独立 Git 分支,完成后合并 +- **工作流引擎**:DAG 驱动,节点可扩展,支持条件分支和断点续跑 +- **AI 编排**:多模型并行(Claude/GLM/DeepSeek),多 Agent 协作 +- **标注系统 (Annotation)**:所有内容支持 FIXME/TODO/QUESTION 等标注,统一收集后批量交给 AI 处理 +- **需求-功能-测试可追溯**:功能可选做/延/不做,每个功能对应测试用例和测试报告 +- **决策留痕 (Decision Journal)**:所有关键决策自动记录(原因/方案/时间/上下文),全程可追溯 +- **经验进化 (Evolution)**:开发过程中的模式自动沉淀为知识库(审查规则/Prompt模板/踩坑经验),持续进化复用 +- **阶段插件**:想法→需求→编码→测试→发布,阶段即模板 + +### 层级模型 + +``` +💡 Idea Pool (想法池) — 独立运转,持续捕捉和评估 + └→ 📂 Project (项目) — 多项目并行 + ├→ 🔀 Task (任务) — 绑定 Git 分支,独立工作流 + │ └→ Workflow DAG (编码→测试→审查) + ├→ 🔀 Task (任务) — 另一个并行任务 + │ └→ Workflow DAG + └→ 🎯 Release (发布) — 合并多个 Task → 集成测试 → 发布 +``` + +## 二、技术栈 + +| 层 | 技术 | 说明 | +|----|------|------| +| Desktop | Tauri v2 | Rust 后端 + WebView 前端 | +| Frontend | Vue 3 + TypeScript + Pinia | Arco Design 组件库 | +| Engine | Rust (Workspace) | 多 crate 架构 | +| Storage | SQLite (rusqlite) | 本地优先,零运维 | +| AI | Multi-Provider | Claude/GLM/DeepSeek/OpenAI 兼容 | +| Container | Docker (bollard) | 隔离构建/测试环境 | + +## 三、系统架构 + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ DevFlow Desktop │ +│ Tauri v2 · Vue 3 · TS │ +├──────────────────────────────────────────────────────────────────┤ +│ 💡 Idea Pool │ 📂 Multi-Project Manager │ +│ 捕捉·评估·晋升 │ 多项目并行·资源调度 │ +├────────────────────────┴─────────────────────────────────────────┤ +│ 🔀 Task & Branch Manager (任务/分支管理) │ +│ 多任务并行·分支创建·合并协调·冲突解决 │ +│ ┌──────────┬──────────┬──────────┬──────────────────────────┐ │ +│ │ Task A │ Task B │ Task C │ Release │ │ +│ │ feat/auth│feat/pay │fix/login │ main │ │ +│ │ [DAG] │ [DAG] │ [DAG] │ [合并→测试→发布] │ │ +│ └──────────┴──────────┴──────────┴──────────────────────────┘ │ +├──────────────────────────────────────────────────────────────────┤ +│ Workflow Engine (DAG · Node · State · Event · Persist) │ +├──────────────────────┬───────────────────────────────────────────┤ +│ AI Orchestrator │ Execution Runtime │ +│ Multi-Provider │ Shell · Docker · SSH · Git (libgit2) │ +│ Agent Coordinator │ Merge · Conflict Resolve │ +├──────────────────────┴───────────────────────────────────────────┤ +│ Storage Layer (SQLite) │ +│ ideas · projects · tasks · branches · workflows · artifacts │ +└──────────────────────────────────────────────────────────────────┘ +``` + +## 四、Crate 结构 + +``` +devflow/ +├── Cargo.toml # Workspace 根 +├── crates/ +│ ├── df-core/ # 核心类型、错误、常量、事件 +│ ├── df-workflow/ # 工作流 DAG 引擎 (核心) +│ ├── df-nodes/ # 内置节点集合 (AI/Script/Docker/Git/Human/HTTP/Subflow) +│ ├── df-ai/ # AI 编排层 (Multi-Provider/Router/Coordinator) +│ ├── 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/动态库) +├── src/ # Tauri 主入口 +│ ├── main.rs +│ ├── state.rs +│ └── commands/ # IPC 命令 +├── frontend/ # Vue 3 前端 +│ └── src/ +│ ├── views/ # 页面组件 +│ ├── components/ # 业务组件 +│ ├── stores/ # Pinia 状态 +│ └── composables/ # 组合式函数 +├── templates/ # 工作流模板 (YAML) +└── plugins/ # 外部插件目录 +``` + +## 五、核心模块设计 + +### 5.1 Workflow Engine (df-workflow) + +引擎不感知具体业务,只负责 DAG 执行。 + +- **Node trait**:所有节点的统一抽象 (`execute`, `schema`, `is_blocking`) +- **DAG Executor**:拓扑排序 → 并行调度 → 状态流转 → 持久化 +- **EventBus**:`tokio::sync::broadcast` 异步事件广播 +- **Persister**:SQLite 快照,支持断点续跑 +- **Conditions**:基于表达式的条件分支引擎 + +### 5.2 Idea Pool (df-ideas) + +想法是独立于项目的第一公民。 + +- **Capture**:文本/剪贴板/快捷键捕捉,不打断当前工作 +- **Evaluator**:AI 自动评估市场潜力、竞品、技术可行性 +- **Scoring**:多维加权评分 (0-100) +- **Graph**:想法关联图,相似想法自动发现,可合并 +- **Promotion**:高分想法晋升为项目,自动携带评估结论 + +想法状态:`Draft → PendingReview → Approved → Promoted / Rejected / Archived` + +### 5.3 Multi-Project Manager (df-project) + +- **ProjectSlot**:每个项目的运行时槽位(活跃任务数、优先级、AI 配额) +- **Scheduler**:AI 并发预算分配、Docker 资源配额 +- **Context**:项目上下文(代码结构、依赖、规范),供 AI 消费 +- **Timeline**:项目时间线,所有事件的时序视图 + +项目状态:`Planning / InProgress / Testing / Releasing / Completed / Paused / Cancelled` + +### 5.3.1 Task & Branch Manager (df-task) + +项目内部的并行任务管理,每个任务绑定一个 Git 分支。 + +- **Task**:独立工作单元,包含标题、描述、绑定的分支、关联的工作流 +- **BranchManager**:自动创建分支、跟踪分支状态、与 main 的 diff 统计 +- **MergeCoordinator**:合并策略管理、冲突检测、AI 辅助冲突解决 +- **ReleasePlanner**:选择多个已完成的 Task 合并,编排集成测试和发布流程 + +``` +Task 生命周期: + Todo → InProgress → InReview → Testing → Done + ↓ (随时) ↓ + Blocked Cancelled + +分支策略: + main ──────────────────────────────── + └── feature/auth ──── ✅ merged + └── feature/payment ─ 🔄 in progress + └── bugfix/login ──── ✅ merged + └── release/v2.3 ──── ⏳ waiting (merge auth + payment) +``` + +合并协调: +1. 冲突检测:Task 完成时自动检测与 main 的冲突 +2. AI 辅助解决:冲突文件交给 AI 分析并建议解决方案 +3. 发布编排:选择多个 Task → 创建 release 分支 → 合并 → 集成测试 → 发布 + +### 5.4 Traceability & Annotation (df-traceability) + +贯穿所有阶段的可追溯性引擎。 + +#### 5.4.1 标注系统 (Annotation) + +任何内容(需求文档、代码、测试报告、设计文档)中都可以插入标注: + +| 标记 | 含义 | 场景 | +|------|------|------| +| `[FIXME]` | 需要修复 | 代码/文档中发现问题 | +| `[TODO]` | 待办事项 | 后续需要补充的内容 | +| `[QUESTION]` | 疑问待确认 | 需要人工决策的问题 | +| `[RISK]` | 风险标记 | 潜在的技术/业务风险 | +| `[DECISION]` | 决策标记 | 记录为什么做此选择 | +| `[OPTIMIZE]` | 优化建议 | 可改进但不紧急 | + +**批量处理流程**: +``` +人工在各内容中插标注 → 系统统一收集所有未处理标注 + → 按类型/优先级分组 → 一键交给 AI 批量处理 + → AI 逐条处理并标记完成 → 人工确认处理结果 +``` + +标注可以附加在任何实体上(需求、功能、代码文件、测试用例)。 + +#### 5.4.2 需求-功能-测试映射 + +``` +📋 需求 (Requirement) + └→ 功能 (Feature) [状态: Selected / Deferred / Rejected] + └→ 测试用例 (TestCase) [自动/手动生成] + └→ 测试执行记录 (TestRun) + └→ 测试报告 (TestReport) +``` + +- **功能状态**:`Selected(选中做)` / `Deferred(延期)` / `Rejected(不做)` +- 每个功能可标注不做的**原因**(自动进入决策留痕) +- AI 根据功能描述**自动生成测试用例** +- 测试用例与功能**双向关联**,覆盖率一目了然 +- 测试报告自动生成,标注 PASS/FAIL/SKIP + +#### 5.4.3 决策留痕 (Decision Journal) + +所有关键决策自动或半自动记录,全程可追溯。 + +```rust +pub struct Decision { + pub id: DecisionId, + pub project_id: ProjectId, + pub context: String, // 决策背景 + pub question: String, // 需要决定的问题 + pub alternatives: Vec,// 考虑过的方案 + pub decision: String, // 最终决定 + pub reason: String, // 决策原因 + pub decided_by: DecidedBy, // AI / Human + pub related_entity: EntityRef,// 关联实体(功能/需求/代码等) + pub impact: String, // 影响范围 + pub created_at: DateTime, +} +``` + +**自动记录的决策场景**: +- 功能标记为"不做"时,强制填写原因 +- AI 选择了方案 A 而非方案 B 时,记录理由 +- 代码审查中发现风险时的处理决策 +- 测试失败后的修复策略选择 +- 发布前的检查点决策 + +**决策时间线视图**:按时间展示项目的所有决策,支持按类型/阶段筛选。 + +### 5.4 AI Orchestrator (df-ai) + +- **LlmProvider trait**:统一接口,各模型实现 +- **ModelRouter**:按任务类型路由到最优模型 + 降级链 +- **AgentCoordinator**:多 Agent 协作(Planner/Coder/Reviewer/Fixer) +- **ContextManager**:Token 预算管理 +- **ToolRegistry**:工具注册(供 Agent 调用) + +### 5.5 内置节点 (df-nodes) + +| 节点 | 功能 | +|------|------| +| AINode | 调用 LLM,支持流式输出、工具调用 | +| ScriptNode | Shell/脚本执行 | +| DockerNode | Docker 容器操作 | +| GitNode | Git 操作 (libgit2) | +| HumanNode | 人工审批/确认 (阻塞) | +| NotifyNode | 通知 (桌面/飞书/Webhook) | +| HTTPNode | HTTP 请求 | +| SubflowNode | 嵌套子工作流 | + +## 六、数据模型 + +### SQLite 表结构 + +```sql +-- 想法池 +CREATE TABLE ideas ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + tags TEXT, -- JSON array + source TEXT NOT NULL, -- Manual/Clipboard/AI + status TEXT NOT NULL, -- Draft/PendingReview/Approved/Rejected/Promoted/Archived + scores TEXT, -- JSON: {market_potential, competition, feasibility, roi, overall} + ai_analysis TEXT, + related_ideas TEXT, -- JSON array of idea ids + promoted_to TEXT, -- project id + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- 项目 +CREATE TABLE projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + idea_id TEXT, -- 来源想法 + repo_path TEXT, -- 仓库路径 + tech_stack TEXT, -- JSON array + status TEXT NOT NULL, -- Planning/InProgress/Testing/Releasing/Completed/Paused/Cancelled + priority INTEGER NOT NULL DEFAULT 1, -- 0=Low, 1=Medium, 2=High, 3=Critical + current_stage TEXT, -- Idea/Requirement/Coding/Testing/Release + config TEXT, -- JSON: 项目级配置 + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- 工作流定义 +CREATE TABLE workflow_defs ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + task_id TEXT, -- 关联的任务 (可为空,如项目级工作流) + name TEXT NOT NULL, + description TEXT, + dag TEXT NOT NULL, -- JSON: DAG 结构 + template_id TEXT, -- 来源模板 + version INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- 任务 (项目内的并行工作单元) +CREATE TABLE tasks ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL, -- Todo/InProgress/InReview/Testing/Done/Blocked/Cancelled + priority INTEGER NOT NULL DEFAULT 1, -- 0=Low, 1=Medium, 2=High, 3=Critical + branch_name TEXT, -- Git 分支名 + base_branch TEXT DEFAULT 'main', -- 基于哪个分支创建 + workflow_def_id TEXT, -- 关联的工作流定义 + assignee TEXT, -- AI agent 或人工 + merge_conflict TEXT, -- JSON: 冲突信息 + merged_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- 发布计划 (合并多个 Task → 发布) +CREATE TABLE releases ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + version TEXT NOT NULL, -- e.g. v2.3.0 + branch_name TEXT, -- release 分支 + task_ids TEXT NOT NULL, -- JSON array: 包含的 Task ID 列表 + status TEXT NOT NULL, -- Planning/Integrating/Testing/Ready/Published + workflow_def_id TEXT, -- 发布工作流 + changelog TEXT, + published_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- 工作流执行 +CREATE TABLE workflow_runs ( + id TEXT PRIMARY KEY, + workflow_def_id TEXT NOT NULL, + project_id TEXT NOT NULL, + task_id TEXT, -- 关联的任务 (可为空) + release_id TEXT, -- 关联的发布 (可为空) + status TEXT NOT NULL DEFAULT 'pending', -- Pending/Running/Paused/Completed/Failed/Cancelled + node_states TEXT, -- JSON: {node_id: {status(Pending/Running/Completed/Failed/Skipped/Waiting), output, started_at, finished_at}} + current_layer INTEGER, + started_at INTEGER, + finished_at INTEGER, + error TEXT +); + +-- 产出物 +CREATE TABLE artifacts ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + workflow_run_id TEXT, + node_id TEXT, + kind TEXT NOT NULL, -- PRD/CodeChange/TestReport/ReleaseNote/Image + title TEXT, + content TEXT, + file_path TEXT, + metadata TEXT, -- JSON + created_at INTEGER NOT NULL +); + +-- 连接配置 +CREATE TABLE connections ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + kind TEXT NOT NULL, -- MySQL/SSH/MongoDB/Redis/HTTP + config TEXT NOT NULL, -- JSON (加密存储) + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- AI 模型配置 +CREATE TABLE ai_providers ( + id TEXT PRIMARY KEY, + provider TEXT NOT NULL, -- Claude/GLM/DeepSeek/OpenAI + api_key TEXT, -- 加密存储 + base_url TEXT, + models TEXT, -- JSON: 可用模型列表 + is_default INTEGER DEFAULT 0, + config TEXT -- JSON: 额外配置 +); + +-- 需求功能清单 +CREATE TABLE features ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + task_id TEXT, -- 关联任务 + requirement_id TEXT, -- 来源需求 + title TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'selected', -- Selected/Deferred/Rejected/Completed + priority INTEGER NOT NULL DEFAULT 1, -- 0=Low, 1=Medium, 2=High, 3=Critical + rejection_reason TEXT, -- 不做的原因(自动进入决策留痕) + test_case_count INTEGER DEFAULT 0, + order_index INTEGER NOT NULL, -- 排序 + metadata TEXT, -- JSON: 额外属性 + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +-- 测试用例 +CREATE TABLE test_cases ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + feature_id TEXT NOT NULL, -- 关联功能 + title TEXT NOT NULL, + description TEXT, + preconditions TEXT, -- 前置条件 + steps TEXT NOT NULL, -- JSON: 测试步骤 [{step, expected}] + kind TEXT NOT NULL DEFAULT 'Manual', -- Manual/Auto/AI-Generated + priority INTEGER NOT NULL DEFAULT 1, -- 0=Low, 1=Medium, 2=High, 3=Critical +); + +-- 测试执行记录 +CREATE TABLE test_runs ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + task_id TEXT, -- 关联任务 + feature_id TEXT, -- 关联功能 + test_case_id TEXT NOT NULL, + status TEXT NOT NULL, -- Passed/Failed/Skipped/Blocked + actual_result TEXT, + error_detail TEXT, -- 失败详情 + executed_by TEXT, -- AI/Human + duration_ms INTEGER, + executed_at INTEGER NOT NULL +); + +-- 标注 (贯穿所有内容) +CREATE TABLE annotations ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + entity_type TEXT NOT NULL, -- Requirement/Feature/Code/TestCase/TestReport/DesignDoc + entity_id TEXT NOT NULL, -- 关联实体 ID + marker TEXT NOT NULL, -- FIXME/TODO/QUESTION/RISK/DECISION/OPTIMIZE + content TEXT NOT NULL, -- 标注内容 + location TEXT, -- 文件路径+行号 / 文档段落 + status TEXT NOT NULL DEFAULT 'Open', -- Open/AI-Processing/Resolved/WontFix + resolved_by TEXT, -- AI/Human + resolution TEXT, -- 处理结果 + created_at INTEGER NOT NULL, + resolved_at INTEGER +); + +-- 决策留痕 +CREATE TABLE decisions ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + context TEXT NOT NULL, -- 决策背景 + question TEXT NOT NULL, -- 需要决定的问题 + alternatives TEXT, -- JSON array: 考虑过的方案 + decision TEXT NOT NULL, -- 最终决定 + reason TEXT NOT NULL, -- 决策原因 + decided_by TEXT NOT NULL, -- AI/Human + entity_type TEXT, -- 关联实体类型 + entity_id TEXT, -- 关联实体 ID + impact TEXT, -- 影响范围 + stage TEXT, -- Idea/Requirement/Coding/Testing/Release + created_at INTEGER NOT NULL +); +``` + +## 七、阶段模板 + +5 个内置阶段作为工作流模板(YAML 定义),用户可自定义。 + +- 💡 **想法**:市场分析 → 竞品调研 → 可行性评分 +- 📋 **需求**:AI 生成 PRD → 人工审阅 → 任务拆解 +- 💻 **编码**:AI 编码 → 代码审查 → 自动修复 +- 🧪 **测试**:运行测试 → AI 分析失败 → 回归验证 +- 🚀 **发布**:构建 → 人工确认 → 部署 → 健康检查 + +## 八、Phase 规划 + +### Phase 1 — 引擎骨架 (4-6 周) +- df-core + df-workflow (DAG + Node trait + Executor) +- df-storage (SQLite 基础表) +- df-execute (Shell 执行) +- 最小前端:项目列表 + 工作流执行日志 +- 验证:能跑通一个 3 节点的简单工作流 + +### Phase 2 — AI 集成 (3-4 周) +- df-ai (Multi-Provider + Router + Stream) +- AI Chat 面板 +- AI Node 实现 +- 验证:AI 节点能流式输出到前端 + +### Phase 3 — 想法池 + 多项目 (3-4 周) +- df-ideas (捕捉/评估/评分/晋升) +- df-project (多项目调度/上下文) +- 前端:想法池视图 + 多项目 Tab +- 验证:想法捕捉 → AI 评估 → 立项 → 工作流执行 + +### Phase 4 — 节点丰富 + 阶段插件 (3-4 周) +- df-nodes (Docker/Git/Human/HTTP) +- df-stages (5 阶段模板) +- 条件分支 + 断点续跑 +- 验证:跑通标准产研流程模板 + +### Phase 5 — 体验打磨 (4-6 周) +- 拖拽式 DAG 编辑器 +- Dashboard + Timeline +- 工作流模板市场 +- 桌面通知 + 系统托盘 +- 自动更新 +- 发布 v1.0 + +## 九、与现有资产的关系 + +| 现有资产 | 关系 | +|---------|------| +| u-desk-rust (Tauri v2) | 复用架构经验:Workspace + crate 拆分 + Vue 3 传输层 | +| workpod (Docker) | DockerNode 的执行后端,API 集成 | +| proxy 工具链 (Rust) | 连接管理 + 执行后端,内嵌调用 | +| Skills 体系 | 核心逻辑迁移为节点模板,不保留 skill 形态 | +| product-delivery-control | 演进为「标准产研工作流模板」 | +| mission-control | 合同/质量门禁机制融入 Workflow Engine | +| CPA (LLM 代理) | AI Provider 之一 | + +## 十、关键设计决策 + +1. **引擎不绑定业务**:Workflow Engine 只做 DAG 执行,阶段是插件 +2. **想法第一公民**:想法池独立于项目,持续运转 +3. **多项目并行**:多项目同时推进,共享资源池 +4. **多任务/分支并行**:同一项目内多任务各绑分支,独立工作流,完成后合并 +5. **标注无处不在**:FIXME/TODO/QUESTION 标注可附加在任何内容上,批量收集→AI 处理 +6. **需求-测试双向追溯**:功能可选做/延/不做,每个功能映射测试用例和报告 +7. **决策必留痕**:所有关键决策自动记录,可追溯、可审计 +8. **本地优先**:SQLite 嵌入,不依赖云服务 +9. **多模型并行**:统一抽象,按任务路由,不锁定单一模型 +10. **流式优先**:AI 输出、Shell 输出全部流式推送到前端 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ebd7a87 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5797 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "devflow" +version = "0.1.0" +dependencies = [ + "anyhow", + "df-ai", + "df-core", + "df-execute", + "df-nodes", + "df-storage", + "df-workflow", + "futures", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-opener", + "tokio", + "tracing", +] + +[[package]] +name = "df-ai" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "df-core", + "eventsource-stream", + "futures", + "reqwest 0.12.28", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "df-core" +version = "0.1.0" +dependencies = [ + "chrono", + "serde", + "serde_json", + "thiserror 2.0.18", + "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" +dependencies = [ + "anyhow", + "async-trait", + "df-core", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "df-ideas" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "df-core", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "df-nodes" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "df-ai", + "df-core", + "df-execute", + "df-workflow", + "serde", + "serde_json", + "tokio", + "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" +dependencies = [ + "anyhow", + "chrono", + "df-core", + "serde", + "serde_json", + "tokio", + "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" +dependencies = [ + "anyhow", + "df-core", + "rusqlite", + "serde", + "serde_json", + "tokio", + "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" +dependencies = [ + "anyhow", + "async-trait", + "df-core", + "futures", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.4", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "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-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.3", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..db4a179 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +members = [ + "crates/*", + "src-tauri", +] +resolver = "2" + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +async-trait = "0.1" +thiserror = "2" +anyhow = "1" +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["v4"] } +rusqlite = { version = "0.31", features = ["bundled"] } +tracing = "0.1" diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..cf0072b --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,397 @@ +# DevFlow — 项目进展与工作交接 + +> 创建: 2026-06-10 | 最后更新: 2026-06-12 | 当前阶段: Phase 2 aichat UX 快赢(A 路线)+ 技能联想需求调研(Sprint 8) + +--- + +## 一、项目速览 + +| 项目 | 值 | +|------|-----| +| 定位 | AI 原生创作流程驾驶舱,从想法到创作成果的全流程管理 | +| 技术栈 | Tauri v2 + Vue 3 + TypeScript + Pinia / Rust Workspace (13 crate) / SQLite | +| 路径 | `E:/wk-lab/devflow/` | +| 架构文档 | `ARCHITECTURE.md` (22,745 字) | +| Git 状态 | 未首次 commit,代码全在 untracked | +| AI 能力 | df-ai OpenAI 兼容 Provider + 12 工具 + Agentic Loop | + +--- + +## 二、代码规模统计 + +### Rust 后端 (71 个 .rs 文件) + +| Crate | 文件数 | 总行数 | 有效行 | 实现程度 | +|-------|--------|--------|--------|---------| +| df-core | 4 | 259 | 151 | ✅ 完整 — 错误/事件/状态枚举/ID生成 | +| df-workflow | 7 | 489 | 333 | ✅ 核心 — DAG拓扑排序/执行器/状态机/事件总线 可用 | +| df-storage | 4 | 294 | 217 | ✅ 部分 — SQLite连接/迁移/建表完整,**缺 CRUD 层** | +| df-execute | 5 | 203 | 129 | ⚡ 混合 — Shell 执行器真实可用,Docker/SSH/Git 骨架 | +| df-ideas | 6 | 421 | 278 | ⚡ 大部分 — 捕获/评估/晋升/关联图 有逻辑,评分固定值 | +| df-task | 5 | 390 | 252 | ⚡ 大部分 — Task/Branch/Semver 可用,合并冲突 骨架 | +| 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** | | + +### 前端 (9 页面 + Store + i18n) + +| 类别 | 文件数 | 行数 | 状态 | +|------|--------|------|------| +| Vue 页面 | 9 | ~3,372 | ✅ UI 全部真实 — 无空壳 | +| Pinia Store | 4 | ~362 | ⚠️ 有 state/getter 但**无 action,View 未引用** | +| Router | 1 | 63 | ✅ 9 条路由含动态路由 | +| i18n | 2 | ~94 | ⚠️ 翻译完整但**未接入 main.ts** | +| 设计系统 | 1 | 105 | ✅ CSS 变量/动画/Arco 暗色覆盖 | +| Tauri Commands | 1 | 13 | 🔩 仅 `greet` 示例,无业务逻辑 | + +**前端关键问题**: +- 所有 View 组件数据**硬编码** (`ref([...])`),与 Store 完全独立 +- 所有按钮事件处理函数为**空函数** `{}` +- i18n 插件未注册到 Vue app +- Arco Design 组件库未实际使用(仅 CSS 变量覆盖) + +--- + +## 三、架构分层与依赖关系 + +``` +真实可用路径(有端到端逻辑): + df-core (类型基础) + → df-workflow (DAG 拓扑排序 + 执行器 + 状态机 + EventBus) + → df-execute/shell (tokio::process Shell 执行) + → df-storage (SQLite 连接 + 迁移 + 建表) + +待打通路径(骨架就位,需填充): + 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 +``` + +--- + +## 四、全局性问题 + +| # | 问题 | 影响 | 优先级 | 状态 | +|---|------|------|--------|------| +| 1 | **无 CRUD 层** — df-storage 只有建表 | 所有 crate 无法持久化数据 | P0 | ✅ Sprint 2 | +| 2 | **Tauri IPC 未建** — 只有 greet 示例 command | 前端无法调用后端 | P0 | ✅ Sprint 3(17 commands) | +| 3 | **Store 未接入 View** — 两套独立硬编码数据 | 数据流断裂 | P0 | ✅ Sprint 4 | +| 4 | **AI Provider 无实现** — trait 定义完整但无 HTTP client | AI 功能全部不可用 | P1 | ✅ Sprint 5 | +| 5 | **同层节点未并行** — DagExecutor 有 TODO 注释 | 工作流执行效率 | P1 | ✅ Sprint 3(join_all) | +| 6 | **条件表达式引擎** — 仅支持 true/false 字面量 | 条件分支不可用 | P2 | ⬜ 待办 | +| 7 | **i18n 未注册** — 翻译文件存在但未挂载 | 多语言不生效 | P2 | ✅ Sprint 7 | +| 8 | **未 git commit** — 代码全 untracked | 无版本基线 | P0 | ✅ Sprint 2 | + +--- + +## 五、Phase 规划与当前进度 + +### Phase 1 — 引擎骨架 ✅ 完成 + +| 任务 | 状态 | 说明 | +|------|------|------| +| df-core 类型系统 | ✅ 完成 | 错误/事件/状态枚举/ID生成 | +| df-workflow DAG 引擎 | ✅ 完成 | 拓扑排序/执行器/状态机/EventBus | +| df-storage SQLite 基础表 | ✅ 完成 | 6 张表 + 4 索引,缺 CRUD | +| df-execute Shell 执行 | ✅ 完成 | tokio::process 实现 | +| 最小前端:项目列表 + 工作流日志 | ✅ 完成 | Sprint 4:5 页面接 Store + 创建/运行交互 | +| 验证:3 节点简单工作流 | ⬜ 待验证 | 后端就绪,需端到端跑通 | +| df-storage CRUD 层 | ✅ 完成 | Sprint 2:6 Repo + impl_repo! 宏;Sprint 3 增 BranchRepo | +| 对抗式评估系统 | ✅ 完成 | Sprint 6:AdversarialEngine + 正反方辩论 + AI 分析师 | +| Migrations V2(关联字段 + branches 表) | ✅ 完成 | Sprint 3 | +| Executor 同层并行 + 状态转换校验 | ✅ 完成 | Sprint 3,4 单测全绿 | +| Tauri IPC 命令 | ✅ 完成 | Sprint 3:17 commands + 事件转发 | +| Store 接入 View | ✅ 完成 | Sprint 4:API 层 + composable Store + 5 页面改造 | + +### Phase 2 — AI 集成 ✅ 核心完成 + +| 任务 | 状态 | 说明 | +|------|------|------| +| df-ai Provider 实现 | ✅ | OpenAI 兼容 HTTP client,流式 SSE 解析 | +| AI 工具注册 + V3 迁移 | ✅ | AiToolRegistry + 12 工具(list/create/update/delete + 文件系统) | +| AI IPC 命令 | ✅ | 17 个 AI command(对话/工具执行/审批门控) | +| 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采集/滑动窗口/摘要)⬜ | | + +### Phase 3 — 想法池 + 多项目 (3-4 周) | ⬜ 未开始 +### Phase 4 — 节点丰富 + 阶段插件 (3-4 周) | ⬜ 未开始 +### Phase 5 — 体验打磨 (4-6 周) | ⬜ 未开始 + +--- + +## 六、工作迭代日志 + +### [Sprint 1] 2026-06-10 — 项目初始化 + +**工作内容**: +- 创建项目骨架:`bun create vite` + Tauri v2 + Cargo Workspace +- 编写完整架构文档 `ARCHITECTURE.md` (22,745 字) +- 13 个 Rust crate 全部创建,类型系统和接口设计完整 +- 9 个 Vue 页面 UI 全部编写(~3,372 行),设计系统完善 +- 4 个 Pinia Store 定义(state + computed) +- i18n 中英文翻译 + +**代码规模**:Rust ~4,108 行 / Vue+TS ~3,896 行 / 总计 ~8,000 行 + +**遗留问题**: +1. 所有数据硬编码,无持久化 +2. 前后端未桥接 +3. 未做 git commit + +**下一步建议**: +1. 首次 git commit 建立基线 +2. 优先实现 df-storage CRUD 层(所有持久化的前置条件) +3. 实现 Tauri IPC 命令层 +4. 将 Store 接入 View,替换硬编码数据 +5. 端到端验证:3 节点简单工作流跑通 + +--- + +### [Sprint 2] 2026-06-10/11 — 设计审查 + 对抗论证 + 阻塞项修复 + +**工作内容**: +- 三路功能审查(架构/前端/数据模型):发现 4 条核心路径全断、缺 10 张表、状态枚举三套不一致 +- 三路对抗论证(市场/技术/需求"魔法打败魔法"):综合结论 = 方向有价值、scope 必须砍 60% +- 修复 Phase 1 阻塞项:Shell Windows 兼容(cmd /C)、状态枚举统一(types.rs 为准)、DAG 序列化(DagDef + NodeRegistry) +- 实现 df-storage CRUD 层(6 Repo + impl_repo! 宏 + spawn_blocking + 列名白名单) +- ScriptNode 接入 df-execute Shell 执行器 +- 建立 docs/ 8 层文档体系(中文命名) +- 新增设计文档:业务系统设计、对抗论证裁决报告、想法探索-对抗式评估 + +**关键决策(详见 docs/02-架构设计/对抗论证裁决报告.md)**: +- 定位调整:"产研操作系统" → "本地优先的个人开发流程驾驶舱" +- 砍掉:插件系统、经验进化(自动)、需求-测试追溯、多模型路由、Agent 协作 +- 降级:想法池(简化+对抗式评估)、标注(TODO 扫描器)、决策留痕(字段级) +- Phase 1 只激活 6 个 crate,其余 7 个标记预留 +- 验证策略:先自用 3 个月再决定是否对外 + +**遗留问题**: +1. Executor 同层并行化 + StateMachine 转换校验(任务 #8) +2. Migrations V2 补字段(promoted_to/workflow_def_id/project_id 等)+ branches 表(任务 #9) +3. Tauri IPC / 前端 Store 对接尚未开始 +4. Sprint 2 的代码变更未 commit + +**下一步**: +1. commit Sprint 2 变更 +2. Migrations V2 + Executor 并行化 +3. Tauri IPC(AppState + project/task/workflow commands) +4. 前端类型对齐 + Store 接 View + +### [Sprint 3] 2026-06-11 — Migrations V2 + Executor 并行化 + Tauri IPC(三代理并行) + +**工作内容**(三个子代理并行,目录互不重叠): +- **Migrations V2**(df-storage/df-core):版本推进到 2 — ideas 补 promoted_to/ai_analysis/scores、tasks 补 workflow_def_id/base_branch、workflow_executions 补 project_id/task_id、新建 branches 表(含 2 索引);BranchRecord/BranchRepo/BranchStatus 枚举同步落地,ALLOWED_COLUMNS 白名单 +8 列 +- **Executor 并行化**(df-workflow):同层节点改 futures::join_all 并发,三阶段模式(串行准备→并发执行→串行收尾)规避借用冲突;StateMachine 增加转换校验(仅 Pending→Running、Running→Completed/Failed 合法,set_xxx 改返回 Result);4 个单测全绿(含并行耗时断言 <180ms、失败中止下游层) +- **Tauri IPC**(src-tauri):AppState(db + 6 Repo + EventBus + NodeRegistry)+ 17 个 command(project/task/idea CRUD + run_workflow/list/get executions);run_workflow 先落库 status=running 立即返回 execution_id,事件经 app.emit("workflow-event") 转发前端,完成后回写状态。注意:NodeRegistry 不能用 default()(script 工厂是 unimplemented! 占位),改为 new() + 手动注册真实 ScriptNode + +**验证**:cargo check 通过(仅 1 个 dead_code 警告:releases/node_executions Repo 暂无 command 使用);cargo test -p df-workflow 4 passed + +**遗留问题**: +1. executor 失败路径只发 NodeFailed 不发 WorkflowFailed,目前由 run_workflow 在 IPC 层补发——后续应下沉到 executor +2. 前端 Store/View 仍硬编码,未调用任何 command +3. NodeRegistry::default() 的 unimplemented! 占位是隐患,应改为注册真实节点或移除 + +**下一步**: +1. 前端类型对齐(id: string、状态枚举对齐 types.rs)+ src/api/ 封装层 +2. Store 加 action 接 API,核心 4 页面(Dashboard/Projects/ProjectDetail/Tasks)接 Store +3. 端到端验证:tauri dev 创建项目 → 运行 3 节点工作流 → 日志实时展示 + +### [Sprint 4] 2026-06-11 — 前端交互功能(API 层 + Store + 5 页面) + +**工作内容**: +- **API 层**:新建 `src/api/` 目录 — types.ts(与 Rust Record 严格对齐)+ project/task/idea/workflow 四个 invoke 封装 + 事件监听(listen workflow-event) +- **Store 重写**:`stores/project.ts` 改为 composable 模式(reactive 全局状态 + 12 个 actions + computed stats),不依赖 Pinia +- **Projects.vue**:接 Store 真数据 + 创建项目模态框(名称/描述)+ 卡片点击跳详情页 + status 中文映射 +- **ProjectDetail.vue**:路由参数加载项目 + 真实任务列表 + **3 节点 Shell DAG 工作流运行**(环境检查→运行测试→构建产物)+ 实时事件日志 + 创建任务模态框 + onUnmounted 清理监听 +- **Tasks.vue**:项目/状态双筛选联动 + 创建任务模态框(选择项目+标题)+ 状态/优先级中文映射 +- **Ideas.vue**:捕捉想法模态框 + 选中详情(tags JSON 解析、scores 展示)+ 删除功能 +- **Dashboard.vue**:统计卡从 Store computed 计算 + Active Projects/Idea Pool 接真数据 + quickCapture 跳 Ideas + +**代码变更**:13 files, +1059/-486 + +**遗留问题**: +1. 端到端验证未完成:需 tauri dev 实际创建项目→运行工作流→确认日志展示 +2. 无数据时空状态展示较简陋 +3. ~~Settings/Knowledge/Decisions 三个页面仍为硬编码 demo~~ ✅ Sprint 7 已解决(三 view 接 stores/{knowledge,settings}.ts,无 ref([ 硬编码) + +**下一步**: +1. 端到端验证:tauri dev → 创建项目 → 运行 3 节点工作流 → 日志实时展示 +2. 修复验证中发现的问题 +3. 考虑 Phase 2 规划 + +### [Sprint 5] 2026-06-11 — AI Chat 全功能(Provider + 工具执行 + Agentic Loop) + +**工作内容**: +- **df-ai Provider**:OpenAI 兼容 HTTP client,SSE 流式解析,ChatMessage/CompletionRequest 类型系统 +- **V3 存储迁移**:ai_providers、ai_conversations、ai_tool_executions 三张表 + 对应 Repo +- **AI IPC 命令层**:17 个 command — 对话发送/流式推送、工具执行/审批门控(低风险自动/中高风险人工)、对话管理(CRUD + 切换) +- **AI 工具注册**:12 个工具 — list_projects/tasks/ideas(只读)、create_project/task/idea(创建)、update_project(更新)、delete_project(删除)、read_file/list_directory/write_file(文件系统) +- **Agentic Loop**:多轮工具调用循环(max 10 轮),流式输出→检测工具调用→执行→结果回传→继续,中高风险暂停等审批后恢复 +- **AI Chat 前端**:侧边栏面板 + 拖拽调宽 + 分离独立窗口 + 对话列表管理 + 工具审批弹窗 + AgentRound 多轮消息 +- **Tauri Capabilities**:ai-detached 窗口权限配置 + +**代码变更**:20+ files,新增 ~2000 行 + +**遗留问题**: +1. ~~空白屏问题:Vite 启动正常(122s)、HTML 200,但 webview 显示空白~~ ✅ Sprint 7 已修复(index.html 主题防闪烁脚本 + 启动计时埋点 __APP_T0 + body 背景色) +2. AI Node 未实现 — 目前只有骨架,需接入 df-ai 的 LlmProvider(任务 #44) +3. AI Chat LLM 调用逻辑待优化:长对话可靠性、大任务执行稳定性、token 消耗控制(任务 #43)— **P0 可靠性已在 Sprint 6 完成,P1 token 控制待办** + +**下一步**: +1. 排查并修复空白屏问题 +2. 实际验证 AI Chat 端到端(选 Provider → 发消息 → 工具调用 → 审批) +3. HumanNode + Settings 页面 +4. AI Chat LLM 调用逻辑优化(任务 #43) +5. AI Node 实现(任务 #44) + +### [Sprint 6] 2026-06-11 — AI Chat 调用可靠性 P0 + 前端类型修复 + +**工作内容**: + +**A. AI Chat 调用可靠性 P0(df-ai + src-tauri,跨前后端 6 文件)** — 三路代理核对(调用链/上下文-token/前端)后定位可靠性死锁类问题,本轮按"先保命"只做 P0 五项(P1 token 控制设计已定,留待下轮): +- **P0-1 连接超时**:`openai_compat.rs` Client 加 `connect_timeout(30s)` 防连接阶段无限 hang;不设总 timeout(避免 reqwest 总时长限制误砍流式长生成任务) +- **P0-2 流式 idle timeout**:`stream_llm` 用 `tokio::time::timeout(120s)` 包 `stream.next()`,防"连上后中途静默"无限 hang +- **P0-3 断连检测丢弃残缺**:维护 `finished_received` 标志,流尽未收到 finished 信号 → emit AiError + **丢弃残缺响应不当完整入库**(防脏历史污染) +- **P0-4 停止生成**:`AiSession.stop_flag: Arc` + 新 `ai_chat_stop` command + 前端停止按钮(streaming 时切换为珊瑚红方块按钮);run_agentic_loop/stream_llm 多检查点响应,停止后保留已生成文本 +- **P0-5 切对话防护**:`ai_conversation_switch` 生成中拒绝切换(防 active_conversation_id 被改导致旧 loop 的 save_conversation 串台写库) + +**B. 前端既有类型错误修复(14 处,让 `bun run build` 通过)**: +- `types.ts`:删重复的 `WorkflowEventPayload`(declaration merging 冲突) +- `stores/project.ts`:补 `invoke` import + `payload.event.data` 类型断言 + **return 包 `reactive()` 自动解包 computed**(一处解决 Dashboard/ProjectDetail 的 store.stats/pendingApproval 解包,消费端零改动) +- `Ideas/Tasks/ProjectDetail`:去掉 `parseInt(number)` 误用 → `new Date(timestamp)` +- `ProjectDetail.vue`:删 unused `nextTick` import +- `AiChat.vue`:删死代码 `TOOL_STATUS_MAP` + `toolStatusLabel` + +**C. AI Chat 代码审查修复(9 处,ai.rs + openai_compat.rs)** — 对 P0 代码做 /review 审查后修复,cargo check ✓: +- 🔴① `finish_reason="length"`(max_tokens 截断)纳入 finished — 修「大任务输出撞 4096 上限被误判断连、丢弃整段响应」(与 ④ 配合是本轮关键) +- 🔴② 路径校验最小加固:正斜杠→反斜杠规范化 + 拒 `..` 遍历 + 扩 `.aws`/`.gnupg`(根治 workspace 白名单 + canonicalize 待定边界后再做) +- 🔴③ tool_calls 按 index 排序(assistant 消息 line506 + tool_result line709 两处),消 HashMap 迭代乱序 +- 🔴④ max_tokens 4096→8192,贴合大任务 +- 🟡⑤ `ai_chat_stop` 在审批等待态清 pending_approvals + 复位 generating(原 stop_flag 无人读取,停止按钮表面无反应、会话卡 generating=true) +- ⚪⑨ 4 个 i18n 小函数(language_instruction/capability_section/behavior_rules/context_label)合并为 `system_prompt_parts` +- ⚪⑩ `extract_title` 单次 chars 遍历 +- ⚪⑪ reqwest builder 降级加 `warn!` + +**代码变更**:df-ai/openai_compat.rs、src-tauri/{commands/ai.rs, lib.rs}、前端 7 文件(+ C 轮 review:ai.rs、openai_compat.rs) + +**验证**:cargo check -p devflow ✓(仅既有 warning)、vue-tsc --noEmit ✓、bun run build ✓(built in 2.47s) + +**遗留问题 / 下一步**: +1. **P1 token 控制(任务 #43 续)**:流式 usage 采集(`stream_options:{include_usage:true}` + SSE usage 字段)+ token 预算自适应窗口(chars≈/3.5 启发式估算)+ 历史摘要压缩(早期摘要 + 近期全文)—— 设计已定,未实施。**⚠️ usage 采集 2026-06-11 用户叫停暂缓(结构层写一半已回退),P1 从 token 估算/裁剪/摘要续** +2. **实测验证**:`cargo tauri dev` 跑起来验证停止按钮/超时/断连真实生效(本轮仅编译 + 类型验证) +3. 停止生成边界:LLM 正常输出时停止立即生效(<1s);idle(无输出)时最多等 120s idle timeout —— P2 可用 `tokio::sync::Notify` 优化为绝对即时 +4. ~~Sprint 5 遗留的空白屏问题仍待排查~~ ✅ Sprint 7 已修复(用户实测确认) + +### [Sprint 7] 2026-06-11 — 空白屏修复 + i18n 接入 + 剩余页面接 Store + +**工作内容**: +- **空白屏修复**(前端):`index.html` 加主题防闪烁脚本(localStorage `df-theme` 渲染前置 `data-theme`)+ 启动计时埋点 `window.__APP_T0` + body 背景 `#0c0e1a`;`main.ts` 加 `.use(i18n)` 并打印 mount 耗时日志。用户实测确认空白屏消失 +- **i18n 接入**:`main.ts` 注册 `i18n` 插件;`i18n/index.ts` 完整(legacy:false / globalInjection / zh-CN 默认 + en fallback / localStorage `df-language` 持久化);App.vue 导航与各 view 改 `$t()` +- **剩余 3 页面接 Store**:`Knowledge.vue`/`Settings.vue`/`Decisions` 去硬编码 `ref([...])`,对接新建 `stores/{knowledge,settings,ai}.ts` +- **新路由**:`/` redirect → `/ai-home`,新增 `AiHome.vue`(默认落地页)/`AiDetached.vue`(分离窗口);路由 9→11 条 +- **df-nodes 实现度修正**:核对发现 `script_node.rs` 早为真实实现(接 df_execute Shell + 非零退出码判断),`human_node.rs` 半实现(发 `HumanApprovalRequest` 事件 + 超时循环,但审批响应 TODO 直接返回"同意")—— 非文档原记"全骨架" + +**遗留问题 / 下一步**: +1. **P1 token 控制(任务 #43 续)**:仍待办(usage 采集用户叫停,从 token 估算/裁剪/摘要续) +2. **条件表达式引擎**(conditions.rs 仍 true/false 字面量)— P2 未动 +3. ~~AI Node 实现(任务 #44)~~ ✅ 本轮已实现:`ai_node.rs` 接 df-ai `OpenAICompatProvider`,从 `ctx.config` 读 base_url/api_key/model/prompt/system_prompt/temperature/max_tokens 调 `complete()`,prompt 支持上游输入优先;`df-nodes/Cargo.toml` 加 df-ai 依赖;`state.rs` build_registry 注册 "ai"。cargo check ✓(未端到端实测真实 LLM 调用) +4. **df-stages** 11 个 execute() 全空骨架 — Phase 4 +5. **HumanNode 审批响应闭环**:目前直接返回"同意",需前端审批弹窗回写结果 + +### [Sprint 8] 2026-06-12 — Anthropic 协议 + 切对话不中断 + 技能联想需求调研 + +**工作内容**: + +**A. aichat 升级 A 路线(UX 快赢,已实现并验证)** — 详见 memory `aichat-roadmap-ab-split`: +- **Anthropic 协议 Provider**:新增 `crates/df-ai/src/anthropic_compat.rs`,实现 `LlmProvider` trait 覆盖 Anthropic Messages API(`x-api-key` + `anthropic-version`、顶层 `system`、必填 `max_tokens`、SSE event 类型解析、`content_block` tool_use)。按 `provider_type` 路由分发(`run_agentic_loop` / `ai_node`)。支持 GLM 订阅端点 `https://open.bigmodel.cn/api/anthropic` — **用户实测对话流式 OK** +- **切对话不打断生成**:后端 `AiChatEvent` 全变体加 `conversation_id`,agentic loop spawn 前快照 conv_id,所有 emit 填充;`ai_conversation_switch` 生成中只读返回;前端 `stores/ai.ts` 按 conversation_id 路由(后台对话事件不污染当前视图,完成/错误时刷新侧边栏) +- **审查项落地**:删全屏 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. 技能联想功能需求(本轮重点,未实现,需求阶段)**: +- **需求**: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 等) + 2. Claude commands:`~/.claude/commands/*.md`(1 个:idea) + 3. Claude plugins:`~/.claude/plugins/marketplaces/**/skills/*/SKILL.md`(caveman 全家、官方 frontend-design / skill-creator / mcp-server-dev、wuming prompt-optimizer;`cache/` 与 `marketplaces/` 重复需按 path 去重) + - 加项目级 `.claude/skills` + `.claude/commands`(DevFlow 暂无) +- **实现要点**:① 后端新 IPC `list_skills()` 扫 Claude 3 类来源解析 frontmatter + 按 path 去重,返回 `[{name, description, argument_hint, triggers, source}]`;② 前端输入框 `/` 触发联想浮层(名称 + description);③ `/name` 发送 → 后端读 SKILL.md 注入 context;④ 跨平台 home 路径(dirs crate) + +**C. 多工具 skill 体系调研结论(仅记录,后续按需扩展)**: +| 工具 | 本机状态 | skill 机制 | 是否纳入 | +|------|---------|-----------|---------| +| Claude Code | ✅ 已装 | SKILL.md(skills + commands + plugins 三类) | ✅ 首批数据源 | +| Codex (OpenAI) | ✅ 已装 | SKILL.md(`~/.codex/vendor_imports/skills/skills/.curated/*/SKILL.md`,39 个:figma / notion / playwright / security / vercel·netlify·render-deploy / latex 等;frontmatter 与 Claude 一致,可统一解析) | ⏳ 后续可扩展 | +| openclaw | ✅ 已装 | 多 agent gateway(`~/.openclaw/openclaw.json` 注册 8 agent 绑各 workspace + glm-5,经飞书 channel binding),非 SKILL.md;`agents/` 实体目录暂空 | ❌ 属 agent 选择层,非 skill 联想,暂不纳入 | +| opencode | ❌ 未装 | AGENTS.md + custom agents + slash commands(机制待确认) | ⏳ 待装后调研 | + +**代码变更**: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、前端联想浮层) +2. **A 路线剩余场景实测**:切对话不中断(场景 2/3)、错误友好化(场景 4)待用户实测 +3. **B 路线(规划式能力)**:coordinator / conditions 空壳,单独立项 + + + +--- + +## 七、开发约定 + +### 构建命令 + +```bash +# Rust 检查 +cd E:/wk-lab/devflow && cargo check + +# 前端开发 +cd E:/wk-lab/devflow && bun run dev + +# Tauri 开发(必须用这个,不能 cargo build) +cd E:/wk-lab/devflow && bun run tauri dev + +# Tauri 构建 +cd E:/wk-lab/devflow && bun run tauri build +``` + +### 关键设计决策 + +1. **引擎不绑定业务** — Workflow Engine 只做 DAG 执行,阶段是插件 +2. **无 panic** — 所有占位代码返回空/默认值,不使用 `todo!`/`unimplemented!` +3. **本地优先** — SQLite 嵌入,不依赖云服务 +4. **想法第一公民** — 想法池独立于项目 +5. **多任务/分支并行** — 同一项目多任务各绑 Git 分支 + +### 复用资源 + +| 资源 | 路径 | 复用点 | +|------|------|--------| +| u-desk-rust Tauri 经验 | `E:/wk-lab/u-desk-rust/` | Workspace + crate 拆分 + Tauri v2 | +| Rust proxy 工具链 | `E:/wk-oth/rust-work/` | 连接管理,内嵌调用 | +| Tauri 开发铁律 | Memory: `feedback_tauri_dev_rules` | 必须用 `cargo tauri dev/build` | diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..2ed74c9 --- /dev/null +++ b/bun.lock @@ -0,0 +1,301 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "devflow", + "dependencies": { + "@arco-design/web-vue": "^2.58.0", + "@tauri-apps/api": "^2", + "dompurify": "^3.4.9", + "marked": "^18.0.5", + "vue": "^3.5.13", + "vue-i18n": "9", + "vue-router": "4", + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/dompurify": "^3.2.0", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "~5.6.3", + "vite": "^6.0.7", + "vue-tsc": "^2.2.0", + }, + }, + }, + "packages": { + "@arco-design/color": ["@arco-design/color@0.4.0", "https://registry.npmmirror.com/@arco-design/color/-/color-0.4.0.tgz", { "dependencies": { "color": "^3.1.3" } }, "sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g=="], + + "@arco-design/web-vue": ["@arco-design/web-vue@2.58.0", "https://registry.npmmirror.com/@arco-design/web-vue/-/web-vue-2.58.0.tgz", { "dependencies": { "@arco-design/color": "^0.4.0", "b-tween": "^0.3.3", "b-validate": "^1.5.3", "compute-scroll-into-view": "^1.0.20", "dayjs": "^1.11.13", "number-precision": "^1.6.0", "resize-observer-polyfill": "^1.5.1", "scroll-into-view-if-needed": "^2.2.31", "vue": "^3.1.0" } }, "sha512-b1vdPYOmjG5VAkVa7jlVwCb+WynBK+rnKN8zH3yKohpZObZbostRd3HgYNtjjZjGVU3OqR0Yy2FX7ftgF0bcOw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/types": ["@babel/types@7.29.7", "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@intlify/core-base": ["@intlify/core-base@9.14.5", "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.14.5.tgz", { "dependencies": { "@intlify/message-compiler": "9.14.5", "@intlify/shared": "9.14.5" } }, "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA=="], + + "@intlify/message-compiler": ["@intlify/message-compiler@9.14.5", "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.14.5.tgz", { "dependencies": { "@intlify/shared": "9.14.5", "source-map-js": "^1.0.2" } }, "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ=="], + + "@intlify/shared": ["@intlify/shared@9.14.5", "https://registry.npmmirror.com/@intlify/shared/-/shared-9.14.5.tgz", {}, "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="], + + "@tauri-apps/api": ["@tauri-apps/api@2.11.0", "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.11.0.tgz", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="], + + "@tauri-apps/cli": ["@tauri-apps/cli@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-2.11.2.tgz", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="], + + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w=="], + + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg=="], + + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA=="], + + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw=="], + + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw=="], + + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ=="], + + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw=="], + + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw=="], + + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA=="], + + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA=="], + + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="], + + "@types/dompurify": ["@types/dompurify@3.2.0", "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.2.0.tgz", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="], + + "@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="], + + "@volar/language-core": ["@volar/language-core@2.4.15", "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", { "dependencies": { "@volar/source-map": "2.4.15" } }, "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA=="], + + "@volar/source-map": ["@volar/source-map@2.4.15", "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", {}, "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg=="], + + "@volar/typescript": ["@volar/typescript@2.4.15", "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.35", "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.35.tgz", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.35", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.35", "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", { "dependencies": { "@vue/compiler-core": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.35", "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/compiler-core": "3.5.35", "@vue/compiler-dom": "3.5.35", "@vue/compiler-ssr": "3.5.35", "@vue/shared": "3.5.35", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.35", "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", { "dependencies": { "@vue/compiler-dom": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw=="], + + "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], + + "@vue/devtools-api": ["@vue/devtools-api@6.6.4", "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], + + "@vue/language-core": ["@vue/language-core@2.2.12", "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", { "dependencies": { "@volar/language-core": "2.4.15", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA=="], + + "@vue/reactivity": ["@vue/reactivity@3.5.35", "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.35.tgz", { "dependencies": { "@vue/shared": "3.5.35" } }, "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.5.35", "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.35.tgz", { "dependencies": { "@vue/reactivity": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.35", "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", { "dependencies": { "@vue/reactivity": "3.5.35", "@vue/runtime-core": "3.5.35", "@vue/shared": "3.5.35", "csstype": "^3.2.3" } }, "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.5.35", "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.35.tgz", { "dependencies": { "@vue/compiler-ssr": "3.5.35", "@vue/shared": "3.5.35" }, "peerDependencies": { "vue": "3.5.35" } }, "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA=="], + + "@vue/shared": ["@vue/shared@3.5.35", "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.35.tgz", {}, "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA=="], + + "alien-signals": ["alien-signals@1.0.13", "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="], + + "b-tween": ["b-tween@0.3.3", "https://registry.npmmirror.com/b-tween/-/b-tween-0.3.3.tgz", {}, "sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA=="], + + "b-validate": ["b-validate@1.5.3", "https://registry.npmmirror.com/b-validate/-/b-validate-1.5.3.tgz", {}, "sha512-iCvCkGFskbaYtfQ0a3GmcQCHl/Sv1GufXFGuUQ+FE+WJa7A/espLOuFIn09B944V8/ImPj71T4+rTASxO2PAuA=="], + + "balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@2.1.1", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + + "color": ["color@3.2.1", "https://registry.npmmirror.com/color/-/color-3.2.1.tgz", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "compute-scroll-into-view": ["compute-scroll-into-view@1.0.20", "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", {}, "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="], + + "csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "dayjs": ["dayjs@1.11.21", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], + + "de-indent": ["de-indent@1.0.2", "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], + + "dompurify": ["dompurify@3.4.9", "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.9.tgz", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ=="], + + "entities": ["entities@7.0.1", "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "esbuild": ["esbuild@0.25.12", "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "estree-walker": ["estree-walker@2.0.2", "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "he": ["he@1.2.0", "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "is-arrayish": ["is-arrayish@0.3.4", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "marked": ["marked@18.0.5", "https://registry.npmmirror.com/marked/-/marked-18.0.5.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="], + + "minimatch": ["minimatch@9.0.9", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "muggle-string": ["muggle-string@0.4.1", "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + + "nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "number-precision": ["number-precision@1.6.0", "https://registry.npmmirror.com/number-precision/-/number-precision-1.6.0.tgz", {}, "sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ=="], + + "path-browserify": ["path-browserify@1.0.1", "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postcss": ["postcss@8.5.15", "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], + + "rollup": ["rollup@4.61.1", "https://registry.npmmirror.com/rollup/-/rollup-4.61.1.tgz", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="], + + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@2.2.31", "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", { "dependencies": { "compute-scroll-into-view": "^1.0.20" } }, "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA=="], + + "simple-swizzle": ["simple-swizzle@0.2.4", "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + + "source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "typescript": ["typescript@5.6.3", "https://registry.npmmirror.com/typescript/-/typescript-5.6.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + + "vite": ["vite@6.4.3", "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], + + "vscode-uri": ["vscode-uri@3.1.0", "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "vue": ["vue@3.5.35", "https://registry.npmmirror.com/vue/-/vue-3.5.35.tgz", { "dependencies": { "@vue/compiler-dom": "3.5.35", "@vue/compiler-sfc": "3.5.35", "@vue/runtime-dom": "3.5.35", "@vue/server-renderer": "3.5.35", "@vue/shared": "3.5.35" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q=="], + + "vue-i18n": ["vue-i18n@9.14.5", "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.5.tgz", { "dependencies": { "@intlify/core-base": "9.14.5", "@intlify/shared": "9.14.5", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g=="], + + "vue-router": ["vue-router@4.6.4", "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="], + + "vue-tsc": ["vue-tsc@2.2.12", "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="], + } +} diff --git a/crates/df-ai/Cargo.toml b/crates/df-ai/Cargo.toml new file mode 100644 index 0000000..d02b327 --- /dev/null +++ b/crates/df-ai/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "df-ai" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["sync"] } +async-trait = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } + +# HTTP + 流式 +reqwest = { version = "0.12", features = ["stream", "json"] } +futures = "0.3" +eventsource-stream = "0.2" diff --git a/crates/df-ai/src/ai_tools.rs b/crates/df-ai/src/ai_tools.rs new file mode 100644 index 0000000..4447e9d --- /dev/null +++ b/crates/df-ai/src/ai_tools.rs @@ -0,0 +1,166 @@ +//! AI 工具注册 — 将现有 CRUD 操作注册为 LLM 可调用的 Tool +//! +//! 风险分级: +//! - Low: 只读操作(list/get),AI 自动执行 +//! - Medium: 创建操作(create),显示意图,可配置自动批准 +//! - High: 破坏性操作(delete / run_workflow),必须人工批准 + +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::provider::ToolDefinition; + +// ============================================================ +// 风险级别 +// ============================================================ + +/// 工具调用风险级别 +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum RiskLevel { + /// 只读操作 — AI 自动执行 + Low, + /// 创建操作 — 可配置自动批准 + Medium, + /// 破坏性操作 — 必须人工批准 + High, +} + +// ============================================================ +// 工具定义 +// ============================================================ + +/// 已注册的 AI 工具 +pub struct AiTool { + /// 工具定义(发送给 LLM 的 JSON Schema) + pub definition: ToolDefinition, + /// 风险级别 + pub risk_level: RiskLevel, + /// 执行处理器 + pub handler: AiToolHandler, +} + +/// 工具处理器类型 — 异步函数,接收 JSON 参数,返回 JSON 结果 +pub type AiToolHandler = + Box Pin> + Send>> + Send + Sync>; + +// ============================================================ +// 工具注册表 +// ============================================================ + +/// AI 工具注册表 +pub struct AiToolRegistry { + tools: HashMap, +} + +impl AiToolRegistry { + /// 创建空注册表 + pub fn new() -> Self { + Self { + tools: HashMap::new(), + } + } + + /// 注册一个工具 + pub fn register( + &mut self, + name: impl Into, + description: impl Into, + parameters: Value, + risk_level: RiskLevel, + handler: AiToolHandler, + ) { + let name_str = name.into(); + let definition = ToolDefinition::function(&name_str, description, parameters); + self.tools.insert( + name_str, + AiTool { + definition, + risk_level, + handler, + }, + ); + } + + /// 获取所有工具定义(发送给 LLM 的 tools 参数) + pub fn tool_definitions(&self) -> Vec { + self.tools.values().map(|t| t.definition.clone()).collect() + } + + /// 根据名称获取工具 + pub fn get(&self, name: &str) -> Option<&AiTool> { + self.tools.get(name) + } + + /// 获取所有已注册工具名称 + pub fn tool_names(&self) -> Vec { + self.tools.keys().cloned().collect() + } + + /// 已注册工具数量 + pub fn len(&self) -> usize { + self.tools.len() + } + + /// 是否为空 + pub fn is_empty(&self) -> bool { + self.tools.is_empty() + } +} + +impl Default for AiToolRegistry { + fn default() -> Self { + Self::new() + } +} + +// ============================================================ +// 工具执行结果 +// ============================================================ + +/// 工具执行结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolExecutionResult { + /// 工具调用 ID + pub tool_call_id: String, + /// 工具名称 + pub tool_name: String, + /// 执行参数 + pub arguments: Value, + /// 执行结果 + pub result: Value, + /// 是否需要人工批准 + pub approval_required: bool, + /// 风险级别 + pub risk_level: RiskLevel, +} + +// ============================================================ +// 辅助: 构建 JSON Schema 参数 +// ============================================================ + +/// 构建一个简单的 object JSON Schema +pub fn object_schema(properties: Vec<(&str, &str, bool)>) -> Value { + let mut props = serde_json::Map::new(); + let mut required = Vec::new(); + + for (name, type_str, is_required) in properties { + props.insert( + name.to_string(), + serde_json::json!({ "type": type_str, "description": "" }), + ); + if is_required { + required.push(name.to_string()); + } + } + + serde_json::json!({ + "type": "object", + "properties": props, + "required": required, + }) +} diff --git a/crates/df-ai/src/anthropic_compat.rs b/crates/df-ai/src/anthropic_compat.rs new file mode 100644 index 0000000..31a4af2 --- /dev/null +++ b/crates/df-ai/src/anthropic_compat.rs @@ -0,0 +1,426 @@ +//! Anthropic 兼容 Provider — 通过 /v1/messages 端点实现 +//! +//! 覆盖: Claude 官方 / GLM 订阅端点 (open.bigmodel.cn/api/anthropic) / 任意 Messages API 网关 +//! 支持: 同步调用 + SSE 流式 + Tool Use +//! +//! 与 OpenAI 协议的关键差异由本模块内部完成转换,对外仍暴露统一的 LlmProvider trait, +//! 上层 (Agentic Loop / AiNode) 无需感知协议。 + +use async_trait::async_trait; +use eventsource_stream::Eventsource; +use futures::{Stream, StreamExt}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tracing::{debug, error, warn}; + +use crate::provider::{ + CompletionRequest, CompletionResponse, LlmProvider, MessageRole, ProviderFeatures, + StreamChunk, StreamResult, TokenUsage, ToolCall, ToolCallDelta, +}; + +// ============================================================ +// Anthropic API 请求/响应结构体 +// ============================================================ + +/// Anthropic 请求体 +#[derive(Debug, Serialize)] +struct AnthropicRequest { + model: String, + messages: Vec, + max_tokens: u32, + #[serde(skip_serializing_if = "Option::is_none")] + system: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, +} + +/// Anthropic 工具定义(input_schema 对应 OpenAI 的 parameters) +#[derive(Debug, Serialize)] +struct AnthropicToolDef { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + input_schema: serde_json::Value, +} + +/// Anthropic 同步响应 +#[derive(Debug, Deserialize)] +struct AnthropicResponse { + #[allow(dead_code)] + id: String, + model: String, + content: Vec, + #[allow(dead_code)] + stop_reason: Option, + usage: AnthropicUsage, +} + +/// 响应 content 块(text 或 tool_use) +#[derive(Debug, Deserialize)] +struct AnthropicContentBlock { + #[serde(rename = "type")] + block_type: String, + #[serde(default)] + text: Option, + /// tool_use 块字段 + id: Option, + name: Option, + input: Option, +} + +#[derive(Debug, Deserialize)] +struct AnthropicUsage { + input_tokens: u32, + output_tokens: u32, +} + +// ============================================================ +// Provider 实现 +// ============================================================ + +/// Anthropic 兼容 LLM Provider +pub struct AnthropicCompatProvider { + client: Client, + api_key: String, + base_url: String, + default_model: String, +} + +/// Anthropic 流式协议版本头 +const ANTHROPIC_VERSION: &str = "2023-06-01"; +/// Anthropic max_tokens 必填,缺省时的兜底值 +const DEFAULT_MAX_TOKENS: u32 = 4096; + +impl AnthropicCompatProvider { + /// 创建 Provider + /// + /// - `base_url`: 如 `https://api.anthropic.com`、`https://open.bigmodel.cn/api/anthropic` + /// - `api_key`: API 密钥(Anthropic 用 x-api-key 头,非 Bearer) + /// - `default_model`: 默认模型名称 + pub fn new(base_url: impl Into, api_key: impl Into, default_model: impl Into) -> Self { + let client = Client::builder() + .connect_timeout(std::time::Duration::from_secs(30)) + .build() + .unwrap_or_else(|e| { + warn!("reqwest builder 失败,降级为默认 client: {}", e); + Client::new() + }); + Self { + client, + api_key: api_key.into(), + base_url: base_url.into(), + default_model: default_model.into(), + } + } + + /// 构建 messages 端点 URL + /// + /// - 已含 `/v1/messages` → 直接用 + /// - 以 `/v1` 结尾 → 补 `/messages` + /// - 否则(如 `.../api/anthropic`、`api.anthropic.com`)→ 补 `/v1/messages` + fn messages_url(&self) -> String { + let base = self.base_url.trim_end_matches('/'); + if base.ends_with("/v1/messages") { + return base.to_string(); + } + if base.ends_with("/v1") { + return format!("{}/messages", base); + } + format!("{}/v1/messages", base) + } + + /// 将统一 CompletionRequest 转换为 Anthropic 请求体 + /// + /// 转换要点: + /// - system 消息从 messages 抽离到顶层 system 字段 + /// - assistant 带 tool_calls → content 数组含 text + tool_use 块 + /// - tool_result(role=Tool)→ user 消息含 tool_result 块;连续多个合并为一条 user + /// - tool_definitions 的 parameters → input_schema + fn convert_request(&self, req: CompletionRequest) -> AnthropicRequest { + let model = if req.model.is_empty() { + self.default_model.clone() + } else { + req.model + }; + + // 抽离 system 消息 + let system: Option = { + let sys: Vec = req + .messages + .iter() + .filter(|m| matches!(m.role, MessageRole::System)) + .map(|m| m.content.clone()) + .collect(); + if sys.is_empty() { + None + } else { + Some(sys.join("\n\n")) + } + }; + + // 构建非 system 消息(保留顺序,合并连续 tool_result) + let mut messages: Vec = Vec::new(); + let mut pending_tool_results: Vec = Vec::new(); + + for m in req.messages.iter() { + match m.role { + MessageRole::System => continue, + MessageRole::Tool => { + // 累积 tool_result 块,遇到非 Tool 消息时 flush + pending_tool_results.push(serde_json::json!({ + "type": "tool_result", + "tool_use_id": m.tool_call_id, + "content": m.content, + })); + } + MessageRole::User => { + Self::flush_tool_results(&mut messages, &mut pending_tool_results); + messages.push(serde_json::json!({ "role": "user", "content": m.content })); + } + MessageRole::Assistant => { + Self::flush_tool_results(&mut messages, &mut pending_tool_results); + let mut content: Vec = Vec::new(); + if !m.content.is_empty() { + content.push(serde_json::json!({ "type": "text", "text": m.content })); + } + if let Some(calls) = &m.tool_calls { + for tc in calls { + let input: serde_json::Value = + serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::Value::Null); + content.push(serde_json::json!({ + "type": "tool_use", + "id": tc.id, + "name": tc.function.name, + "input": input, + })); + } + } + if content.is_empty() { + content.push(serde_json::json!({ "type": "text", "text": "" })); + } + messages.push(serde_json::json!({ "role": "assistant", "content": content })); + } + } + } + Self::flush_tool_results(&mut messages, &mut pending_tool_results); + + let tools = req.tools.map(|defs| { + defs.into_iter() + .map(|d| AnthropicToolDef { + name: d.function.name, + description: Some(d.function.description).filter(|s| !s.is_empty()), + input_schema: d.function.parameters, + }) + .collect() + }); + + AnthropicRequest { + model, + messages, + max_tokens: req.max_tokens.unwrap_or(DEFAULT_MAX_TOKENS), + system, + temperature: req.temperature, + stream: req.stream, + tools, + tool_choice: req.tool_choice, + } + } + + /// 将累积的 tool_result 块作为一条 user 消息 flush 进消息列表 + fn flush_tool_results( + messages: &mut Vec, + pending: &mut Vec, + ) { + if pending.is_empty() { + return; + } + let blocks: Vec = pending.drain(..).collect(); + messages.push(serde_json::json!({ "role": "user", "content": blocks })); + } + + /// 统一鉴权头:x-api-key + anthropic-version + fn auth_headers(&self, rb: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + rb.header("x-api-key", &self.api_key) + .header("anthropic-version", ANTHROPIC_VERSION) + .header("Content-Type", "application/json") + } +} + +#[async_trait] +impl LlmProvider for AnthropicCompatProvider { + async fn complete(&self, request: CompletionRequest) -> anyhow::Result { + let mut req = request; + req.stream = false; + let body = self.convert_request(req); + + debug!(model = %body.model, "Anthropic 同步调用"); + + let resp = self + .auth_headers(self.client.post(self.messages_url())) + .json(&body) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + error!(%status, %text, "Anthropic API 调用失败"); + anyhow::bail!("Anthropic API 错误 {}: {}", status, text); + } + + let resp: AnthropicResponse = resp.json().await?; + + // content 块中拼接 text,收集 tool_use + let mut text = String::new(); + let mut tool_calls: Vec = Vec::new(); + for block in resp.content { + match block.block_type.as_str() { + "text" => { + if let Some(t) = block.text { + text.push_str(&t); + } + } + "tool_use" => { + let id = block.id.unwrap_or_default(); + let name = block.name.unwrap_or_default(); + let args = block + .input + .map(|v| serde_json::to_string(&v).unwrap_or_default()) + .unwrap_or_default(); + tool_calls.push(ToolCall::new(id, name, args)); + } + other => warn!(block_type = other, "Anthropic 响应含未知 content 块类型,已忽略"), + } + } + + let usage = TokenUsage { + prompt_tokens: resp.usage.input_tokens, + completion_tokens: resp.usage.output_tokens, + total_tokens: resp.usage.input_tokens + resp.usage.output_tokens, + }; + + Ok(CompletionResponse { + text, + model: resp.model, + usage, + tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) }, + }) + } + + async fn stream(&self, request: CompletionRequest) -> anyhow::Result { + let mut req = request; + req.stream = true; + let body = self.convert_request(req); + + debug!(model = %body.model, "Anthropic 流式调用"); + + let resp = self + .auth_headers(self.client.post(self.messages_url())) + .json(&body) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + error!(%status, %text, "Anthropic 流式 API 调用失败"); + anyhow::bail!("Anthropic 流式 API 错误 {}: {}", status, text); + } + + // 流式解析:eventsource 逐事件处理,按 type 字段分发转 StreamChunk + let stream = resp + .bytes_stream() + .eventsource() + .map(move |event| match event { + Ok(ev) => { + // 解析 data 中的 JSON,按 type 字段决定如何转 StreamChunk + let v: serde_json::Value = match serde_json::from_str(&ev.data) { + Ok(v) => v, + Err(_) => return Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None }), + }; + let ty = v.get("type").and_then(|t| t.as_str()).unwrap_or(""); + match ty { + // 文本增量 + "content_block_delta" => { + if let Some(delta) = v.get("delta") { + if delta.get("type").and_then(|t| t.as_str()) == Some("text_delta") { + let text = delta.get("text").and_then(|t| t.as_str()).unwrap_or("").to_string(); + return Ok(StreamChunk { delta: text, finished: false, tool_calls: None }); + } + // 工具入参增量 + if delta.get("type").and_then(|t| t.as_str()) == Some("input_json_delta") { + let partial = delta.get("partial_json").and_then(|t| t.as_str()).unwrap_or("").to_string(); + let idx = v.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as u32; + return Ok(StreamChunk { + delta: String::new(), + finished: false, + tool_calls: Some(vec![ToolCallDelta { + index: idx, + id: None, + function_name: None, + function_arguments: Some(partial), + }]), + }); + } + } + Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None }) + } + // 工具块开始:带 id + name + "content_block_start" => { + if let Some(cb) = v.get("content_block") { + if cb.get("type").and_then(|t| t.as_str()) == Some("tool_use") { + let idx = v.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as u32; + let id = cb.get("id").and_then(|t| t.as_str()).map(|s| s.to_string()); + let name = cb.get("name").and_then(|t| t.as_str()).map(|s| s.to_string()); + return Ok(StreamChunk { + delta: String::new(), + finished: false, + tool_calls: Some(vec![ToolCallDelta { + index: idx, + id, + function_name: name, + function_arguments: None, + }]), + }); + } + } + Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None }) + } + // 消息结束 + "message_stop" => Ok(StreamChunk { delta: String::new(), finished: true, tool_calls: None }), + // 错误事件 + "error" => { + let msg = v.get("error").and_then(|e| e.get("message")).and_then(|m| m.as_str()).unwrap_or("stream error"); + error!(%msg, "Anthropic 流式错误事件"); + Ok(StreamChunk { delta: String::new(), finished: true, tool_calls: None }) + } + // message_start / content_block_stop / message_delta 等不产出 chunk + _ => Ok(StreamChunk { delta: String::new(), finished: false, tool_calls: None }), + } + } + Err(e) => { + error!(error = %e, "Anthropic SSE 事件流错误"); + Err(anyhow::anyhow!("Anthropic SSE 错误: {}", e)) + } + }); + + Ok(Box::pin(stream)) + } + + fn name(&self) -> &str { + "anthropic-compat" + } + + fn supported_features(&self) -> ProviderFeatures { + ProviderFeatures { + streaming: true, + function_calling: true, + vision: false, + } + } +} diff --git a/crates/df-ai/src/context.rs b/crates/df-ai/src/context.rs new file mode 100644 index 0000000..a69a206 --- /dev/null +++ b/crates/df-ai/src/context.rs @@ -0,0 +1,59 @@ +//! 上下文管理器 — 管理对话上下文和 token 预算 + +use std::collections::VecDeque; + +use serde::{Deserialize, Serialize}; + +use crate::provider::ChatMessage; + +/// 上下文窗口配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextConfig { + /// 最大 token 数 + pub max_tokens: u32, + /// 保留的系统提示 token 数 + pub system_reserve: u32, +} + +impl Default for ContextConfig { + fn default() -> Self { + Self { + max_tokens: 128_000, + system_reserve: 4_000, + } + } +} + +/// 上下文管理器 +pub struct ContextManager { + /// 消息历史 + messages: VecDeque, + /// 配置 + config: ContextConfig, +} + +impl ContextManager { + /// 创建上下文管理器 + pub fn new(config: ContextConfig) -> Self { + Self { + messages: VecDeque::new(), + config, + } + } + + /// 添加消息 + pub fn push(&mut self, message: ChatMessage) { + self.messages.push_back(message); + // TODO: 当超过 token 预算时,淘汰旧消息 + } + + /// 获取当前消息列表 + pub fn messages(&self) -> &VecDeque { + &self.messages + } + + /// 清空上下文 + pub fn clear(&mut self) { + self.messages.clear(); + } +} diff --git a/crates/df-ai/src/coordinator.rs b/crates/df-ai/src/coordinator.rs new file mode 100644 index 0000000..57a2be6 --- /dev/null +++ b/crates/df-ai/src/coordinator.rs @@ -0,0 +1,28 @@ +//! Agent 协调器 — 管理多 Agent 协作 + +/// Agent 协调器 +/// +/// TODO: 实现多 Agent 协作逻辑 +pub struct AgentCoordinator; + +impl AgentCoordinator { + /// 创建协调器 + pub fn new() -> Self { + Self + } + + /// 启动 Agent 协作任务 + /// + /// TODO: 实现 Agent 间消息传递和任务分配 + pub async fn run(&self, _task: &str) -> anyhow::Result { + tracing::info!("AgentCoordinator: 协调任务开始"); + // TODO: 实现多 Agent 协作 + Ok("TODO: Agent 协作结果".to_string()) + } +} + +impl Default for AgentCoordinator { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/df-ai/src/lib.rs b/crates/df-ai/src/lib.rs new file mode 100644 index 0000000..d4eb343 --- /dev/null +++ b/crates/df-ai/src/lib.rs @@ -0,0 +1,10 @@ +//! df-ai: AI 编排 — LLM Provider、模型路由、Agent 协调、上下文管理、流式处理、工具注册 + +pub mod ai_tools; +pub mod anthropic_compat; +pub mod context; +pub mod coordinator; +pub mod openai_compat; +pub mod provider; +pub mod router; +pub mod stream; diff --git a/crates/df-ai/src/openai_compat.rs b/crates/df-ai/src/openai_compat.rs new file mode 100644 index 0000000..06c0b9d --- /dev/null +++ b/crates/df-ai/src/openai_compat.rs @@ -0,0 +1,410 @@ +//! OpenAI 兼容 Provider — 通过 /v1/chat/completions 端点实现 +//! +//! 覆盖: OpenAI / GLM (open.bigmodel.cn) / DeepSeek / Claude OpenAI 兼容模式 +//! 支持: 同步调用 + SSE 流式 + Function Calling / Tool Use + +use std::pin::Pin; + +use async_trait::async_trait; +use eventsource_stream::Eventsource; +use futures::{Stream, StreamExt}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tracing::{debug, error, info, warn}; + +use crate::provider::{ + CompletionRequest, CompletionResponse, LlmProvider, ProviderFeatures, StreamChunk, StreamResult, + TokenUsage, ToolCall, ToolCallDelta, +}; + +// ============================================================ +// OpenAI API 请求/响应结构体 +// ============================================================ + +/// OpenAI 兼容请求体 +#[derive(Debug, Serialize)] +struct OpenAiRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, +} + +/// OpenAI 消息格式 +#[derive(Debug, Serialize, Deserialize)] +struct OpenAiMessage { + role: String, + content: String, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, +} + +/// OpenAI 同步响应 +#[derive(Debug, Deserialize)] +struct OpenAiResponse { + choices: Vec, + model: String, + usage: Option, +} + +#[derive(Debug, Deserialize)] +struct OpenAiChoice { + message: OpenAiMessageResp, + finish_reason: Option, +} + +#[derive(Debug, Deserialize)] +struct OpenAiMessageResp { + content: Option, + tool_calls: Option>, +} + +#[derive(Debug, Deserialize)] +struct OpenAiToolCallResp { + id: String, + #[serde(rename = "type")] + call_type: String, + function: OpenAiFunctionResp, +} + +#[derive(Debug, Deserialize)] +struct OpenAiFunctionResp { + name: String, + arguments: String, +} + +#[derive(Debug, Deserialize)] +struct OpenAiUsage { + prompt_tokens: u32, + completion_tokens: u32, + total_tokens: u32, +} + +/// SSE 流式响应 chunk +#[derive(Debug, Deserialize)] +struct OpenAiStreamChunk { + choices: Vec, +} + +#[derive(Debug, Deserialize)] +struct OpenAiStreamChoice { + delta: OpenAiStreamDelta, + finish_reason: Option, +} + +#[derive(Debug, Deserialize)] +struct OpenAiStreamDelta { + content: Option, + tool_calls: Option>, +} + +#[derive(Debug, Deserialize)] +struct OpenAiStreamToolCall { + index: u32, + id: Option, + function: Option, +} + +#[derive(Debug, Deserialize)] +struct OpenAiStreamFunction { + name: Option, + arguments: Option, +} + +// ============================================================ +// OpenAI Compat Provider +// ============================================================ + +/// OpenAI 兼容 LLM Provider +pub struct OpenAICompatProvider { + client: Client, + api_key: String, + base_url: String, + default_model: String, +} + +impl OpenAICompatProvider { + /// 创建 Provider + /// + /// - `base_url`: 如 "https://api.openai.com", "https://open.bigmodel.cn/api/paas", "https://api.deepseek.com" + /// - `api_key`: API 密钥 + /// - `default_model`: 默认模型名称 + pub fn new(base_url: impl Into, api_key: impl Into, default_model: impl Into) -> Self { + // connect_timeout 防连接阶段无限 hang(网络静默断);不设总 timeout—— + // reqwest 的 .timeout() 会限制整个响应 body 时长,流式长生成任务会被误砍。 + // 读取阶段的中途静默由上层 stream_llm 的 idle timeout 兜底。 + let client = Client::builder() + .connect_timeout(std::time::Duration::from_secs(30)) + .build() + .unwrap_or_else(|e| { + warn!("reqwest builder 失败,降级为默认 client(无 connect_timeout): {}", e); + Client::new() + }); + Self { + client, + api_key: api_key.into(), + base_url: base_url.into(), + default_model: default_model.into(), + } + } + + /// 构建完整 API URL + /// + /// 智能拼接,兼容三种 base_url 约定: + /// - 已含完整端点(…/chat/completions)→ 直接用 + /// - 已含版本段(…/v1 …/v4 等,如 GLM 的 /api/paas/v4)→ 补 /chat/completions + /// - 仅域名无版本(如 api.openai.com / api.deepseek.com)→ 补 /v1/chat/completions(OpenAI 约定) + fn chat_url(&self) -> String { + let base = self.base_url.trim_end_matches('/'); + if base.ends_with("/chat/completions") { + return base.to_string(); + } + if Self::ends_with_version(base) { + return format!("{}/chat/completions", base); + } + format!("{}/v1/chat/completions", base) + } + + /// base_url 是否以 `/v<数字>` 结尾(如 /v1 /v4) + fn ends_with_version(base: &str) -> bool { + match base.rsplit_once('/') { + Some((_, last)) if last.starts_with('v') && last.len() > 1 => { + last[1..].bytes().all(|b| b.is_ascii_digit()) + } + _ => false, + } + } + + /// 将通用请求转换为 OpenAI 格式 + fn convert_request(&self, req: CompletionRequest) -> OpenAiRequest { + let model = if req.model.is_empty() { + self.default_model.clone() + } else { + req.model + }; + + let messages: Vec = req + .messages + .into_iter() + .map(|m| { + let role = match m.role { + crate::provider::MessageRole::System => "system", + crate::provider::MessageRole::User => "user", + crate::provider::MessageRole::Assistant => "assistant", + crate::provider::MessageRole::Tool => "tool", + }; + let tool_calls = m.tool_calls.map(|calls| { + calls + .into_iter() + .map(|tc| { + serde_json::json!({ + "id": tc.id, + "type": tc.call_type, + "function": { + "name": tc.function.name, + "arguments": tc.function.arguments, + } + }) + }) + .collect() + }); + OpenAiMessage { + role: role.to_string(), + content: m.content, + tool_call_id: m.tool_call_id, + tool_calls, + } + }) + .collect(); + + let tools = req.tools.map(|defs| { + defs.into_iter() + .map(|d| serde_json::to_value(d).unwrap_or_default()) + .collect() + }); + + OpenAiRequest { + model, + messages, + temperature: req.temperature, + max_tokens: req.max_tokens, + stream: req.stream, + tools, + tool_choice: req.tool_choice, + } + } + + /// 解析同步响应中的工具调用 + fn parse_tool_calls(calls: Vec) -> Vec { + calls + .into_iter() + .map(|c| ToolCall::new(c.id, c.function.name, c.function.arguments)) + .collect() + } +} + +#[async_trait] +impl LlmProvider for OpenAICompatProvider { + async fn complete(&self, request: CompletionRequest) -> anyhow::Result { + let mut req = request; + req.stream = false; + let openai_req = self.convert_request(req); + + debug!(model = %openai_req.model, "OpenAI 同步调用"); + + let resp = self + .client + .post(self.chat_url()) + .header("Authorization", format!("Bearer {}", self.api_key)) + .header("Content-Type", "application/json") + .json(&openai_req) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + error!(%status, %body, "LLM API 调用失败"); + anyhow::bail!("LLM API 错误 {}: {}", status, body); + } + + let body: OpenAiResponse = resp.json().await?; + let choice = body + .choices + .into_iter() + .next() + .ok_or_else(|| anyhow::anyhow!("LLM 响应无 choices"))?; + + let text = choice.message.content.unwrap_or_default(); + let tool_calls = choice.message.tool_calls.map(Self::parse_tool_calls); + + let usage = body.usage.map(|u| TokenUsage { + prompt_tokens: u.prompt_tokens, + completion_tokens: u.completion_tokens, + total_tokens: u.total_tokens, + }).unwrap_or(TokenUsage { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }); + + Ok(CompletionResponse { + text, + model: body.model, + usage, + tool_calls, + }) + } + + async fn stream(&self, request: CompletionRequest) -> anyhow::Result { + let mut req = request; + req.stream = true; + let openai_req = self.convert_request(req); + + debug!(model = %openai_req.model, "OpenAI 流式调用"); + + let resp = self + .client + .post(self.chat_url()) + .header("Authorization", format!("Bearer {}", self.api_key)) + .header("Content-Type", "application/json") + .json(&openai_req) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + error!(%status, %body, "LLM 流式 API 调用失败"); + anyhow::bail!("LLM 流式 API 错误 {}: {}", status, body); + } + + let stream = resp + .bytes_stream() + .eventsource() + .map(move |event| { + match event { + Ok(event) => { + // OpenAI 发送 "data: [DONE]" 表示流结束 + if event.data == "[DONE]" { + return Ok(StreamChunk { + delta: String::new(), + finished: true, + tool_calls: None, + }); + } + + match serde_json::from_str::(&event.data) { + Ok(chunk) => { + if let Some(choice) = chunk.choices.into_iter().next() { + let delta_text = choice.delta.content.unwrap_or_default(); + // "length" = max_tokens 截断,属正常终止(非断连),纳入 finished + let finished = choice.finish_reason.as_deref() == Some("stop") + || choice.finish_reason.as_deref() == Some("tool_calls") + || choice.finish_reason.as_deref() == Some("length"); + + let tool_calls = choice.delta.tool_calls.map(|tcs| { + tcs.into_iter() + .map(|tc| ToolCallDelta { + index: tc.index, + id: tc.id, + function_name: tc.function.as_ref().and_then(|f| f.name.clone()), + function_arguments: tc.function.and_then(|f| f.arguments), + }) + .collect() + }); + + Ok(StreamChunk { + delta: delta_text, + finished, + tool_calls, + }) + } else { + Ok(StreamChunk { + delta: String::new(), + finished: false, + tool_calls: None, + }) + } + } + Err(e) => { + debug!("SSE 数据解析失败: {} — data: {}", e, event.data); + Ok(StreamChunk { + delta: String::new(), + finished: false, + tool_calls: None, + }) + } + } + } + Err(e) => { + error!("SSE 流错误: {}", e); + Err(anyhow::anyhow!("SSE 流错误: {}", e)) + } + } + }); + + Ok(Box::pin(stream)) + } + + fn name(&self) -> &str { + &self.default_model + } + + fn supported_features(&self) -> ProviderFeatures { + ProviderFeatures { + streaming: true, + function_calling: true, + vision: false, + } + } +} diff --git a/crates/df-ai/src/provider.rs b/crates/df-ai/src/provider.rs new file mode 100644 index 0000000..875cb00 --- /dev/null +++ b/crates/df-ai/src/provider.rs @@ -0,0 +1,207 @@ +//! LLM Provider trait — 统一的 LLM 调用抽象 +//! +//! 支持 OpenAI 兼容 API(覆盖 OpenAI / GLM / DeepSeek / Claude 兼容模式), +//! 含 function calling / tool use 能力。 + +use std::pin::Pin; + +use async_trait::async_trait; +use futures::Stream; +use serde::{Deserialize, Serialize}; + +// ============================================================ +// 核心数据结构 +// ============================================================ + +/// LLM 调用请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CompletionRequest { + /// 模型名称 + pub model: String, + /// 提示消息列表 + pub messages: Vec, + /// 温度(0.0 ~ 2.0) + pub temperature: Option, + /// 最大生成 token 数 + pub max_tokens: Option, + /// 是否流式输出 + pub stream: bool, + /// 可调用的工具定义 + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, + /// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"} + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_choice: Option, +} + +/// 聊天消息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatMessage { + pub role: MessageRole, + pub content: String, + /// 工具调用 ID(role=Tool 时必填) + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + /// AI 发起的工具调用列表(role=Assistant 时可能有) + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, +} + +impl ChatMessage { + pub fn system(content: impl Into) -> Self { + Self { role: MessageRole::System, content: content.into(), tool_call_id: None, tool_calls: None } + } + pub fn user(content: impl Into) -> Self { + Self { role: MessageRole::User, content: content.into(), tool_call_id: None, tool_calls: None } + } + pub fn assistant(content: impl Into) -> Self { + Self { role: MessageRole::Assistant, content: content.into(), tool_call_id: None, tool_calls: None } + } + pub fn assistant_with_tools(content: impl Into, tool_calls: Vec) -> Self { + Self { role: MessageRole::Assistant, content: content.into(), tool_call_id: None, tool_calls: Some(tool_calls) } + } + pub fn tool_result(call_id: impl Into, content: impl Into) -> Self { + Self { role: MessageRole::Tool, content: content.into(), tool_call_id: Some(call_id.into()), tool_calls: None } + } +} + +/// 消息角色 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum MessageRole { + System, + User, + Assistant, + Tool, +} + +/// 工具定义 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolDefinition { + #[serde(rename = "type")] + pub tool_type: String, + pub function: ToolFunction, +} + +impl ToolDefinition { + pub fn function(name: impl Into, description: impl Into, parameters: serde_json::Value) -> Self { + Self { + tool_type: "function".into(), + function: ToolFunction { name: name.into(), description: description.into(), parameters }, + } + } +} + +/// 函数定义 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolFunction { + pub name: String, + pub description: String, + pub parameters: serde_json::Value, +} + +/// 工具调用(AI 发起) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolCall { + pub id: String, + #[serde(rename = "type")] + pub call_type: String, + pub function: ToolCallFunction, +} + +impl ToolCall { + pub fn new(id: impl Into, name: impl Into, arguments: impl Into) -> Self { + Self { + id: id.into(), + call_type: "function".into(), + function: ToolCallFunction { name: name.into(), arguments: arguments.into() }, + } + } +} + +/// 工具调用函数部分 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolCallFunction { + pub name: String, + pub arguments: String, +} + +/// LLM 调用响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CompletionResponse { + /// 生成的文本 + pub text: String, + /// 使用的模型 + pub model: String, + /// 消耗的 token 数 + pub usage: TokenUsage, + /// AI 发起的工具调用(如有) + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, +} + +/// Token 用量 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TokenUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + pub total_tokens: u32, +} + +/// Provider 支持的特性标志 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProviderFeatures { + pub streaming: bool, + pub function_calling: bool, + pub vision: bool, +} + +/// 流式输出的 chunk +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StreamChunk { + /// 增量文本 + pub delta: String, + /// 是否结束 + pub finished: bool, + /// 工具调用增量(如有) + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, +} + +/// 工具调用增量(流式中的片段) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolCallDelta { + /// 索引 + pub index: u32, + /// 工具调用 ID(仅第一个 chunk 有) + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// 函数名片段 + #[serde(skip_serializing_if = "Option::is_none")] + pub function_name: Option, + /// 函数参数片段 + #[serde(skip_serializing_if = "Option::is_none")] + pub function_arguments: Option, +} + +/// 异步流类型别名 +pub type StreamResult = Pin> + Send>>; + +/// LLM Provider trait +#[async_trait] +pub trait LlmProvider: Send + Sync { + /// 同步调用 + async fn complete(&self, request: CompletionRequest) -> anyhow::Result; + + /// 流式调用(返回异步流) + async fn stream( + &self, + request: CompletionRequest, + ) -> anyhow::Result; + + /// Provider 名称 + fn name(&self) -> &str; + + /// 支持的特性 + fn supported_features(&self) -> ProviderFeatures; +} diff --git a/crates/df-ai/src/router.rs b/crates/df-ai/src/router.rs new file mode 100644 index 0000000..c57ac04 --- /dev/null +++ b/crates/df-ai/src/router.rs @@ -0,0 +1,51 @@ +//! 模型路由 — 根据任务类型选择最优模型 + +use serde::{Deserialize, Serialize}; + +/// 任务类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TaskType { + /// 代码生成 + CodeGeneration, + /// 代码审查 + CodeReview, + /// 文档生成 + Documentation, + /// 分析推理 + Analysis, + /// 摘要总结 + Summarization, + /// 通用对话 + Chat, +} + +/// 模型路由器 +/// +/// 根据任务类型、成本、延迟等选择最优模型 +pub struct ModelRouter { + /// 默认模型 + default_model: String, +} + +impl ModelRouter { + /// 创建路由器 + pub fn new(default_model: &str) -> Self { + Self { + default_model: default_model.to_string(), + } + } + + /// 根据任务类型路由到合适的模型 + pub fn route(&self, task_type: &TaskType) -> String { + // TODO: 实现基于规则的模型路由 + match task_type { + TaskType::CodeGeneration => self.default_model.clone(), + TaskType::CodeReview => self.default_model.clone(), + TaskType::Documentation => self.default_model.clone(), + TaskType::Analysis => self.default_model.clone(), + TaskType::Summarization => self.default_model.clone(), + TaskType::Chat => self.default_model.clone(), + } + } +} diff --git a/crates/df-ai/src/stream.rs b/crates/df-ai/src/stream.rs new file mode 100644 index 0000000..ba1c7cd --- /dev/null +++ b/crates/df-ai/src/stream.rs @@ -0,0 +1,45 @@ +//! 流式处理 — LLM 响应的流式输出管理 + +use crate::provider::StreamChunk; + +/// 流式响应收集器 +pub struct StreamCollector { + /// 已收集的文本 + text: String, + /// 是否完成 + finished: bool, +} + +impl StreamCollector { + /// 创建收集器 + pub fn new() -> Self { + Self { + text: String::new(), + finished: false, + } + } + + /// 追加一个 chunk + pub fn push(&mut self, chunk: &StreamChunk) { + self.text.push_str(&chunk.delta); + if chunk.finished { + self.finished = true; + } + } + + /// 获取已收集的文本 + pub fn text(&self) -> &str { + &self.text + } + + /// 是否已完成 + pub fn is_finished(&self) -> bool { + self.finished + } +} + +impl Default for StreamCollector { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/df-core/Cargo.toml b/crates/df-core/Cargo.toml new file mode 100644 index 0000000..3bdb0e6 --- /dev/null +++ b/crates/df-core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "df-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } diff --git a/crates/df-core/src/error.rs b/crates/df-core/src/error.rs new file mode 100644 index 0000000..406b203 --- /dev/null +++ b/crates/df-core/src/error.rs @@ -0,0 +1,49 @@ +//! 统一错误类型定义 + +use thiserror::Error; + +/// 统一错误类型 +#[derive(Debug, Error)] +pub enum Error { + #[error("未找到: {0}")] + NotFound(String), + + #[error("已存在: {0}")] + AlreadyExists(String), + + #[error("验证失败: {0}")] + Validation(String), + + #[error("状态错误: 当前状态 {current}, 期望 {expected}")] + InvalidState { + current: String, + expected: String, + }, + + #[error("工作流错误: {0}")] + Workflow(String), + + #[error("执行错误: {0}")] + Execution(String), + + #[error("存储错误: {0}")] + Storage(String), + + #[error("插件错误: {0}")] + Plugin(String), + + #[error("AI 提供者错误: {0}")] + AiProvider(String), + + #[error("配置错误: {0}")] + Config(String), + + #[error("IO 错误: {0}")] + Io(#[from] std::io::Error), + + #[error("序列化错误: {0}")] + Serialization(#[from] serde_json::Error), +} + +/// 统一 Result 别名 +pub type Result = std::result::Result; diff --git a/crates/df-core/src/events.rs b/crates/df-core/src/events.rs new file mode 100644 index 0000000..b0ab1a8 --- /dev/null +++ b/crates/df-core/src/events.rs @@ -0,0 +1,74 @@ +//! 工作流事件定义 + +use serde::{Deserialize, Serialize}; + +use crate::types::NodeId; + +/// 人工审批响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HumanApprovalResponse { + pub execution_id: String, + pub node_id: NodeId, + pub decision: String, + pub comment: Option, +} + +/// 工作流事件 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum WorkflowEvent { + /// 节点开始执行 + NodeStarted { + node_id: NodeId, + }, + /// 节点执行进度更新 + NodeProgress { + node_id: NodeId, + progress: f32, + message: String, + }, + /// 节点产生输出 + NodeOutput { + node_id: NodeId, + output: String, + }, + /// 节点执行完成 + NodeCompleted { + node_id: NodeId, + duration_ms: u64, + }, + /// 节点执行失败 + NodeFailed { + node_id: NodeId, + error: String, + }, + /// 工作流暂停(等待外部输入) + WorkflowPaused { + reason: String, + waiting_node: NodeId, + }, + /// 工作流执行完成 + WorkflowCompleted { + total_duration_ms: u64, + }, + /// 工作流执行失败 + WorkflowFailed { + error: String, + failed_node: NodeId, + }, + /// 人工审批请求 + HumanApprovalRequest { + execution_id: String, + node_id: NodeId, + title: String, + description: String, + options: Vec, + }, + /// 人工审批响应 + HumanApprovalResponse { + execution_id: String, + node_id: NodeId, + decision: String, + comment: Option, + }, +} diff --git a/crates/df-core/src/lib.rs b/crates/df-core/src/lib.rs new file mode 100644 index 0000000..e27d659 --- /dev/null +++ b/crates/df-core/src/lib.rs @@ -0,0 +1,5 @@ +//! df-core: 核心类型定义,所有 crate 的基础依赖 + +pub mod error; +pub mod events; +pub mod types; diff --git a/crates/df-core/src/types.rs b/crates/df-core/src/types.rs new file mode 100644 index 0000000..579334c --- /dev/null +++ b/crates/df-core/src/types.rs @@ -0,0 +1,301 @@ +//! 核心类型定义:ID 别名、状态枚举、优先级等 + +use serde::{Deserialize, Serialize}; + +// ============================================================ +// ID 类型别名 +// ============================================================ + +/// 想法 ID +pub type IdeaId = String; +/// 项目 ID +pub type ProjectId = String; +/// 任务 ID +pub type TaskId = String; +/// 工作流 ID +pub type WorkflowId = String; +/// 节点 ID +pub type NodeId = String; +/// 发布 ID +pub type ReleaseId = String; +/// 分支 ID +pub type BranchId = String; +/// 插件 ID +pub type PluginId = String; +/// 执行 ID +pub type ExecutionId = String; +/// 决策 ID +pub type DecisionId = String; + +// ============================================================ +// 状态枚举 +// ============================================================ + +/// 想法状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IdeaStatus { + /// 草稿 + Draft, + /// 待评估 + PendingReview, + /// 已批准 + Approved, + /// 已拒绝 + Rejected, + /// 已转为项目 + Promoted, + /// 已归档 + Archived, +} + +impl IdeaStatus { + /// 返回数据库存储用的小写字符串 + pub fn as_str(&self) -> &'static str { + match self { + IdeaStatus::Draft => "draft", + IdeaStatus::PendingReview => "pending_review", + IdeaStatus::Approved => "approved", + IdeaStatus::Rejected => "rejected", + IdeaStatus::Promoted => "promoted", + IdeaStatus::Archived => "archived", + } + } +} + +impl Default for IdeaStatus { + fn default() -> Self { + IdeaStatus::Draft + } +} + +/// 项目状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProjectStatus { + /// 规划中 + Planning, + /// 开发中 + InProgress, + /// 测试中 + Testing, + /// 发布中 + Releasing, + /// 已完成 + Completed, + /// 已暂停 + Paused, + /// 已取消 + Cancelled, +} + +impl ProjectStatus { + /// 返回数据库存储用的小写字符串 + pub fn as_str(&self) -> &'static str { + match self { + ProjectStatus::Planning => "planning", + ProjectStatus::InProgress => "in_progress", + ProjectStatus::Testing => "testing", + ProjectStatus::Releasing => "releasing", + ProjectStatus::Completed => "completed", + ProjectStatus::Paused => "paused", + ProjectStatus::Cancelled => "cancelled", + } + } +} + +impl Default for ProjectStatus { + fn default() -> Self { + ProjectStatus::Planning + } +} + +/// 任务状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TaskStatus { + /// 待开始 + Todo, + /// 进行中 + InProgress, + /// 代码审查中 + InReview, + /// 测试中 + Testing, + /// 已完成 + Done, + /// 已阻塞 + Blocked, + /// 已取消 + Cancelled, +} + +impl TaskStatus { + /// 返回数据库存储用的小写字符串 + pub fn as_str(&self) -> &'static str { + match self { + TaskStatus::Todo => "todo", + TaskStatus::InProgress => "in_progress", + TaskStatus::InReview => "in_review", + TaskStatus::Testing => "testing", + TaskStatus::Done => "done", + TaskStatus::Blocked => "blocked", + TaskStatus::Cancelled => "cancelled", + } + } +} + +impl Default for TaskStatus { + fn default() -> Self { + TaskStatus::Todo + } +} + +/// 工作流状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum WorkflowStatus { + /// 待执行 + Pending, + /// 运行中 + Running, + /// 已暂停(等待人工输入等) + Paused, + /// 已完成 + Completed, + /// 已失败 + Failed, + /// 已取消 + Cancelled, +} + +impl WorkflowStatus { + /// 返回数据库存储用的小写字符串 + pub fn as_str(&self) -> &'static str { + match self { + WorkflowStatus::Pending => "pending", + WorkflowStatus::Running => "running", + WorkflowStatus::Paused => "paused", + WorkflowStatus::Completed => "completed", + WorkflowStatus::Failed => "failed", + WorkflowStatus::Cancelled => "cancelled", + } + } +} + +impl Default for WorkflowStatus { + fn default() -> Self { + WorkflowStatus::Pending + } +} + +/// 节点状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NodeStatus { + /// 待执行 + Pending, + /// 运行中 + Running, + /// 已完成 + Completed, + /// 已失败 + Failed, + /// 已跳过 + Skipped, + /// 等待中(如等待人工操作) + Waiting, + /// 已取消 + Cancelled, +} + +impl NodeStatus { + /// 返回数据库存储用的小写字符串 + pub fn as_str(&self) -> &'static str { + match self { + NodeStatus::Pending => "pending", + NodeStatus::Running => "running", + NodeStatus::Completed => "completed", + NodeStatus::Failed => "failed", + NodeStatus::Skipped => "skipped", + NodeStatus::Waiting => "waiting", + NodeStatus::Cancelled => "cancelled", + } + } +} + +impl Default for NodeStatus { + fn default() -> Self { + NodeStatus::Pending + } +} + +/// 分支状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BranchStatus { + /// 活跃中 + Active, + /// 已合并 + Merged, + /// 已废弃 + Abandoned, +} + +impl BranchStatus { + /// 返回数据库存储用的小写字符串 + pub fn as_str(&self) -> &'static str { + match self { + BranchStatus::Active => "active", + BranchStatus::Merged => "merged", + BranchStatus::Abandoned => "abandoned", + } + } +} + +impl Default for BranchStatus { + fn default() -> Self { + BranchStatus::Active + } +} + +/// 优先级 +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Priority { + /// 低 + Low = 0, + /// 中 + Medium = 1, + /// 高 + High = 2, + /// 紧急 + Critical = 3, +} + +impl Priority { + /// 返回数据库存储用的小写字符串 + pub fn as_str(&self) -> &'static str { + match self { + Priority::Low => "low", + Priority::Medium => "medium", + Priority::High => "high", + Priority::Critical => "critical", + } + } +} + +impl Default for Priority { + fn default() -> Self { + Priority::Medium + } +} + +// ============================================================ +// 辅助函数 +// ============================================================ + +/// 生成新的 UUID v4 字符串 +pub fn new_id() -> String { + uuid::Uuid::new_v4().to_string() +} diff --git a/crates/df-evolve/Cargo.toml b/crates/df-evolve/Cargo.toml new file mode 100644 index 0000000..c8ca796 --- /dev/null +++ b/crates/df-evolve/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "df-evolve" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-evolve/src/evolve_engine.rs b/crates/df-evolve/src/evolve_engine.rs new file mode 100644 index 0000000..1c2b7d2 --- /dev/null +++ b/crates/df-evolve/src/evolve_engine.rs @@ -0,0 +1,58 @@ +//! 进化引擎:知识沉淀的核心闭环 +//! +//! 使用 → 沉淀 → 复用 → 改进 → 再沉淀 + +use crate::knowledge::{Knowledge, KnowledgeKind, KnowledgeStore}; +use crate::pattern::PatternExtractor; + +/// 进化引擎 +pub struct EvolveEngine { + extractor: PatternExtractor, +} + +impl EvolveEngine { + pub fn new() -> Self { + Self { + extractor: PatternExtractor, + } + } + + /// 自动扫描项目事件,提取可沉淀的知识 + /// + /// 触发时机: + /// - 工作流节点完成后 + /// - 代码审查完成后 + /// - Bug 修复完成后 + /// - 发布完成后 + pub async fn evolve_from_events(&self, _events: &[serde_json::Value]) -> Vec { + let mut new_knowledge = Vec::new(); + + // TODO: 遍历事件,分类处理 + // 1. 审查事件 → 提取审查规则 + // 2. Bug 修复事件 → 提取诊断知识 + // 3. 发布事件 → 提取部署经验 + // 4. Prompt 事件 → 提取 Prompt 模板 + + new_knowledge + } + + /// 查询当前任务相关的知识(供 AI 节点使用) + /// + /// AI 在执行任务前可以查询知识库,获取相关经验和规则 + pub fn query_relevant( + &self, + _context: &str, + _kind: Option<&KnowledgeKind>, + ) -> Vec { + // TODO: 语义搜索知识库 + KnowledgeStore::search(_context, _kind, 5) + } + + /// 验证知识的有效性(定期执行) + /// + /// 检查知识是否仍然适用(依赖版本是否过时、规则是否仍有意义等) + pub async fn validate_knowledge(&self) -> Vec { + // TODO: 遍历知识库,标记过时的知识 + vec![] + } +} diff --git a/crates/df-evolve/src/knowledge.rs b/crates/df-evolve/src/knowledge.rs new file mode 100644 index 0000000..3584765 --- /dev/null +++ b/crates/df-evolve/src/knowledge.rs @@ -0,0 +1,98 @@ +//! 知识条目:经验沉淀的基本单元 + +use df_core::types::ProjectId; +use serde::{Deserialize, Serialize}; + +/// 知识类型 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum KnowledgeKind { + /// 代码审查规则(如"禁止在循环中创建连接") + ReviewRule, + /// 有效的 Prompt 模板 + PromptTemplate, + /// 踩坑经验 + Pitfall, + /// 架构模式 + ArchitecturePattern, + /// 诊断知识(Bug 根因分析) + Diagnosis, + /// 部署经验 + DeploymentNote, + /// 工作流优化建议 + WorkflowOptimization, +} + +/// 知识条目 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Knowledge { + pub id: String, + pub kind: KnowledgeKind, + /// 标题 + pub title: String, + /// 内容 + pub content: String, + /// 标签 + pub tags: Vec, + /// 来源项目 + pub source_project: Option, + /// 来源实体(如某次审查、某个 Bug 修复) + pub source_ref: Option, + /// 被复用次数 + pub reuse_count: usize, + /// 效果评分 (0-100,由用户或 AI 评估) + pub effectiveness: Option, + /// 是否已验证有效 + pub verified: bool, + pub created_at: i64, + pub updated_at: i64, +} + +impl Knowledge { + pub fn new(kind: KnowledgeKind, title: String, content: String) -> Self { + let now = chrono::Utc::now().timestamp(); + Self { + id: df_core::types::new_id(), + kind, + title, + content, + tags: vec![], + source_project: None, + source_ref: None, + reuse_count: 0, + effectiveness: None, + verified: false, + created_at: now, + updated_at: now, + } + } + + /// 记录一次复用 + pub fn record_reuse(&mut self) { + self.reuse_count += 1; + self.updated_at = chrono::Utc::now().timestamp(); + } +} + +/// 知识库(内存索引,持久化到 SQLite) +pub struct KnowledgeStore; + +impl KnowledgeStore { + /// 搜索相关知识 + pub fn search(_query: &str, _kind: Option<&KnowledgeKind>, _limit: usize) -> Vec { + // TODO: SQLite 全文搜索或向量搜索 + vec![] + } + + /// 获取最常用的知识 + pub fn top_used(_limit: usize) -> Vec { + // TODO: 按 reuse_count 降序 + vec![] + } + + /// 保存知识条目 + pub fn save(_knowledge: &Knowledge) -> anyhow::Result<()> { + // TODO: SQLite INSERT/UPDATE + Ok(()) + } +} diff --git a/crates/df-evolve/src/lib.rs b/crates/df-evolve/src/lib.rs new file mode 100644 index 0000000..11b9859 --- /dev/null +++ b/crates/df-evolve/src/lib.rs @@ -0,0 +1,12 @@ +//! 经验进化引擎:从开发过程中自动沉淀知识,持续进化复用 +//! +//! 核心闭环:使用 → 沉淀 → 复用 → 改进 → 再沉淀 + +pub mod knowledge; +pub mod pattern; +pub mod prompt_template; +pub mod review_rule; +pub mod evolve_engine; + +pub use evolve_engine::EvolveEngine; +pub use knowledge::{Knowledge, KnowledgeKind, KnowledgeStore}; diff --git a/crates/df-evolve/src/pattern.rs b/crates/df-evolve/src/pattern.rs new file mode 100644 index 0000000..2abe92f --- /dev/null +++ b/crates/df-evolve/src/pattern.rs @@ -0,0 +1,47 @@ +//! 模式提取器:从开发过程中自动识别可沉淀的模式 + +use crate::knowledge::{Knowledge, KnowledgeKind}; + +/// 模式提取器 +/// +/// 自动从以下场景中识别可沉淀的模式: +/// - 代码审查 → 审查规则 +/// - Bug 修复 → 诊断知识 +/// - 发布流程 → 部署经验 +/// - Prompt 调优 → Prompt 模板 +pub struct PatternExtractor; + +impl PatternExtractor { + /// 从代码审查结果中提取审查规则 + /// + /// 如果同一类问题在多次审查中重复出现,自动沉淀为规则 + pub fn extract_review_rule( + _findings: &[serde_json::Value], + _occurrence_threshold: usize, + ) -> Option { + // TODO: + // 1. 分析 findings 的共性 + // 2. 如果出现次数 >= threshold,生成规则 + // 3. 去重(与已有规则比较) + None + } + + /// 从 Bug 修复过程中提取诊断知识 + pub fn extract_diagnosis( + _bug_description: &str, + _root_cause: &str, + _fix_description: &str, + ) -> Option { + // TODO: AI 总结为可复用的诊断知识 + None + } + + /// 从成功的 Prompt 中提取模板 + pub fn extract_prompt_template( + _prompt: &str, + _result_quality: f32, + ) -> Option { + // TODO: 如果 result_quality > 0.8,提取为模板 + None + } +} diff --git a/crates/df-evolve/src/prompt_template.rs b/crates/df-evolve/src/prompt_template.rs new file mode 100644 index 0000000..341336e --- /dev/null +++ b/crates/df-evolve/src/prompt_template.rs @@ -0,0 +1,41 @@ +//! Prompt 模板管理:AI 交互经验的沉淀与复用 + +use serde::{Deserialize, Serialize}; + +/// Prompt 模板 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PromptTemplate { + pub id: String, + /// 模板名称 + pub name: String, + /// 模板内容(支持 {variable} 占位符) + pub template: String, + /// 变量说明 + pub variables: Vec, + /// 适用场景 + pub applicable_scenarios: Vec, + /// 效果评分 + pub avg_score: f32, + /// 使用次数 + pub use_count: usize, +} + +/// 模板变量 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TemplateVariable { + pub name: String, + pub description: String, + pub default_value: Option, + pub required: bool, +} + +impl PromptTemplate { + /// 渲染模板(替换变量) + pub fn render(&self, vars: &std::collections::HashMap) -> String { + let mut result = self.template.clone(); + for (key, value) in vars { + result = result.replace(&format!("{{{}}}", key), value); + } + result + } +} diff --git a/crates/df-evolve/src/review_rule.rs b/crates/df-evolve/src/review_rule.rs new file mode 100644 index 0000000..eedcc2a --- /dev/null +++ b/crates/df-evolve/src/review_rule.rs @@ -0,0 +1,45 @@ +//! 审查规则:从历史审查经验中沉淀的代码审查规则 + +use serde::{Deserialize, Serialize}; + +/// 审查规则 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReviewRule { + pub id: String, + /// 规则标题 + pub title: String, + /// 规则描述 + pub description: String, + /// 严重级别 + pub severity: RuleSeverity, + /// 适用的语言/框架 + pub scope: Vec, + /// 检查方式(正则/AST/AI) + pub check_method: CheckMethod, + /// 发现次数(历史累计) + pub found_count: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuleSeverity { + /// 必须修复 + MustFix, + /// 建议改进 + ShouldFix, + /// 可选优化 + NiceToHave, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CheckMethod { + /// 正则匹配 + Regex, + /// AST 分析 + Ast, + /// AI 判断 + AiAnalysis, + /// 人工判断 + Manual, +} diff --git a/crates/df-execute/Cargo.toml b/crates/df-execute/Cargo.toml new file mode 100644 index 0000000..d3a6f4f --- /dev/null +++ b/crates/df-execute/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "df-execute" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +async-trait = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-execute/src/docker.rs b/crates/df-execute/src/docker.rs new file mode 100644 index 0000000..94cd4b5 --- /dev/null +++ b/crates/df-execute/src/docker.rs @@ -0,0 +1,46 @@ +//! Docker 执行器 — 在容器中运行任务 + +use serde::{Deserialize, Serialize}; + +/// Docker 容器执行请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DockerRequest { + /// 镜像名称 + pub image: String, + /// 容器内执行的命令 + pub command: Option, + /// 环境变量 + pub env: std::collections::HashMap, + /// 挂载卷 + pub volumes: Vec, + /// 是否在执行后自动删除容器 + pub auto_remove: bool, +} + +/// 卷挂载 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VolumeMount { + pub host_path: String, + pub container_path: String, + pub read_only: bool, +} + +/// Docker 执行结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DockerResult { + pub stdout: String, + pub stderr: String, + pub exit_code: Option, +} + +/// 在 Docker 容器中执行命令 +/// +/// TODO: 实现 Docker API 调用或 CLI 包装 +pub async fn execute(_request: DockerRequest) -> anyhow::Result { + tracing::info!("Docker 执行: TODO"); + Ok(DockerResult { + stdout: String::new(), + stderr: String::new(), + exit_code: None, + }) +} diff --git a/crates/df-execute/src/git_ops.rs b/crates/df-execute/src/git_ops.rs new file mode 100644 index 0000000..3595c30 --- /dev/null +++ b/crates/df-execute/src/git_ops.rs @@ -0,0 +1,47 @@ +//! Git 操作 — 克隆、提交、推送、合并等 + +use serde::{Deserialize, Serialize}; + +/// Git 操作类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum GitAction { + Clone, + Commit, + Push, + Pull, + Merge, + Checkout, + CreateBranch, +} + +/// Git 操作请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitRequest { + /// 操作类型 + pub action: GitAction, + /// 仓库路径(本地路径或远程 URL) + pub repo: String, + /// 分支名 + pub branch: Option, + /// 提交消息 + pub message: Option, +} + +/// Git 操作结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitResult { + pub success: bool, + pub message: String, +} + +/// 执行 Git 操作 +/// +/// TODO: 实现完整的 Git 操作(可包装 git CLI 或使用 git2 crate) +pub async fn execute(_request: GitRequest) -> anyhow::Result { + tracing::info!("Git 操作: TODO"); + Ok(GitResult { + success: true, + message: "TODO: 未实现".to_string(), + }) +} diff --git a/crates/df-execute/src/lib.rs b/crates/df-execute/src/lib.rs new file mode 100644 index 0000000..dcb3acb --- /dev/null +++ b/crates/df-execute/src/lib.rs @@ -0,0 +1,6 @@ +//! df-execute: 执行运行时 — Shell、Docker、SSH、Git 操作 + +pub mod docker; +pub mod git_ops; +pub mod shell; +pub mod ssh; diff --git a/crates/df-execute/src/shell.rs b/crates/df-execute/src/shell.rs new file mode 100644 index 0000000..ae37897 --- /dev/null +++ b/crates/df-execute/src/shell.rs @@ -0,0 +1,73 @@ +//! Shell 执行器 — 通过 tokio::process 执行 shell 命令 + +use serde::{Deserialize, Serialize}; + +/// Shell 命令执行结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShellResult { + /// 标准输出 + pub stdout: String, + /// 标准错误 + pub stderr: String, + /// 退出码 + pub exit_code: Option, + /// 执行耗时(毫秒) + pub duration_ms: u64, +} + +/// Shell 命令执行请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShellRequest { + /// 要执行的命令 + pub command: String, + /// 工作目录 + pub working_dir: Option, + /// 环境变量 + pub env: std::collections::HashMap, + /// 超时时间(秒),None 表示不超时 + pub timeout_secs: Option, +} + +/// 执行 Shell 命令 +/// +/// TODO: 完整实现,支持超时、环境变量、工作目录等 +pub async fn execute(request: ShellRequest) -> anyhow::Result { + let start = std::time::Instant::now(); + + let mut cmd = if cfg!(windows) { + let mut c = tokio::process::Command::new("cmd"); + c.arg("/C").arg(&request.command); + c + } else { + let mut c = tokio::process::Command::new("sh"); + c.arg("-c").arg(&request.command); + c + }; + + if let Some(dir) = &request.working_dir { + cmd.current_dir(dir); + } + + for (key, value) in &request.env { + cmd.env(key, value); + } + + let output = match request.timeout_secs { + Some(secs) => tokio::time::timeout( + std::time::Duration::from_secs(secs), + cmd.output(), + ) + .await + .map_err(|_| anyhow::anyhow!("命令执行超时: {}秒", secs))??, + None => cmd.output().await?, + }; + + let duration = start.elapsed().as_millis() as u64; + + Ok(ShellResult { + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + exit_code: output.status.code(), + duration_ms: duration, + }) +} diff --git a/crates/df-execute/src/ssh.rs b/crates/df-execute/src/ssh.rs new file mode 100644 index 0000000..f0695b2 --- /dev/null +++ b/crates/df-execute/src/ssh.rs @@ -0,0 +1,38 @@ +//! SSH 执行器 — 远程命令执行 + +use serde::{Deserialize, Serialize}; + +/// SSH 执行请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SshRequest { + /// 主机地址 + pub host: String, + /// 端口 + pub port: u16, + /// 用户名 + pub user: String, + /// 要执行的命令 + pub command: String, + /// 超时时间(秒) + pub timeout_secs: Option, +} + +/// SSH 执行结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SshResult { + pub stdout: String, + pub stderr: String, + pub exit_code: Option, +} + +/// 通过 SSH 执行远程命令 +/// +/// TODO: 实现SSH连接(可用 ssh2 crate 或包装 ssh 命令) +pub async fn execute(_request: SshRequest) -> anyhow::Result { + tracing::info!("SSH 执行: TODO"); + Ok(SshResult { + stdout: String::new(), + stderr: String::new(), + exit_code: None, + }) +} diff --git a/crates/df-ideas/Cargo.toml b/crates/df-ideas/Cargo.toml new file mode 100644 index 0000000..8c0fde5 --- /dev/null +++ b/crates/df-ideas/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "df-ideas" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +anyhow = { workspace = true } +chrono = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-ideas/src/adversarial.rs b/crates/df-ideas/src/adversarial.rs new file mode 100644 index 0000000..de930aa --- /dev/null +++ b/crates/df-ideas/src/adversarial.rs @@ -0,0 +1,308 @@ +//! 对抗式评估系统 — 正反方辩论 + AI 分析师 + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use df_core::types::IdeaId; +use crate::capture::Idea; + +/// 对抗评估结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdversarialEval { + pub idea_id: IdeaId, + pub positive: Argument, + pub negative: Argument, + pub analyst: AnalystAnalysis, + pub final_score: f64, + pub recommendation: Recommendation, +} + +/// 正方论点 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Argument { + pub thesis: String, // 核心观点 + pub evidence: Vec, // 证据支持 + pub reasoning: Vec, // 推理过程 + pub confidence: f64, // 置信度 0-1 +} + +/// 反方论点 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CounterArgument { + pub thesis: String, // 反对观点 + pub evidence: Vec, // 反对证据 + pub reasoning: Vec, // 反驳推理 + pub confidence: f64, // 置信度 0-1 +} + +/// AI 分析师综合分析 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnalystAnalysis { + pub summary: String, // 综合总结 + pub strengths: Vec, // 主要优势 + pub weaknesses: Vec, // 主要劣势 + pub risks: Vec, // 潜在风险 + pub opportunities: Vec, // 机会点 + pub final_assessment: AssessmentLevel, // 最终评估 +} + +/// 评估等级 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum AssessmentLevel { + StrongGo, // 强烈推荐执行 + Recommended, // 推荐执行 + Conditional, // 有条件执行 + Revised, // 需要修改后执行 + Defer, // 推迟执行 + Reject, // 不推荐执行 +} + +/// 最终建议 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum Recommendation { + ImmediateAction, // 立即行动 + Soon, // 尽快行动 + WithResources, // 配置资源后行动 + ResearchMore, // 需要更多研究 + Monitor, // 持续监控 + Cancel, // 取消想法 +} + +/// 对抗评估引擎 +pub struct AdversarialEngine; + +impl AdversarialEngine { + /// 执行完整的对抗评估 + pub async fn evaluate(idea: &Idea) -> Result { + // 1. 生成正方观点 + let positive = Self::generate_positive_argument(idea).await?; + + // 2. 生成反方观点 + let negative = Self::generate_negative_argument(idea, &positive).await?; + + // 3. AI 分析师综合分析 + let analyst = Self::analyst_analysis(idea, &positive, &negative).await?; + + // 4. 计算最终分数和建议 + let (final_score, recommendation) = Self::compute_final_assessment(&analyst); + + Ok(AdversarialEval { + idea_id: idea.id.clone(), + positive, + negative, + analyst, + final_score, + recommendation, + }) + } + + /// 生成正方观点(支持执行) + async fn generate_positive_argument(idea: &Idea) -> Result { + // TODO: 接入 AI 生成正方观点 + // 当前使用启发式模板 + + let title = &idea.title; + let desc = &idea.description; + + Ok(Argument { + thesis: format!("{} 具有很高的价值和可行性,应该优先执行", title), + evidence: vec![ + format!("满足业务需求:{}", desc), + "投入产出比高".to_string(), + "技术实现可行".to_string(), + "时间窗口合适".to_string(), + ], + reasoning: vec![ + "能够解决现有痛点".to_string(), + "竞争优势明显".to_string(), + "风险可控".to_string(), + ], + confidence: 0.75, + }) + } + + /// 生成反方观点(反对或谨慎) + async fn generate_negative_argument(idea: &Idea, positive: &Argument) -> Result { + // TODO: 接入 AI 生成反方观点,考虑正方观点 + + let title = &idea.title; + + Ok(CounterArgument { + thesis: format!("{} 需要谨慎评估,存在一定风险", title), + evidence: vec![ + "资源投入较大".to_string(), + "市场不确定性高".to_string(), + "技术挑战存在".to_string(), + "机会成本高".to_string(), + ], + reasoning: vec![ + "ROI 需要进一步验证".to_string(), + "优先级可能过高".to_string(), + "存在更优替代方案".to_string(), + ], + confidence: 0.65, + }) + } + + /// AI 分析师综合分析 + async fn analyst_analysis( + idea: &Idea, + positive: &Argument, + negative: &CounterArgument, + ) -> Result { + // TODO: 接入 AI 进行深度分析 + + let positive_strengths = vec![ + "方向正确,符合业务战略".to_string(), + "技术创新性较强".to_string(), + "用户价值明确".to_string(), + ]; + + let weaknesses = vec![ + "资源需求评估不足".to_string(), + "风险控制需要加强".to_string(), + "时间规划可能过于乐观".to_string(), + ]; + + let risks = vec![ + "技术实现难度超出预期".to_string(), + "市场竞争加剧".to_string(), + "用户接受度不确定".to_string(), + ]; + + let opportunities = vec![ + "可能形成新的竞争优势".to_string(), + "技术积累价值显著".to_string(), + "市场机会窗口良好".to_string(), + ]; + + // 基于正反方观点的强度计算 + let positive_strength = positive.confidence; + let negative_strength = negative.confidence; + let net_positive = (positive_strength - negative_strength + 1.0) / 2.0; + + let final_assessment = if net_positive > 0.7 { + AssessmentLevel::StrongGo + } else if net_positive > 0.5 { + AssessmentLevel::Recommended + } else if net_positive > 0.3 { + AssessmentLevel::Conditional + } else if net_positive > 0.1 { + AssessmentLevel::Revised + } else { + AssessmentLevel::Defer + }; + + Ok(AnalystAnalysis { + summary: format!( + "该想法整体价值评估中等偏上,建议在有条件的情况下执行。主要价值在于{},需要关注{}。", + idea.title, + if net_positive > 0.5 { "风险控制" } else { "价值验证" } + ), + strengths: positive_strengths, + weaknesses, + risks, + opportunities, + final_assessment, + }) + } + + /// 计算最终评估分数和建议 + fn compute_final_assessment(analyst: &AnalystAnalysis) -> (f64, Recommendation) { + // 基于评估等级映射分数 + let base_score = match analyst.final_assessment { + AssessmentLevel::StrongGo => 8.5, + AssessmentLevel::Recommended => 7.0, + AssessmentLevel::Conditional => 5.5, + AssessmentLevel::Revised => 4.0, + AssessmentLevel::Defer => 2.5, + AssessmentLevel::Reject => 1.0, + }; + + // 根据优劣势微调分数 + let strength_count = analyst.strengths.len() as f64; + let weakness_count = analyst.weaknesses.len() as f64; + let score_adjustment = (strength_count - weakness_count) * 0.3; + + let final_score = (base_score + score_adjustment).clamp(0.0, 10.0); + + let recommendation = match analyst.final_assessment { + AssessmentLevel::StrongGo => Recommendation::ImmediateAction, + AssessmentLevel::Recommended => Recommendation::Soon, + AssessmentLevel::Conditional => Recommendation::WithResources, + AssessmentLevel::Revised => Recommendation::ResearchMore, + AssessmentLevel::Defer => Recommendation::Monitor, + AssessmentLevel::Reject => Recommendation::Cancel, + }; + + (final_score, recommendation) + } +} + +/// 评估结果展示格式 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EvalDisplay { + pub idea_title: String, + pub positive_strength: f64, + pub negative_strength: f64, + pub net_sentiment: f64, // -1 到 1,正为正面 + pub assessment_level: String, + pub key_takeaways: Vec, + pub action_items: Vec, +} + +impl From for EvalDisplay { + fn from(eval: AdversarialEval) -> Self { + let net_sentiment = (eval.positive.confidence - eval.negative.confidence) as f64; + + let key_takeaways = vec![ + format!("优势:{}", eval.analyst.strengths.join("、")), + format!("风险:{}", eval.analyst.risks.join("、")), + format!("建议:{:?}", eval.recommendation), + ]; + + let action_items = match eval.recommendation { + Recommendation::ImmediateAction => vec![ + "立即组建项目团队".to_string(), + "制定详细执行计划".to_string(), + "分配必要资源".to_string(), + ], + Recommendation::Soon => vec![ + "下周启动项目".to_string(), + "准备资源需求".to_string(), + "制定时间表".to_string(), + ], + Recommendation::WithResources => vec![ + "确认资源预算".to_string(), + "评估ROI".to_string(), + "制定风险预案".to_string(), + ], + Recommendation::ResearchMore => vec![ + "进行市场调研".to_string(), + "收集用户反馈".to_string(), + "验证技术可行性".to_string(), + ], + Recommendation::Monitor => vec![ + "持续跟踪相关指标".to_string(), + "定期评估进展".to_string(), + "等待更好的时机".to_string(), + ], + Recommendation::Cancel => vec![ + "记录归档原因".to_string(), + "释放相关资源".to_string(), + "提取经验教训".to_string(), + ], + }; + + EvalDisplay { + idea_title: eval.positive.thesis.split(' ').take(3).collect::>().join(" "), + positive_strength: eval.positive.confidence, + negative_strength: eval.negative.confidence, + net_sentiment, + assessment_level: format!("{:?}", eval.analyst.final_assessment), + key_takeaways, + action_items, + } + } +} \ No newline at end of file diff --git a/crates/df-ideas/src/capture.rs b/crates/df-ideas/src/capture.rs new file mode 100644 index 0000000..f7810cb --- /dev/null +++ b/crates/df-ideas/src/capture.rs @@ -0,0 +1,98 @@ +//! 想法捕获 — 快速记录与管理 + +use serde::{Deserialize, Serialize}; + +use df_core::types::{IdeaId, Priority}; + +/// 捕获一个新想法的输入 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CaptureInput { + /// 标题 + pub title: String, + /// 详细描述 + pub description: String, + /// 优先级 + #[serde(default)] + pub priority: Priority, + /// 标签 + #[serde(default)] + pub tags: Vec, + /// 来源(如 "用户输入"、"AI 生成"、"会议记录") + pub source: Option, +} + +/// 想法实体 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Idea { + /// 唯一 ID + pub id: IdeaId, + /// 标题 + pub title: String, + /// 详细描述 + pub description: String, + /// 当前状态 + pub status: df_core::types::IdeaStatus, + /// 优先级 + pub priority: Priority, + /// 评分 + pub scores: Option, + /// 标签 + pub tags: Vec, + /// 来源 + pub source: Option, + /// 关联的想法 ID + pub related_ids: Vec, + /// 创建时间 + pub created_at: chrono::DateTime, + /// 更新时间 + pub updated_at: chrono::DateTime, +} + +/// 想法评分详情 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IdeaScores { + /// 可行性评分 (0-10) + pub feasibility: f64, + /// 影响力评分 (0-10) + pub impact: f64, + /// 紧急度评分 (0-10) + pub urgency: f64, + /// 综合评分 (加权平均) + pub overall: f64, +} + +/// 想法捕获器 +pub struct IdeaCapture; + +impl IdeaCapture { + /// 捕获一个新想法 + /// + /// TODO: 接入存储层持久化 + pub fn capture(input: CaptureInput) -> Idea { + let now = chrono::Utc::now(); + Idea { + id: df_core::types::new_id(), + title: input.title, + description: input.description, + status: df_core::types::IdeaStatus::Draft, + priority: input.priority, + scores: None, + tags: input.tags, + source: input.source, + related_ids: Vec::new(), + created_at: now, + updated_at: now, + } + } + + /// 快速捕获(仅标题) + pub fn quick_capture(title: String) -> Idea { + Self::capture(CaptureInput { + title, + description: String::new(), + priority: Priority::default(), + tags: Vec::new(), + source: None, + }) + } +} diff --git a/crates/df-ideas/src/evaluator.rs b/crates/df-ideas/src/evaluator.rs new file mode 100644 index 0000000..77743de --- /dev/null +++ b/crates/df-ideas/src/evaluator.rs @@ -0,0 +1,79 @@ +//! 想法评估器 — 对想法进行多维度评估 + +use anyhow::Result; + +use df_core::types::IdeaId; + +use crate::adversarial::{AdversarialEngine, AdversarialEval}; +use crate::capture::Idea; +use crate::scoring::IdeaScores; + +/// 评估维度 +#[derive(Debug, Clone, Copy)] +pub enum EvalDimension { + /// 可行性 + Feasibility, + /// 影响力 + Impact, + /// 紧急度 + Urgency, +} + +/// 评估结果 +#[derive(Debug, Clone)] +pub struct EvalResult { + pub idea_id: IdeaId, + pub scores: IdeaScores, + pub recommendation: Recommendation, + pub comments: Vec, +} + +/// 评估建议 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Recommendation { + /// 强烈推荐立即执行 + StrongApprove, + /// 推荐执行 + Approve, + /// 需要更多信息 + NeedsInfo, + /// 建议推迟 + Defer, + /// 不推荐 + Reject, +} + +/// 想法评估器 +pub struct IdeaEvaluator; + +impl IdeaEvaluator { + /// 评估一个想法 - 使用对抗式评估 + pub async fn evaluate_adversarial(idea: &Idea) -> Result { + AdversarialEngine::evaluate(idea).await + } + + /// 评估一个想法 - 保持向后兼容 + pub fn evaluate(idea: &Idea) -> Result { + // 使用简单评分作为后备 + let scores = crate::scoring::ScoringEngine::compute_default(idea); + + let recommendation = if scores.overall >= 8.0 { + Recommendation::StrongApprove + } else if scores.overall >= 6.0 { + Recommendation::Approve + } else if scores.overall >= 4.0 { + Recommendation::NeedsInfo + } else if scores.overall >= 2.0 { + Recommendation::Defer + } else { + Recommendation::Reject + }; + + Ok(EvalResult { + idea_id: idea.id.clone(), + scores, + recommendation, + comments: Vec::new(), + }) + } +} diff --git a/crates/df-ideas/src/graph.rs b/crates/df-ideas/src/graph.rs new file mode 100644 index 0000000..b1d7ca5 --- /dev/null +++ b/crates/df-ideas/src/graph.rs @@ -0,0 +1,76 @@ +//! 想法关联图 — 管理想法之间的关系 + +use std::collections::HashMap; + +use df_core::types::IdeaId; + +/// 想法之间的关系类型 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RelationKind { + /// 相似(语义相近) + Similar, + /// 依赖(A 依赖 B) + DependsOn, + /// 衍生(A 衍生自 B) + DerivedFrom, + /// 互补(A 和 B 可以互补) + Complementary, +} + +/// 想法关系边 +#[derive(Debug, Clone)] +pub struct Relation { + pub source_id: IdeaId, + pub target_id: IdeaId, + pub kind: RelationKind, + pub strength: f64, // 0.0 ~ 1.0 +} + +/// 想法关联图 +pub struct IdeaGraph { + /// 邻接表(idea_id -> 相关关系列表) + edges: HashMap>, +} + +impl IdeaGraph { + /// 创建空图 + pub fn new() -> Self { + Self { + edges: HashMap::new(), + } + } + + /// 添加关系 + pub fn add_relation(&mut self, source_id: IdeaId, target_id: IdeaId, kind: RelationKind, strength: f64) { + let relation = Relation { + source_id: source_id.clone(), + target_id: target_id.clone(), + kind, + strength, + }; + self.edges.entry(source_id).or_default().push(relation.clone()); + self.edges.entry(target_id).or_default().push(relation); + } + + /// 获取与指定想法相关的所有关系 + pub fn get_relations(&self, idea_id: &IdeaId) -> Vec<&Relation> { + self.edges.get(idea_id).map(|r| r.iter().collect()).unwrap_or_default() + } + + /// 查找相似想法 + pub fn find_similar(&self, idea_id: &IdeaId) -> Vec<&Relation> { + self.get_relations(idea_id) + .into_iter() + .filter(|r| r.kind == RelationKind::Similar) + .collect() + } + + // TODO: 基于向量相似度的自动关联发现 + // TODO: 图遍历、聚类算法 +} + +impl Default for IdeaGraph { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/df-ideas/src/lib.rs b/crates/df-ideas/src/lib.rs new file mode 100644 index 0000000..1970665 --- /dev/null +++ b/crates/df-ideas/src/lib.rs @@ -0,0 +1,8 @@ +//! df-ideas: 想法池 — 捕获、评估、评分、关联图、晋升 + +pub mod adversarial; +pub mod capture; +pub mod evaluator; +pub mod graph; +pub mod promotion; +pub mod scoring; diff --git a/crates/df-ideas/src/promotion.rs b/crates/df-ideas/src/promotion.rs new file mode 100644 index 0000000..78ccbe5 --- /dev/null +++ b/crates/df-ideas/src/promotion.rs @@ -0,0 +1,91 @@ +//! 想法晋升 — 将想法转为项目 + +use anyhow::Result; + +use df_core::types::{IdeaId, ProjectId}; + +use crate::capture::Idea; +use crate::evaluator::Recommendation; + +/// 晋升结果 +#[derive(Debug, Clone)] +pub struct PromotionResult { + pub idea_id: IdeaId, + pub project_id: ProjectId, + pub promoted: bool, + pub reason: String, +} + +/// 晋升策略 +#[derive(Debug, Clone, Copy)] +pub enum PromotionPolicy { + /// 自动晋升(评分达标时自动转为项目) + Auto, + /// 手动确认(需要人工审批) + Manual, + /// 半自动(AI 评估 + 人工确认) + SemiAuto, +} + +/// 想法晋升器 +pub struct IdeaPromoter { + policy: PromotionPolicy, +} + +impl IdeaPromoter { + /// 创建晋升器 + pub fn new(policy: PromotionPolicy) -> Self { + Self { policy } + } + + /// 评估并尝试晋升想法 + /// + /// TODO: 接入 df-project 创建项目 + pub fn try_promote(&self, idea: &Idea, recommendation: &Recommendation) -> Result { + match self.policy { + PromotionPolicy::Auto => { + if matches!(recommendation, Recommendation::StrongApprove | Recommendation::Approve) { + self.do_promote(idea) + } else { + Ok(PromotionResult { + idea_id: idea.id.clone(), + project_id: String::new(), + promoted: false, + reason: format!("评估建议 {:?},未达到自动晋升标准", recommendation), + }) + } + } + PromotionPolicy::Manual => { + Ok(PromotionResult { + idea_id: idea.id.clone(), + project_id: String::new(), + promoted: false, + reason: "手动模式,等待人工审批".to_string(), + }) + } + PromotionPolicy::SemiAuto => { + // TODO: AI 评估 + 人工确认流程 + Ok(PromotionResult { + idea_id: idea.id.clone(), + project_id: String::new(), + promoted: false, + reason: "半自动模式,等待确认".to_string(), + }) + } + } + } + + /// 执行晋升操作 + fn do_promote(&self, idea: &Idea) -> Result { + let project_id = df_core::types::new_id(); + // TODO: 调用 df-project 创建项目 + tracing::info!("想法 {} 晋升为项目 {}", idea.id, project_id); + + Ok(PromotionResult { + idea_id: idea.id.clone(), + project_id, + promoted: true, + reason: "评估达标,自动晋升".to_string(), + }) + } +} diff --git a/crates/df-ideas/src/scoring.rs b/crates/df-ideas/src/scoring.rs new file mode 100644 index 0000000..c61e283 --- /dev/null +++ b/crates/df-ideas/src/scoring.rs @@ -0,0 +1,75 @@ +//! 评分引擎 — 多维度加权评分 + +use crate::capture::Idea; + +/// 想法评分详情(重新导出 capture 模块中的定义) +pub use crate::capture::IdeaScores; + +/// 评分权重配置 +#[derive(Debug, Clone)] +pub struct ScoringWeights { + pub feasibility: f64, + pub impact: f64, + pub urgency: f64, +} + +impl Default for ScoringWeights { + fn default() -> Self { + Self { + feasibility: 0.4, + impact: 0.4, + urgency: 0.2, + } + } +} + +/// 评分引擎 +pub struct ScoringEngine; + +impl ScoringEngine { + /// 使用默认权重计算评分 + /// + /// TODO: 接入 AI 进行深度评分,当前返回基于启发式的分数 + pub fn compute_default(idea: &Idea) -> IdeaScores { + let weights = ScoringWeights::default(); + Self::compute(idea, &weights) + } + + /// 使用指定权重计算评分 + pub fn compute(idea: &Idea, weights: &ScoringWeights) -> IdeaScores { + // TODO: 基于想法内容、历史数据、AI 分析等多维度评分 + // 当前使用基于启发式的占位评分 + let feasibility = Self::heuristic_feasibility(idea); + let impact = Self::heuristic_impact(idea); + let urgency = Self::heuristic_urgency(idea); + + let overall = feasibility * weights.feasibility + + impact * weights.impact + + urgency * weights.urgency; + + IdeaScores { + feasibility, + impact, + urgency, + overall, + } + } + + /// 启发式可行性评分 + fn heuristic_feasibility(_idea: &Idea) -> f64 { + // TODO: 基于描述复杂度、资源需求等评估 + 5.0 + } + + /// 启发式影响力评分 + fn heuristic_impact(_idea: &Idea) -> f64 { + // TODO: 基于业务价值、用户影响等评估 + 5.0 + } + + /// 启发式紧急度评分 + fn heuristic_urgency(_idea: &Idea) -> f64 { + // TODO: 基于优先级、时间窗口等评估 + 5.0 + } +} diff --git a/crates/df-nodes/Cargo.toml b/crates/df-nodes/Cargo.toml new file mode 100644 index 0000000..5a9b045 --- /dev/null +++ b/crates/df-nodes/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "df-nodes" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +df-execute = { path = "../df-execute" } +df-workflow = { path = "../df-workflow" } +df-ai = { path = "../df-ai" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +async-trait = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-nodes/src/ai_node.rs b/crates/df-nodes/src/ai_node.rs new file mode 100644 index 0000000..c2a5ecf --- /dev/null +++ b/crates/df-nodes/src/ai_node.rs @@ -0,0 +1,167 @@ +//! AI 节点 — 调用 LLM 完成文本生成/分析任务 +//! +//! 工作流中无人值守的 AI 步骤:从节点 config 读取 OpenAI 兼容 provider 配置与 +//! prompt,自建 client 调用一次 LLM,输出文本供下游节点消费。 +//! +//! 与 AI Chat(侧边栏交互对话)的区别:AI Node 由 DAG Executor 自动驱动, +//! 适合嵌入自动化链路(如 想法 → AI分析 → 脚本落地 → 人工审批)。 + +use async_trait::async_trait; +use df_ai::anthropic_compat::AnthropicCompatProvider; +use df_ai::openai_compat::OpenAICompatProvider; +use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider}; +use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; + +/// AI 节点 +pub struct AiNode; + +#[async_trait] +impl Node for AiNode { + async fn execute(&self, ctx: NodeContext) -> NodeResult { + tracing::info!("AiNode 执行: node_id={}", ctx.node_id); + + // ── provider 配置(必填)── + let base_url = ctx + .config + .get("base_url") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: base_url"))? + .to_string(); + + let api_key = ctx + .config + .get("api_key") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: api_key"))? + .to_string(); + + // ── prompt(必填):优先取上游节点 "prompt" 输出,回退 config.prompt ── + let prompt = ctx + .inputs + .get("prompt") + .and_then(|o| o.data.as_str()) + .map(|s| s.to_string()) + .or_else(|| { + ctx.config + .get("prompt") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }) + .ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: prompt(config 或上游输入均无)"))?; + + // ── 可选参数 ── + let model = ctx + .config + .get("model") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let temperature = ctx + .config + .get("temperature") + .and_then(|v| v.as_f64()) + .map(|f| f as f32); + let max_tokens = ctx + .config + .get("max_tokens") + .and_then(|v| v.as_u64()) + .map(|n| n as u32); + let system_prompt = ctx + .config + .get("system_prompt") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + // 协议类型:默认 openai_compat,可设 anthropic(GLM 订阅端点 / Claude 官方) + let protocol = ctx + .config + .get("protocol") + .and_then(|v| v.as_str()) + .unwrap_or("openai_compat") + .to_string(); + + // default_model:留空时给一个占位,避免 provider 构造 panic + let default_model = if model.is_empty() { + "gpt-4o-mini".to_string() + } else { + model.clone() + }; + + let provider: Box = match protocol.as_str() { + "anthropic" => Box::new(AnthropicCompatProvider::new(&base_url, &api_key, &default_model)), + _ => Box::new(OpenAICompatProvider::new(&base_url, &api_key, &default_model)), + }; + + // ── 构建消息 ── + let mut messages = Vec::with_capacity(2); + if let Some(sys) = system_prompt { + messages.push(ChatMessage::system(sys)); + } + messages.push(ChatMessage::user(prompt)); + + let request = CompletionRequest { + model: model.clone(), + messages, + temperature, + max_tokens, + stream: false, + tools: None, + tool_choice: None, + }; + + tracing::info!( + "AiNode 调用 LLM: model={}, base_url={}", + default_model, + base_url + ); + let response = provider.complete(request).await?; + + tracing::info!( + "AiNode 完成: model={}, prompt_tokens={}, completion_tokens={}", + response.model, + response.usage.prompt_tokens, + response.usage.completion_tokens + ); + + Ok(NodeOutput::from_value(serde_json::json!({ + "text": response.text, + "model": response.model, + "usage": { + "prompt_tokens": response.usage.prompt_tokens, + "completion_tokens": response.usage.completion_tokens, + "total_tokens": response.usage.total_tokens, + }, + }))) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "prompt": { "type": "string", "description": "用户提示词(若无则取上游 prompt 输出)" }, + "system_prompt": { "type": "string", "description": "系统提示词(可选)" }, + "protocol": { "type": "string", "description": "协议类型:openai_compat(默认)或 anthropic(GLM订阅/Claude官方)" }, + "base_url": { "type": "string", "description": "API 地址,OpenAI 兼容如 https://api.deepseek.com;Anthropic 如 https://open.bigmodel.cn/api/anthropic" }, + "api_key": { "type": "string", "description": "API 密钥" }, + "model": { "type": "string", "description": "模型名(可选,留空用默认)" }, + "temperature": { "type": "number", "description": "温度 0.0~2.0(可选)" }, + "max_tokens": { "type": "integer", "description": "最大生成 token(可选,anthropic 协议无值时默认 4096)" } + }, + "required": ["prompt", "base_url", "api_key"] + }), + output: serde_json::json!({ + "type": "object", + "properties": { + "text": { "type": "string" }, + "model": { "type": "string" }, + "usage": { "type": "object" } + } + }), + } + } + + fn node_type(&self) -> &str { + "ai" + } +} diff --git a/crates/df-nodes/src/docker_node.rs b/crates/df-nodes/src/docker_node.rs new file mode 100644 index 0000000..9e04837 --- /dev/null +++ b/crates/df-nodes/src/docker_node.rs @@ -0,0 +1,41 @@ +//! Docker 节点 — 在容器中执行任务 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; + +/// Docker 节点 +pub struct DockerNode; + +#[async_trait] +impl Node for DockerNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 接入 df-execute 的 Docker 执行器 + tracing::info!("DockerNode 执行: 在容器中运行"); + Ok(NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "image": { "type": "string" }, + "command": { "type": "string" }, + "env": { "type": "object" } + }, + "required": ["image"] + }), + output: serde_json::json!({ + "type": "object", + "properties": { + "stdout": { "type": "string" }, + "exit_code": { "type": "integer" } + } + }), + } + } + + fn node_type(&self) -> &str { + "docker" + } +} diff --git a/crates/df-nodes/src/git_node.rs b/crates/df-nodes/src/git_node.rs new file mode 100644 index 0000000..0d86af8 --- /dev/null +++ b/crates/df-nodes/src/git_node.rs @@ -0,0 +1,41 @@ +//! Git 节点 — 执行 Git 操作(克隆、提交、推送、合并等) + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; + +/// Git 节点 +pub struct GitNode; + +#[async_trait] +impl Node for GitNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 接入 df-execute 的 Git 操作 + tracing::info!("GitNode 执行: Git 操作"); + Ok(NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "action": { "type": "string", "enum": ["clone", "commit", "push", "merge", "checkout"] }, + "repo": { "type": "string" }, + "branch": { "type": "string" } + }, + "required": ["action"] + }), + output: serde_json::json!({ + "type": "object", + "properties": { + "success": { "type": "boolean" }, + "message": { "type": "string" } + } + }), + } + } + + fn node_type(&self) -> &str { + "git" + } +} diff --git a/crates/df-nodes/src/http_node.rs b/crates/df-nodes/src/http_node.rs new file mode 100644 index 0000000..1227eb3 --- /dev/null +++ b/crates/df-nodes/src/http_node.rs @@ -0,0 +1,43 @@ +//! HTTP 节点 — 发起 HTTP 请求 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; + +/// HTTP 节点 +pub struct HttpNode; + +#[async_trait] +impl Node for HttpNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 实现HTTP请求逻辑 + tracing::info!("HttpNode 执行: 发送 HTTP 请求"); + Ok(NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "method": { "type": "string", "enum": ["GET", "POST", "PUT", "DELETE"] }, + "url": { "type": "string" }, + "headers": { "type": "object" }, + "body": {} + }, + "required": ["method", "url"] + }), + output: serde_json::json!({ + "type": "object", + "properties": { + "status": { "type": "integer" }, + "body": {}, + "headers": { "type": "object" } + } + }), + } + } + + fn node_type(&self) -> &str { + "http" + } +} diff --git a/crates/df-nodes/src/human_node.rs b/crates/df-nodes/src/human_node.rs new file mode 100644 index 0000000..ed378cd --- /dev/null +++ b/crates/df-nodes/src/human_node.rs @@ -0,0 +1,96 @@ +//! 人工审批节点 — 阻塞工作流,等待人工确认或输入 + +use async_trait::async_trait; +use std::time::Duration; +use tokio::time; +use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; +use df_core::events::WorkflowEvent; + +/// 人工审批节点(阻塞节点) +pub struct HumanNode; + +#[async_trait] +impl Node for HumanNode { + async fn execute(&self, ctx: NodeContext) -> NodeResult { + let config = ctx.config.as_object().cloned().unwrap_or_default(); + let title = config.get("title") + .and_then(|v| v.as_str()) + .unwrap_or("请确认"); + + let description = config.get("description") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let options = config.get("options") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter() + .filter_map(|v| v.as_str()) + .collect::>()) + .unwrap_or_else(|| vec!["同意", "拒绝"]); + + // 发送人工审批请求到事件总线 + let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { + execution_id: ctx.execution_id.clone(), + node_id: ctx.node_id.clone(), + title: title.to_string(), + description: description.to_string(), + options: options.iter().map(|s| s.to_string()).collect(), + }); + + // 等待用户响应(最多等待 1 小时) + let timeout = Duration::from_secs(3600); + let start_time = std::time::Instant::now(); + + loop { + // 检查是否超时 + if start_time.elapsed() > timeout { + return Err(anyhow::anyhow!("人工审批超时")); + } + + // 检查节点是否被取消 + if ctx.node_status.is_cancelled(&ctx.node_id) { + return Err(anyhow::anyhow!("人工审批被取消")); + } + + // TODO: 检查审批响应(需要前端实现) + // 目前直接返回同意 + let output = serde_json::json!({ + "decision": "同意", + "comment": "", + }); + return Ok(NodeOutput::from_value(output)); + } + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "title": { "type": "string" }, + "description": { "type": "string" }, + "options": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["title"] + }), + output: serde_json::json!({ + "type": "object", + "properties": { + "decision": { "type": "string" }, + "comment": { "type": "string" } + } + }), + } + } + + fn is_blocking(&self) -> bool { + true + } + + fn node_type(&self) -> &str { + "human" + } +} diff --git a/crates/df-nodes/src/lib.rs b/crates/df-nodes/src/lib.rs new file mode 100644 index 0000000..efa9908 --- /dev/null +++ b/crates/df-nodes/src/lib.rs @@ -0,0 +1,10 @@ +//! df-nodes: 内置节点集合 — AI、脚本、Docker、Git、人工审批、HTTP、子流程、通知 + +pub mod ai_node; +pub mod docker_node; +pub mod git_node; +pub mod http_node; +pub mod human_node; +pub mod notify_node; +pub mod script_node; +pub mod subflow_node; diff --git a/crates/df-nodes/src/notify_node.rs b/crates/df-nodes/src/notify_node.rs new file mode 100644 index 0000000..15d9105 --- /dev/null +++ b/crates/df-nodes/src/notify_node.rs @@ -0,0 +1,41 @@ +//! 通知节点 — 发送通知(邮件、飞书、钉钉等) + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; + +/// 通知节点 +pub struct NotifyNode; + +#[async_trait] +impl Node for NotifyNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 实现通知发送逻辑 + tracing::info!("NotifyNode 执行: 发送通知"); + Ok(NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "channel": { "type": "string", "enum": ["email", "feishu", "dingtalk", "webhook"] }, + "to": { "type": "string" }, + "title": { "type": "string" }, + "body": { "type": "string" } + }, + "required": ["channel", "to", "body"] + }), + output: serde_json::json!({ + "type": "object", + "properties": { + "success": { "type": "boolean" } + } + }), + } + } + + fn node_type(&self) -> &str { + "notify" + } +} diff --git a/crates/df-nodes/src/script_node.rs b/crates/df-nodes/src/script_node.rs new file mode 100644 index 0000000..7a8eaaf --- /dev/null +++ b/crates/df-nodes/src/script_node.rs @@ -0,0 +1,94 @@ +//! 脚本节点 — 执行 Shell 脚本或自定义命令 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; + +/// 脚本节点 +pub struct ScriptNode; + +#[async_trait] +impl Node for ScriptNode { + async fn execute(&self, ctx: NodeContext) -> NodeResult { + tracing::info!("ScriptNode 执行: {}", ctx.node_id); + + // 从 config 解析 command(必填) + let command = ctx + .config + .get("command") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("ScriptNode 缺少必填参数: command"))?; + + // 解析可选参数 + let timeout_secs = ctx + .config + .get("timeout_secs") + .and_then(|v| v.as_u64()); + + let working_dir = ctx + .config + .get("working_dir") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + // 构建请求并执行 + let request = df_execute::shell::ShellRequest { + command: command.to_string(), + working_dir, + env: std::collections::HashMap::new(), + timeout_secs, + }; + + tracing::info!("ScriptNode 执行命令: {}", command); + let result = df_execute::shell::execute(request).await?; + + // 非零退出码视为执行失败 + let exit_code = result.exit_code.unwrap_or(-1); + if exit_code != 0 { + anyhow::bail!( + "脚本执行失败 (exit_code={}): {}", + exit_code, + result.stderr.trim() + ); + } + + tracing::info!( + "ScriptNode 完成: 耗时 {}ms, exit_code={}", + result.duration_ms, + exit_code + ); + + Ok(NodeOutput::from_value(serde_json::json!({ + "stdout": result.stdout, + "stderr": result.stderr, + "exit_code": exit_code, + "duration_ms": result.duration_ms, + }))) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "command": { "type": "string" }, + "timeout_secs": { "type": "integer" }, + "working_dir": { "type": "string" } + }, + "required": ["command"] + }), + output: serde_json::json!({ + "type": "object", + "properties": { + "stdout": { "type": "string" }, + "stderr": { "type": "string" }, + "exit_code": { "type": "integer" }, + "duration_ms": { "type": "integer" } + } + }), + } + } + + fn node_type(&self) -> &str { + "script" + } +} diff --git a/crates/df-nodes/src/subflow_node.rs b/crates/df-nodes/src/subflow_node.rs new file mode 100644 index 0000000..8222a94 --- /dev/null +++ b/crates/df-nodes/src/subflow_node.rs @@ -0,0 +1,39 @@ +//! 子流程节点 — 嵌套执行另一个工作流 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; + +/// 子流程节点 +pub struct SubflowNode; + +#[async_trait] +impl Node for SubflowNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 实现子工作流加载与执行 + tracing::info!("SubflowNode 执行: 启动子工作流"); + Ok(NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "workflow_id": { "type": "string" }, + "inputs": { "type": "object" } + }, + "required": ["workflow_id"] + }), + output: serde_json::json!({ + "type": "object", + "properties": { + "outputs": { "type": "object" } + } + }), + } + } + + fn node_type(&self) -> &str { + "subflow" + } +} diff --git a/crates/df-plugin/Cargo.toml b/crates/df-plugin/Cargo.toml new file mode 100644 index 0000000..95dabbf --- /dev/null +++ b/crates/df-plugin/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "df-plugin" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +df-workflow = { path = "../df-workflow" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +async-trait = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-plugin/src/host.rs b/crates/df-plugin/src/host.rs new file mode 100644 index 0000000..9ab368c --- /dev/null +++ b/crates/df-plugin/src/host.rs @@ -0,0 +1,66 @@ +//! 插件宿主环境 — 管理插件生命周期与沙箱 + +use std::collections::HashMap; + +use df_core::types::PluginId; +use df_workflow::node::Node; + +use crate::loader::{LoadedPlugin, PluginLoader, PluginMetadata}; + +/// 插件宿主环境 +pub struct PluginHost { + /// 插件加载器 + loader: PluginLoader, + /// 已加载的插件 + plugins: HashMap, +} + +impl PluginHost { + /// 创建宿主环境 + pub fn new(loader: PluginLoader) -> Self { + Self { + loader, + plugins: HashMap::new(), + } + } + + /// 初始化:加载所有插件 + pub async fn initialize(&mut self) -> anyhow::Result<()> { + self.plugins = self.loader.load_all()?; + tracing::info!("插件宿主环境初始化完成,加载了 {} 个插件", self.plugins.len()); + Ok(()) + } + + /// 获取所有已注册的节点 + pub fn all_nodes(&self) -> Vec<(&PluginId, &Box)> { + let mut nodes = Vec::new(); + for (plugin_id, plugin) in &self.plugins { + for node in &plugin.nodes { + nodes.push((plugin_id, node)); + } + } + nodes + } + + /// 获取指定插件的元数据 + pub fn get_metadata(&self, plugin_id: &PluginId) -> Option<&PluginMetadata> { + self.plugins.get(plugin_id).map(|p| &p.metadata) + } + + /// 卸载指定插件 + /// + /// TODO: 实现插件卸载(清理资源、取消注册节点等) + pub fn unload(&mut self, plugin_id: &PluginId) -> anyhow::Result<()> { + if let Some(plugin) = self.plugins.remove(plugin_id) { + tracing::info!("插件 {} 已卸载", plugin.metadata.name); + } + Ok(()) + } + + /// 关闭宿主环境,卸载所有插件 + pub fn shutdown(&mut self) { + let count = self.plugins.len(); + self.plugins.clear(); + tracing::info!("插件宿主环境关闭,卸载了 {} 个插件", count); + } +} diff --git a/crates/df-plugin/src/lib.rs b/crates/df-plugin/src/lib.rs new file mode 100644 index 0000000..c5da26c --- /dev/null +++ b/crates/df-plugin/src/lib.rs @@ -0,0 +1,5 @@ +//! df-plugin: 插件系统 — 插件加载、宿主环境、SDK + +pub mod host; +pub mod loader; +pub mod sdk; diff --git a/crates/df-plugin/src/loader.rs b/crates/df-plugin/src/loader.rs new file mode 100644 index 0000000..1c4ae65 --- /dev/null +++ b/crates/df-plugin/src/loader.rs @@ -0,0 +1,94 @@ +//! 插件加载器 — 发现、加载、注册插件 + +use std::collections::HashMap; +use std::path::PathBuf; + +use df_core::types::PluginId; +use df_workflow::node::Node; + +/// 插件元数据 +#[derive(Debug, Clone)] +pub struct PluginMetadata { + /// 插件 ID + pub id: PluginId, + /// 插件名称 + pub name: String, + /// 版本 + pub version: String, + /// 描述 + pub description: String, + /// 作者 + pub author: Option, + /// 入口文件路径 + pub entry_path: PathBuf, +} + +/// 已加载的插件 +pub struct LoadedPlugin { + pub metadata: PluginMetadata, + pub nodes: Vec>, +} + +/// 插件加载器 +pub struct PluginLoader { + /// 插件搜索目录 + search_dirs: Vec, +} + +impl PluginLoader { + /// 创建插件加载器 + pub fn new() -> Self { + Self { + search_dirs: Vec::new(), + } + } + + /// 添加插件搜索目录 + pub fn add_search_dir(&mut self, dir: PathBuf) { + self.search_dirs.push(dir); + } + + /// 扫描并发现所有可用插件 + /// + /// TODO: 实现文件系统扫描、manifest 解析 + pub fn discover(&self) -> anyhow::Result> { + tracing::info!("扫描插件目录: {:?}", self.search_dirs); + // TODO: 实现插件发现逻辑 + Ok(Vec::new()) + } + + /// 加载指定插件 + /// + /// TODO: 实现动态库加载(dlopen/LoadLibrary)或 WASM 加载 + pub fn load(&self, _metadata: &PluginMetadata) -> anyhow::Result { + // TODO: 实现插件加载 + tracing::warn!("插件加载尚未实现"); + Err(anyhow::anyhow!("插件加载尚未实现")) + } + + /// 批量加载所有发现的插件 + pub fn load_all(&self) -> anyhow::Result> { + let metadata_list = self.discover()?; + let mut loaded = HashMap::new(); + + for meta in metadata_list { + match self.load(&meta) { + Ok(plugin) => { + tracing::info!("插件 {} v{} 加载成功", meta.name, meta.version); + loaded.insert(meta.id.clone(), plugin); + } + Err(e) => { + tracing::error!("插件 {} 加载失败: {}", meta.name, e); + } + } + } + + Ok(loaded) + } +} + +impl Default for PluginLoader { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/df-plugin/src/sdk.rs b/crates/df-plugin/src/sdk.rs new file mode 100644 index 0000000..42f8974 --- /dev/null +++ b/crates/df-plugin/src/sdk.rs @@ -0,0 +1,90 @@ +//! 插件 SDK — 插件开发者使用的工具与trait + +use async_trait::async_trait; +use df_workflow::node::Node; + +/// 插件入口 trait — 所有插件必须实现 +#[async_trait] +pub trait Plugin: Send + Sync { + /// 插件 ID + fn id(&self) -> &str; + + /// 插件名称 + fn name(&self) -> &str; + + /// 插件版本 + fn version(&self) -> &str; + + /// 注册的所有节点 + fn nodes(&self) -> Vec>; + + /// 插件初始化(可选) + async fn initialize(&self) -> anyhow::Result<()> { + Ok(()) + } + + /// 插件销毁(可选) + async fn shutdown(&self) -> anyhow::Result<()> { + Ok(()) + } +} + +/// 插件构建器 — 简化插件定义 +pub struct PluginBuilder { + id: String, + name: String, + version: String, + nodes: Vec>, +} + +impl PluginBuilder { + /// 创建插件构建器 + pub fn new(id: impl Into, name: impl Into, version: impl Into) -> Self { + Self { + id: id.into(), + name: name.into(), + version: version.into(), + nodes: Vec::new(), + } + } + + /// 注册节点 + pub fn node(mut self, node: Box) -> Self { + self.nodes.push(node); + self + } + + /// 获取插件 ID + pub fn id(&self) -> &str { + &self.id + } + + /// 获取插件名称 + pub fn name(&self) -> &str { + &self.name + } + + /// 获取插件版本 + pub fn version(&self) -> &str { + &self.version + } + + /// 构建并获取所有节点 + pub fn build(self) -> Vec> { + self.nodes + } +} + +/// 声明插件的宏(简化版) +/// +/// TODO: 实现完整的声明宏 +/// 使用示例: +/// ``` +/// declare_plugin!("my-plugin", "My Plugin", "0.1.0"); +/// ``` +#[macro_export] +macro_rules! declare_plugin { + ($id:expr, $name:expr, $version:expr) => { + // TODO: 实现宏 + }; +} diff --git a/crates/df-project/Cargo.toml b/crates/df-project/Cargo.toml new file mode 100644 index 0000000..58c1c4b --- /dev/null +++ b/crates/df-project/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "df-project" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +anyhow = { workspace = true } +chrono = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-project/src/context.rs b/crates/df-project/src/context.rs new file mode 100644 index 0000000..5603689 --- /dev/null +++ b/crates/df-project/src/context.rs @@ -0,0 +1,52 @@ +//! 项目上下文 — 项目运行时的环境与配置信息 + +use serde::{Deserialize, Serialize}; + +use df_core::types::ProjectId; + +/// 项目上下文 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectContext { + /// 项目 ID + pub project_id: ProjectId, + /// 项目根目录(本地文件系统路径) + pub root_path: Option, + /// Git 仓库 URL + pub repo_url: Option, + /// 当前分支 + pub current_branch: Option, + /// 环境变量 + pub env_vars: std::collections::HashMap, + /// 技术栈 + pub tech_stack: Vec, + /// AI 上下文(项目相关的 AI 记忆) + pub ai_context: Option, +} + +/// AI 上下文信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AiContext { + /// 项目摘要 + pub summary: String, + /// 架构描述 + pub architecture: Option, + /// 关键决策记录 + pub decisions: Vec, + /// 最近修改摘要 + pub recent_changes: Vec, +} + +impl ProjectContext { + /// 创建空上下文 + pub fn new(project_id: ProjectId) -> Self { + Self { + project_id, + root_path: None, + repo_url: None, + current_branch: None, + env_vars: std::collections::HashMap::new(), + tech_stack: Vec::new(), + ai_context: None, + } + } +} diff --git a/crates/df-project/src/lib.rs b/crates/df-project/src/lib.rs new file mode 100644 index 0000000..7f532c1 --- /dev/null +++ b/crates/df-project/src/lib.rs @@ -0,0 +1,6 @@ +//! df-project: 项目管理 — 项目创建、调度、上下文、时间线 + +pub mod context; +pub mod manager; +pub mod scheduler; +pub mod timeline; diff --git a/crates/df-project/src/manager.rs b/crates/df-project/src/manager.rs new file mode 100644 index 0000000..c057ee6 --- /dev/null +++ b/crates/df-project/src/manager.rs @@ -0,0 +1,84 @@ +//! 项目管理器 — 项目的 CRUD 与生命周期管理 + +use serde::{Deserialize, Serialize}; + +use df_core::types::{IdeaId, Priority, ProjectId, ProjectStatus}; + +/// 项目实体 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + /// 唯一 ID + pub id: ProjectId, + /// 项目名称 + pub name: String, + /// 项目描述 + pub description: String, + /// 当前状态 + pub status: ProjectStatus, + /// 来源想法 ID(可选,如果是从想法晋升来的) + pub idea_id: Option, + /// 优先级 + pub priority: Priority, + /// 标签 + pub tags: Vec, + /// 创建时间 + pub created_at: chrono::DateTime, + /// 更新时间 + pub updated_at: chrono::DateTime, +} + +/// 创建项目的输入 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateProjectInput { + pub name: String, + pub description: String, + pub idea_id: Option, + #[serde(default)] + pub priority: Priority, + #[serde(default)] + pub tags: Vec, +} + +/// 项目管理器 +pub struct ProjectManager; + +impl ProjectManager { + /// 创建新项目 + /// + /// TODO: 接入存储层持久化 + pub fn create(input: CreateProjectInput) -> Project { + let now = chrono::Utc::now(); + Project { + id: df_core::types::new_id(), + name: input.name, + description: input.description, + status: ProjectStatus::Planning, + idea_id: input.idea_id, + priority: input.priority, + tags: input.tags, + created_at: now, + updated_at: now, + } + } + + /// 从想法创建项目 + pub fn create_from_idea(name: String, description: String, idea_id: IdeaId) -> Project { + Self::create(CreateProjectInput { + name, + description, + idea_id: Some(idea_id), + priority: Priority::default(), + tags: Vec::new(), + }) + } + + /// 更新项目状态 + /// + /// TODO: 状态转换校验 + pub fn transition_status(project: &mut Project, new_status: ProjectStatus) -> anyhow::Result<()> { + // TODO: 校验状态转换是否合法 + project.status = new_status; + project.updated_at = chrono::Utc::now(); + Ok(()) + } +} diff --git a/crates/df-project/src/scheduler.rs b/crates/df-project/src/scheduler.rs new file mode 100644 index 0000000..d90ac7a --- /dev/null +++ b/crates/df-project/src/scheduler.rs @@ -0,0 +1,66 @@ +//! 项目调度器 — 自动化任务分配与调度 + +use df_core::types::{ProjectId, TaskId}; + +/// 调度策略 +#[derive(Debug, Clone, Copy)] +pub enum SchedulingStrategy { + /// FIFO(先进先出) + Fifo, + /// 按优先级调度 + Priority, + /// 最短任务优先 + ShortestFirst, + /// AI 智能调度 + AiDriven, +} + +/// 调度决策 +#[derive(Debug, Clone)] +pub struct SchedulingDecision { + pub project_id: ProjectId, + pub task_order: Vec, + pub strategy: SchedulingStrategy, +} + +/// 项目调度器 +pub struct ProjectScheduler { + strategy: SchedulingStrategy, +} + +impl ProjectScheduler { + /// 创建调度器 + pub fn new(strategy: SchedulingStrategy) -> Self { + Self { strategy } + } + + /// 为项目生成调度计划 + /// + /// TODO: 实现基于策略的调度算法 + pub fn schedule(&self, project_id: &ProjectId) -> anyhow::Result { + match self.strategy { + SchedulingStrategy::Fifo => { + // TODO: 按创建时间排序 + tracing::info!("FIFO 调度,项目: {}", project_id); + } + SchedulingStrategy::Priority => { + // TODO: 按优先级排序 + tracing::info!("优先级调度,项目: {}", project_id); + } + SchedulingStrategy::ShortestFirst => { + // TODO: 按预估工作量排序 + tracing::info!("最短任务优先调度,项目: {}", project_id); + } + SchedulingStrategy::AiDriven => { + // TODO: 接入 AI 进行智能调度 + tracing::info!("AI 智能调度,项目: {}", project_id); + } + } + + Ok(SchedulingDecision { + project_id: project_id.clone(), + task_order: Vec::new(), + strategy: self.strategy, + }) + } +} diff --git a/crates/df-project/src/timeline.rs b/crates/df-project/src/timeline.rs new file mode 100644 index 0000000..3ed1877 --- /dev/null +++ b/crates/df-project/src/timeline.rs @@ -0,0 +1,64 @@ +//! 项目时间线 — 里程碑与进度追踪 + +use serde::{Deserialize, Serialize}; + +use df_core::types::ProjectId; + +/// 里程碑 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Milestone { + /// 唯一 ID + pub id: String, + /// 所属项目 ID + pub project_id: ProjectId, + /// 里程碑名称 + pub name: String, + /// 描述 + pub description: String, + /// 计划完成时间 + pub due_date: Option>, + /// 实际完成时间 + pub completed_at: Option>, + /// 进度百分比 (0-100) + pub progress: u8, + /// 是否已完成 + pub completed: bool, +} + +/// 项目时间线 +pub struct Timeline { + pub project_id: ProjectId, + pub milestones: Vec, +} + +impl Timeline { + /// 创建空时间线 + pub fn new(project_id: ProjectId) -> Self { + Self { + project_id, + milestones: Vec::new(), + } + } + + /// 添加里程碑 + pub fn add_milestone(&mut self, milestone: Milestone) { + self.milestones.push(milestone); + } + + /// 计算整体进度 + pub fn overall_progress(&self) -> f64 { + if self.milestones.is_empty() { + return 0.0; + } + let total: f64 = self.milestones.iter().map(|m| m.progress as f64).sum(); + total / self.milestones.len() as f64 + } + + /// 获取下一个待完成的里程碑 + pub fn next_milestone(&self) -> Option<&Milestone> { + self.milestones + .iter() + .filter(|m| !m.completed) + .min_by_key(|m| m.due_date) + } +} diff --git a/crates/df-stages/Cargo.toml b/crates/df-stages/Cargo.toml new file mode 100644 index 0000000..a4f6d7a --- /dev/null +++ b/crates/df-stages/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "df-stages" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +df-workflow = { path = "../df-workflow" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +async-trait = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-stages/src/coding.rs b/crates/df-stages/src/coding.rs new file mode 100644 index 0000000..3da648f --- /dev/null +++ b/crates/df-stages/src/coding.rs @@ -0,0 +1,50 @@ +//! 编码阶段 — 代码生成与审查阶段模板 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema}; + +/// 代码生成节点 +pub struct CodeGenNode; + +#[async_trait] +impl Node for CodeGenNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 AI 生成代码 + tracing::info!("代码生成节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.coding.codegen" + } +} + +/// 代码审查节点 +pub struct CodeReviewNode; + +#[async_trait] +impl Node for CodeReviewNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 AI 审查代码 + tracing::info!("代码审查节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.coding.review" + } +} diff --git a/crates/df-stages/src/idea.rs b/crates/df-stages/src/idea.rs new file mode 100644 index 0000000..c98e1b9 --- /dev/null +++ b/crates/df-stages/src/idea.rs @@ -0,0 +1,57 @@ +//! 想法阶段 — 想法捕获与评估阶段模板 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema}; + +/// 想法捕获节点 +pub struct IdeaCaptureNode; + +#[async_trait] +impl Node for IdeaCaptureNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 df-ideas 的捕获功能 + tracing::info!("想法捕获节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({ + "type": "object", + "properties": { + "title": { "type": "string" }, + "description": { "type": "string" } + }, + "required": ["title"] + }), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.idea.capture" + } +} + +/// 想法评估节点 +pub struct IdeaEvalNode; + +#[async_trait] +impl Node for IdeaEvalNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 df-ideas 的评估功能 + tracing::info!("想法评估节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.idea.eval" + } +} diff --git a/crates/df-stages/src/lib.rs b/crates/df-stages/src/lib.rs new file mode 100644 index 0000000..3acbfcb --- /dev/null +++ b/crates/df-stages/src/lib.rs @@ -0,0 +1,7 @@ +//! df-stages: 阶段插件 — 开发流程各阶段的节点模板注册 + +pub mod coding; +pub mod idea; +pub mod release; +pub mod requirement; +pub mod testing; diff --git a/crates/df-stages/src/release.rs b/crates/df-stages/src/release.rs new file mode 100644 index 0000000..94510e2 --- /dev/null +++ b/crates/df-stages/src/release.rs @@ -0,0 +1,73 @@ +//! 发布阶段 — 构建、部署与发布阶段模板 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema}; + +/// 构建节点 +pub struct BuildNode; + +#[async_trait] +impl Node for BuildNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 df-execute 执行构建命令 + tracing::info!("构建节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.release.build" + } +} + +/// 部署节点 +pub struct DeployNode; + +#[async_trait] +impl Node for DeployNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 df-execute 执行部署命令 + tracing::info!("部署节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.release.deploy" + } +} + +/// 发布公告节点 +pub struct ReleaseNotesNode; + +#[async_trait] +impl Node for ReleaseNotesNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 AI 生成发布公告 + tracing::info!("发布公告节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.release.notes" + } +} diff --git a/crates/df-stages/src/requirement.rs b/crates/df-stages/src/requirement.rs new file mode 100644 index 0000000..9b45d7c --- /dev/null +++ b/crates/df-stages/src/requirement.rs @@ -0,0 +1,50 @@ +//! 需求阶段 — 需求分析与文档生成阶段模板 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema}; + +/// 需求分析节点 +pub struct RequirementAnalysisNode; + +#[async_trait] +impl Node for RequirementAnalysisNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 AI 分析需求 + tracing::info!("需求分析节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.requirement.analysis" + } +} + +/// 需求文档生成节点 +pub struct RequirementDocNode; + +#[async_trait] +impl Node for RequirementDocNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 AI 生成需求文档 + tracing::info!("需求文档生成节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.requirement.doc" + } +} diff --git a/crates/df-stages/src/testing.rs b/crates/df-stages/src/testing.rs new file mode 100644 index 0000000..e9961af --- /dev/null +++ b/crates/df-stages/src/testing.rs @@ -0,0 +1,50 @@ +//! 测试阶段 — 测试生成与执行阶段模板 + +use async_trait::async_trait; +use df_workflow::node::{Node, NodeContext, NodeResult, NodeSchema}; + +/// 测试用例生成节点 +pub struct TestGenNode; + +#[async_trait] +impl Node for TestGenNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 AI 生成测试用例 + tracing::info!("测试用例生成节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.testing.testgen" + } +} + +/// 测试执行节点 +pub struct TestRunNode; + +#[async_trait] +impl Node for TestRunNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + // TODO: 调用 df-execute 执行测试命令 + tracing::info!("测试执行节点执行"); + Ok(df_workflow::node::NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::json!({"type": "object"}), + output: serde_json::json!({"type": "object"}), + } + } + + fn node_type(&self) -> &str { + "stage.testing.run" + } +} diff --git a/crates/df-storage/Cargo.toml b/crates/df-storage/Cargo.toml new file mode 100644 index 0000000..8938140 --- /dev/null +++ b/crates/df-storage/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "df-storage" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +rusqlite = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-storage/src/crud.rs b/crates/df-storage/src/crud.rs new file mode 100644 index 0000000..75ce6ad --- /dev/null +++ b/crates/df-storage/src/crud.rs @@ -0,0 +1,614 @@ +//! Repository 模式的 CRUD 操作 +//! +//! 为 7 张表各提供一个 Repo 结构体,统一实现 insert / get_by_id / list_all / query / update_field / delete。 + +use std::sync::Arc; + +use rusqlite::{params, Connection, OptionalExtension, Row}; +use tokio::sync::Mutex; + +use df_core::error::{Error, Result}; + +use crate::db::Database; +use crate::models::{ + AiConversationRecord, AiProviderRecord, AiToolExecutionRecord, BranchRecord, IdeaRecord, + NodeExecutionRecord, ProjectRecord, ReleaseRecord, TaskRecord, WorkflowRecord, +}; + +// ============================================================ +// 辅助宏 — 消除 6 个 Repo 的重复样板 +// ============================================================ + +/// 一次性为 7 张表生成完整的 Repo 结构体及其 CRUD 实现。 +/// +/// 用法: `impl_repo!(RepoName, ModelType, "table_name", from_row_body, insert_body);` +macro_rules! impl_repo { + ( + $(#[$meta:meta])* + $name:ident, $record:ident, $table:literal, + from_row => |$row:ident| $from_body:expr, + insert => |$conn:ident, $rec:ident| $insert_body:expr + ) => { + $(#[$meta])* + pub struct $name { + conn: Arc>, + } + + impl $name { + pub fn new(db: &Database) -> Self { + Self { conn: db.conn() } + } + + pub async fn insert(&self, record: $record) -> Result { + let conn = self.conn.clone(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let id = record.id.clone(); + let $conn = &*guard; + let $rec = &record; + $insert_body.map_err(|e: rusqlite::Error| Error::Storage(e.to_string()))?; + Ok(id) + }) + .await + .map_err(|e| Error::Storage(e.to_string()))? + } + + pub async fn get_by_id(&self, id: &str) -> Result> { + let conn = self.conn.clone(); + let id = id.to_owned(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let mut stmt = guard + .prepare(&format!("SELECT * FROM {} WHERE id = ?1", $table)) + .map_err(|e| Error::Storage(e.to_string()))?; + let row = stmt + .query_row(params![id], |$row| $from_body) + .optional() + .map_err(|e| Error::Storage(e.to_string()))?; + Ok(row) + }) + .await + .map_err(|e| Error::Storage(e.to_string()))? + } + + pub async fn list_all(&self) -> Result> { + let conn = self.conn.clone(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let mut stmt = guard + .prepare(&format!("SELECT * FROM {} ORDER BY created_at DESC", $table)) + .map_err(|e| Error::Storage(e.to_string()))?; + let rows = stmt + .query_map([], |$row| $from_body) + .map_err(|e| Error::Storage(e.to_string()))?; + let mut results = Vec::new(); + for r in rows { + results.push( + r.map_err(|e| Error::Storage(e.to_string()))?, + ); + } + Ok(results) + }) + .await + .map_err(|e| Error::Storage(e.to_string()))? + } + + pub async fn query(&self, field: &str, value: &str) -> Result> { + // 白名单校验,防止 SQL 注入 + validate_column_name(field)?; + let conn = self.conn.clone(); + let sql = format!("SELECT * FROM {} WHERE {} = ?1 ORDER BY created_at DESC", $table, field); + let value = value.to_owned(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let mut stmt = guard + .prepare(&sql) + .map_err(|e| Error::Storage(e.to_string()))?; + let rows = stmt + .query_map(params![value], |$row| $from_body) + .map_err(|e| Error::Storage(e.to_string()))?; + let mut results = Vec::new(); + for r in rows { + results.push( + r.map_err(|e| Error::Storage(e.to_string()))?, + ); + } + Ok(results) + }) + .await + .map_err(|e| Error::Storage(e.to_string()))? + } + + pub async fn update_field(&self, id: &str, field: &str, value: &str) -> Result { + validate_column_name(field)?; + let conn = self.conn.clone(); + let sql = format!("UPDATE {} SET {} = ?1, updated_at = ?2 WHERE id = ?3", $table, field); + let id = id.to_owned(); + let value = value.to_owned(); + let now = now_iso(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let affected = guard + .execute(&sql, params![value, now, id]) + .map_err(|e| Error::Storage(e.to_string()))?; + Ok(affected > 0) + }) + .await + .map_err(|e| Error::Storage(e.to_string()))? + } + + pub async fn delete(&self, id: &str) -> Result { + let conn = self.conn.clone(); + let id = id.to_owned(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let affected = guard + .execute(&format!("DELETE FROM {} WHERE id = ?1", $table), params![id]) + .map_err(|e| Error::Storage(e.to_string()))?; + Ok(affected > 0) + }) + .await + .map_err(|e| Error::Storage(e.to_string()))? + } + } + }; +} + +// ============================================================ +// 列名白名单 — 防止 SQL 注入 +// ============================================================ + +/// 所有表允许用于 query / update_field 的列名 +const ALLOWED_COLUMNS: &[&str] = &[ + // ideas + "id", + "title", + "description", + "status", + "priority", + "score", + "tags", + "source", + "promoted_to", + "ai_analysis", + "scores", + "created_at", + "updated_at", + // projects / tasks 额外 + "name", + "idea_id", + "project_id", + "branch_name", + "assignee", + "workflow_def_id", + "base_branch", + // branches 额外 + "task_id", + "base", + "merged_at", + // releases 额外 + "version", + "task_ids", + "changelog", + "released_at", + // workflow / node 额外 + "dag_json", + "triggered_by", + "completed_at", + "workflow_id", + "node_id", + "node_type", + "input_json", + "output_json", + "error_message", + "started_at", + // AI 相关 + "provider_type", + "api_key", + "base_url", + "default_model", + "models", + "is_default", + "config", + "conversation_id", + "tool_call_id", + "tool_name", + "arguments", + "result", + "risk_level", + "requested_at", + "executed_at", + "decided_by", +]; + +fn validate_column_name(field: &str) -> Result<()> { + if ALLOWED_COLUMNS.contains(&field) { + Ok(()) + } else { + Err(Error::Storage(format!("不允许的字段名: {}", field))) + } +} + +// ============================================================ +// 时间工具 +// ============================================================ + +fn now_iso() -> String { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + .to_string() +} + +// ============================================================ +// from_row 辅助函数 +// ============================================================ + +fn idea_from_row(row: &Row<'_>) -> std::result::Result { + Ok(IdeaRecord { + id: row.get("id")?, + title: row.get("title")?, + description: row.get("description")?, + status: row.get("status")?, + priority: row.get("priority")?, + score: row.get("score")?, + tags: row.get("tags")?, + source: row.get("source")?, + promoted_to: row.get("promoted_to")?, + ai_analysis: row.get("ai_analysis")?, + scores: row.get("scores")?, + created_at: row.get("created_at")?, + updated_at: row.get("updated_at")?, + }) +} + +fn project_from_row(row: &Row<'_>) -> std::result::Result { + Ok(ProjectRecord { + id: row.get("id")?, + name: row.get("name")?, + description: row.get("description")?, + status: row.get("status")?, + idea_id: row.get("idea_id")?, + created_at: row.get("created_at")?, + updated_at: row.get("updated_at")?, + }) +} + +fn task_from_row(row: &Row<'_>) -> std::result::Result { + Ok(TaskRecord { + id: row.get("id")?, + project_id: row.get("project_id")?, + title: row.get("title")?, + description: row.get("description")?, + status: row.get("status")?, + priority: row.get("priority")?, + branch_name: row.get("branch_name")?, + assignee: row.get("assignee")?, + workflow_def_id: row.get("workflow_def_id")?, + base_branch: row.get("base_branch")?, + created_at: row.get("created_at")?, + updated_at: row.get("updated_at")?, + }) +} + +fn release_from_row(row: &Row<'_>) -> std::result::Result { + Ok(ReleaseRecord { + id: row.get("id")?, + project_id: row.get("project_id")?, + version: row.get("version")?, + status: row.get("status")?, + task_ids: row.get("task_ids")?, + changelog: row.get("changelog")?, + created_at: row.get("created_at")?, + released_at: row.get("released_at")?, + }) +} + +fn workflow_from_row(row: &Row<'_>) -> std::result::Result { + Ok(WorkflowRecord { + id: row.get("id")?, + name: row.get("name")?, + dag_json: row.get("dag_json")?, + status: row.get("status")?, + triggered_by: row.get("triggered_by")?, + project_id: row.get("project_id")?, + task_id: row.get("task_id")?, + created_at: row.get("created_at")?, + completed_at: row.get("completed_at")?, + }) +} + +fn branch_from_row(row: &Row<'_>) -> std::result::Result { + Ok(BranchRecord { + id: row.get("id")?, + project_id: row.get("project_id")?, + task_id: row.get("task_id")?, + name: row.get("name")?, + base: row.get("base")?, + status: row.get("status")?, + created_at: row.get("created_at")?, + updated_at: row.get("updated_at")?, + merged_at: row.get("merged_at")?, + }) +} + +fn node_execution_from_row( + row: &Row<'_>, +) -> std::result::Result { + Ok(NodeExecutionRecord { + id: row.get("id")?, + workflow_id: row.get("workflow_id")?, + node_id: row.get("node_id")?, + node_type: row.get("node_type")?, + status: row.get("status")?, + input_json: row.get("input_json")?, + output_json: row.get("output_json")?, + error_message: row.get("error_message")?, + started_at: row.get("started_at")?, + completed_at: row.get("completed_at")?, + }) +} + +// ============================================================ +// 7 个 Repo 实现 +// ============================================================ + +impl_repo!( + /// 想法表 CRUD + IdeaRepo, + IdeaRecord, + "ideas", + from_row => |row| idea_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO ideas (id, title, description, status, priority, score, tags, source, promoted_to, ai_analysis, scores, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + params![ + rec.id, rec.title, rec.description, rec.status, rec.priority, + rec.score, rec.tags, rec.source, rec.promoted_to, rec.ai_analysis, + rec.scores, rec.created_at, rec.updated_at + ], + ) + } +); + +impl_repo!( + /// 项目表 CRUD + ProjectRepo, + ProjectRecord, + "projects", + from_row => |row| project_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO projects (id, name, description, status, idea_id, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + rec.id, rec.name, rec.description, rec.status, rec.idea_id, + rec.created_at, rec.updated_at + ], + ) + } +); + +impl_repo!( + /// 任务表 CRUD + TaskRepo, + TaskRecord, + "tasks", + from_row => |row| task_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + params![ + rec.id, rec.project_id, rec.title, rec.description, rec.status, rec.priority, + rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch, + rec.created_at, rec.updated_at + ], + ) + } +); + +impl_repo!( + /// 分支表 CRUD + BranchRepo, + BranchRecord, + "branches", + from_row => |row| branch_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO branches (id, project_id, task_id, name, base, status, created_at, updated_at, merged_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + params![ + rec.id, rec.project_id, rec.task_id, rec.name, rec.base, + rec.status, rec.created_at, rec.updated_at, rec.merged_at + ], + ) + } +); + +impl_repo!( + /// 发布表 CRUD + ReleaseRepo, + ReleaseRecord, + "releases", + from_row => |row| release_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO releases (id, project_id, version, status, task_ids, changelog, created_at, released_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + rec.id, rec.project_id, rec.version, rec.status, rec.task_ids, + rec.changelog, rec.created_at, rec.released_at + ], + ) + } +); + +impl_repo!( + /// 工作流执行表 CRUD + WorkflowRepo, + WorkflowRecord, + "workflow_executions", + from_row => |row| workflow_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO workflow_executions (id, name, dag_json, status, triggered_by, project_id, task_id, created_at, completed_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + params![ + rec.id, rec.name, rec.dag_json, rec.status, rec.triggered_by, + rec.project_id, rec.task_id, rec.created_at, rec.completed_at + ], + ) + } +); + +impl_repo!( + /// 节点执行表 CRUD + NodeExecutionRepo, + NodeExecutionRecord, + "node_executions", + from_row => |row| node_execution_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO node_executions (id, workflow_id, node_id, node_type, status, input_json, output_json, error_message, started_at, completed_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + params![ + rec.id, rec.workflow_id, rec.node_id, rec.node_type, rec.status, + rec.input_json, rec.output_json, rec.error_message, rec.started_at, rec.completed_at + ], + ) + } +); + +// ============================================================ +// AI 相关 Repo (V3) +// ============================================================ + +fn ai_provider_from_row(row: &Row<'_>) -> std::result::Result { + Ok(AiProviderRecord { + id: row.get("id")?, + name: row.get("name")?, + provider_type: row.get("provider_type")?, + api_key: row.get("api_key")?, + base_url: row.get("base_url")?, + default_model: row.get("default_model")?, + models: row.get("models")?, + is_default: row.get::<_, i32>("is_default")? != 0, + config: row.get("config")?, + created_at: row.get("created_at")?, + updated_at: row.get("updated_at")?, + }) +} + +fn ai_conversation_from_row(row: &Row<'_>) -> std::result::Result { + Ok(AiConversationRecord { + id: row.get("id")?, + title: row.get("title")?, + messages: row.get("messages")?, + provider_id: row.get("provider_id")?, + model: row.get("model")?, + created_at: row.get("created_at")?, + updated_at: row.get("updated_at")?, + }) +} + +fn ai_tool_execution_from_row(row: &Row<'_>) -> std::result::Result { + Ok(AiToolExecutionRecord { + id: row.get("id")?, + conversation_id: row.get("conversation_id")?, + tool_call_id: row.get("tool_call_id")?, + tool_name: row.get("tool_name")?, + arguments: row.get("arguments")?, + result: row.get("result")?, + status: row.get("status")?, + risk_level: row.get("risk_level")?, + requested_at: row.get("requested_at")?, + executed_at: row.get("executed_at")?, + decided_by: row.get("decided_by")?, + }) +} + +impl_repo!( + /// AI 提供商配置表 CRUD + AiProviderRepo, + AiProviderRecord, + "ai_providers", + from_row => |row| ai_provider_from_row(row), + insert => |conn, rec| { + let is_default = if rec.is_default { 1i32 } else { 0i32 }; + conn.execute( + "INSERT OR REPLACE INTO ai_providers (id, name, provider_type, api_key, base_url, default_model, models, is_default, config, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![ + rec.id, rec.name, rec.provider_type, rec.api_key, rec.base_url, + rec.default_model, rec.models, is_default, rec.config, rec.created_at, rec.updated_at + ], + ) + } +); + +impl_repo!( + /// AI 对话历史表 CRUD + AiConversationRepo, + AiConversationRecord, + "ai_conversations", + from_row => |row| ai_conversation_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO ai_conversations (id, title, messages, provider_id, model, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + rec.id, rec.title, rec.messages, rec.provider_id, rec.model, + rec.created_at, rec.updated_at + ], + ) + } +); + +impl_repo!( + /// AI 工具执行审计表 CRUD + AiToolExecutionRepo, + AiToolExecutionRecord, + "ai_tool_executions", + from_row => |row| ai_tool_execution_from_row(row), + insert => |conn, rec| { + conn.execute( + "INSERT INTO ai_tool_executions (id, conversation_id, tool_call_id, tool_name, arguments, result, status, risk_level, requested_at, executed_at, decided_by) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![ + rec.id, rec.conversation_id, rec.tool_call_id, rec.tool_name, + rec.arguments, rec.result, rec.status, rec.risk_level, + rec.requested_at, rec.executed_at, rec.decided_by + ], + ) + } +); + +// ============================================================ +// AiConversationRepo 扩展方法(绕过 update_field 白名单限制) +// ============================================================ + +impl AiConversationRepo { + /// 整体更新对话记录(title + messages + updated_at) + pub async fn update_full(&self, record: &AiConversationRecord) -> Result { + let conn = self.conn.clone(); + let id = record.id.clone(); + let title = record.title.clone(); + let messages = record.messages.clone(); + let provider_id = record.provider_id.clone(); + let model = record.model.clone(); + let updated_at = record.updated_at.clone(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let affected = guard.execute( + "UPDATE ai_conversations SET title = ?1, messages = ?2, provider_id = ?3, model = ?4, updated_at = ?5 WHERE id = ?6", + params![title, messages, provider_id, model, updated_at, id], + ).map_err(|e| Error::Storage(e.to_string()))?; + Ok(affected > 0) + }) + .await + .map_err(|e| Error::Storage(e.to_string()))? + } +} diff --git a/crates/df-storage/src/db.rs b/crates/df-storage/src/db.rs new file mode 100644 index 0000000..5ca884f --- /dev/null +++ b/crates/df-storage/src/db.rs @@ -0,0 +1,61 @@ +//! SQLite 数据库连接管理 + +use std::path::Path; +use std::sync::Arc; + +use anyhow::Result; +use rusqlite::Connection; +use tokio::sync::Mutex; + +use crate::migrations; + +/// SQLite 数据库管理器 +pub struct Database { + /// 数据库连接(线程安全) + conn: Arc>, +} + +impl Database { + /// 打开(或创建)数据库文件 + pub async fn open(path: &Path) -> Result { + let conn = Connection::open(path)?; + conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?; + + // 执行迁移 + let db = Self { + conn: Arc::new(Mutex::new(conn)), + }; + db.run_migrations().await?; + + tracing::info!("数据库已打开: {}", path.display()); + Ok(db) + } + + /// 打开内存数据库(用于测试) + pub async fn open_in_memory() -> Result { + let conn = Connection::open_in_memory()?; + conn.execute_batch("PRAGMA foreign_keys=ON;")?; + + let db = Self { + conn: Arc::new(Mutex::new(conn)), + }; + db.run_migrations().await?; + + Ok(db) + } + + /// 执行数据库迁移 + async fn run_migrations(&self) -> Result<()> { + let conn = self.conn.lock().await; + migrations::run(&conn)?; + tracing::info!("数据库迁移完成"); + Ok(()) + } + + /// 获取连接的锁守卫 + /// + /// TODO: 考虑使用 r2d2 连接池替代单连接 Mutex + pub fn conn(&self) -> Arc> { + Arc::clone(&self.conn) + } +} diff --git a/crates/df-storage/src/lib.rs b/crates/df-storage/src/lib.rs new file mode 100644 index 0000000..bc70573 --- /dev/null +++ b/crates/df-storage/src/lib.rs @@ -0,0 +1,6 @@ +//! df-storage: 存储层 — SQLite 数据库、模型定义、迁移 + +pub mod crud; +pub mod db; +pub mod migrations; +pub mod models; diff --git a/crates/df-storage/src/migrations.rs b/crates/df-storage/src/migrations.rs new file mode 100644 index 0000000..0e09b76 --- /dev/null +++ b/crates/df-storage/src/migrations.rs @@ -0,0 +1,174 @@ +//! 数据库迁移 — 建表 SQL 与版本管理 + +use anyhow::Result; +use rusqlite::Connection; + +/// 当前迁移版本 +const MIGRATION_VERSION: i32 = 2; + +/// 执行所有迁移 +pub fn run(conn: &Connection) -> Result<()> { + // 创建迁移版本表 + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS schema_version ( + version INTEGER PRIMARY KEY + );" + )?; + + let current_version: i32 = conn + .query_row( + "SELECT COALESCE(MAX(version), 0) FROM schema_version", + [], + |row| row.get(0), + ) + .unwrap_or(0); + + if current_version < 1 { + migrate_v1(conn)?; + } + + if current_version < 2 { + migrate_v2(conn)?; + } + + // 未来迁移在此扩展: + // if current_version < 3 { migrate_v3(conn)?; } + + Ok(()) +} + +/// V1: 初始表结构 +fn migrate_v1(conn: &Connection) -> Result<()> { + conn.execute_batch(V1_SQL)?; + conn.execute("INSERT INTO schema_version (version) VALUES (?)", [1])?; + tracing::info!("迁移 v1 完成"); + Ok(()) +} + +/// V2: 补齐关联字段 + branches 表 +fn migrate_v2(conn: &Connection) -> Result<()> { + conn.execute_batch(V2_SQL)?; + conn.execute("INSERT INTO schema_version (version) VALUES (?)", [2])?; + tracing::info!("迁移 v2 完成"); + Ok(()) +} + +/// V1 建表 SQL +const V1_SQL: &str = " +-- 想法表 +CREATE TABLE IF NOT EXISTS ideas ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'draft', + priority INTEGER NOT NULL DEFAULT 1, + score REAL, + tags TEXT, + source TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- 项目表 +CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'planning', + idea_id TEXT REFERENCES ideas(id), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- 任务表 +CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id), + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'todo', + priority INTEGER NOT NULL DEFAULT 1, + branch_name TEXT, + assignee TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- 发布表 +CREATE TABLE IF NOT EXISTS releases ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id), + version TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'planned', + task_ids TEXT NOT NULL DEFAULT '[]', + changelog TEXT, + created_at TEXT NOT NULL, + released_at TEXT +); + +-- 工作流执行表 +CREATE TABLE IF NOT EXISTS workflow_executions ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + dag_json TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + triggered_by TEXT, + created_at TEXT NOT NULL, + completed_at TEXT +); + +-- 节点执行表 +CREATE TABLE IF NOT EXISTS node_executions ( + id TEXT PRIMARY KEY, + workflow_id TEXT NOT NULL REFERENCES workflow_executions(id), + node_id TEXT NOT NULL, + node_type TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + input_json TEXT, + output_json TEXT, + error_message TEXT, + started_at TEXT, + completed_at TEXT +); + +-- 索引 +CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id); +CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); +CREATE INDEX IF NOT EXISTS idx_releases_project_id ON releases(project_id); +CREATE INDEX IF NOT EXISTS idx_node_executions_workflow_id ON node_executions(workflow_id); +"; + +/// V2 迁移 SQL — 补齐数据层断裂字段 +/// +/// 注意: SQLite 的 ALTER TABLE ADD COLUMN 一条语句只能加一列。 +const V2_SQL: &str = " +-- 想法表: 晋升关联 + AI 分析 + 多维评分 +ALTER TABLE ideas ADD COLUMN promoted_to TEXT; +ALTER TABLE ideas ADD COLUMN ai_analysis TEXT; +ALTER TABLE ideas ADD COLUMN scores TEXT; + +-- 任务表: 工作流定义关联 + 基础分支 +ALTER TABLE tasks ADD COLUMN workflow_def_id TEXT; +ALTER TABLE tasks ADD COLUMN base_branch TEXT; + +-- 工作流执行表: 项目 / 任务关联 +ALTER TABLE workflow_executions ADD COLUMN project_id TEXT; +ALTER TABLE workflow_executions ADD COLUMN task_id TEXT; + +-- 分支表 — 任务与 Git 分支绑定(核心功能) +CREATE TABLE IF NOT EXISTS branches ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id), + task_id TEXT REFERENCES tasks(id), + name TEXT NOT NULL, + base TEXT NOT NULL DEFAULT 'main', + status TEXT NOT NULL DEFAULT 'active', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + merged_at TEXT +); + +-- 索引 +CREATE INDEX IF NOT EXISTS idx_branches_project_id ON branches(project_id); +CREATE INDEX IF NOT EXISTS idx_branches_task_id ON branches(task_id); +"; diff --git a/crates/df-storage/src/models.rs b/crates/df-storage/src/models.rs new file mode 100644 index 0000000..5900385 --- /dev/null +++ b/crates/df-storage/src/models.rs @@ -0,0 +1,170 @@ +//! 数据模型定义 — 与数据库表对应的 Rust 结构体 + +use serde::{Deserialize, Serialize}; + +// ============================================================ +// 想法相关模型 +// ============================================================ + +/// 想法记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IdeaRecord { + pub id: String, + pub title: String, + pub description: String, + pub status: String, + pub priority: i32, + pub score: Option, + pub tags: Option, // JSON 数组 + pub source: Option, + pub promoted_to: Option, // 晋升后的 project_id + pub ai_analysis: Option, // AI 分析结果 JSON + pub scores: Option, // 多维评分 JSON (feasibility/impact/urgency/overall) + pub created_at: String, + pub updated_at: String, +} + +// ============================================================ +// 项目相关模型 +// ============================================================ + +/// 项目记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectRecord { + pub id: String, + pub name: String, + pub description: String, + pub status: String, + pub idea_id: Option, + pub created_at: String, + pub updated_at: String, +} + +// ============================================================ +// 任务相关模型 +// ============================================================ + +/// 任务记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskRecord { + pub id: String, + pub project_id: String, + pub title: String, + pub description: String, + pub status: String, + pub priority: i32, + pub branch_name: Option, + pub assignee: Option, + pub workflow_def_id: Option, // 关联的工作流定义 ID + pub base_branch: Option, // 基础分支 + pub created_at: String, + pub updated_at: String, +} + +/// 分支记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BranchRecord { + pub id: String, + pub project_id: String, + pub task_id: Option, + pub name: String, + pub base: String, + pub status: String, + pub created_at: String, + pub updated_at: String, + pub merged_at: Option, +} + +/// 发布记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReleaseRecord { + pub id: String, + pub project_id: String, + pub version: String, + pub status: String, + pub task_ids: String, // JSON 数组 + pub changelog: Option, + pub created_at: String, + pub released_at: Option, +} + +// ============================================================ +// 工作流相关模型 +// ============================================================ + +/// 工作流执行记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkflowRecord { + pub id: String, + pub name: String, + pub dag_json: String, // DAG 的 JSON 序列化 + pub status: String, + pub triggered_by: Option, + pub project_id: Option, // 关联项目 ID + pub task_id: Option, // 关联任务 ID + pub created_at: String, + pub completed_at: Option, +} + +/// 节点执行记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeExecutionRecord { + pub id: String, + pub workflow_id: String, + pub node_id: String, + pub node_type: String, + pub status: String, + pub input_json: Option, + pub output_json: Option, + pub error_message: Option, + pub started_at: Option, + pub completed_at: Option, +} + +// ============================================================ +// AI 相关模型 (V3) +// ============================================================ + +/// AI 提供商配置记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AiProviderRecord { + pub id: String, + pub name: String, + pub provider_type: String, + pub api_key: String, + pub base_url: String, + pub default_model: String, + pub models: Option, // JSON array of model names + pub is_default: bool, + pub config: Option, // JSON extra config + pub created_at: String, + pub updated_at: String, +} + +/// AI 对话记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AiConversationRecord { + pub id: String, + pub title: Option, + pub messages: String, // JSON array of ChatMessage + pub provider_id: Option, + pub model: Option, + pub created_at: String, + pub updated_at: String, +} + +/// AI 工具执行审计记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AiToolExecutionRecord { + pub id: String, + pub conversation_id: Option, + pub tool_call_id: String, + pub tool_name: String, + pub arguments: String, + pub result: Option, + pub status: String, // pending/approved/rejected/executing/completed/failed + pub risk_level: String, // low/medium/high + pub requested_at: String, + pub executed_at: Option, + pub decided_by: Option, // human/auto +} diff --git a/crates/df-task/Cargo.toml b/crates/df-task/Cargo.toml new file mode 100644 index 0000000..e258cf0 --- /dev/null +++ b/crates/df-task/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "df-task" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +anyhow = { workspace = true } +chrono = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-task/src/branch.rs b/crates/df-task/src/branch.rs new file mode 100644 index 0000000..160b5dc --- /dev/null +++ b/crates/df-task/src/branch.rs @@ -0,0 +1,74 @@ +//! 分支管理 — Git 分支的创建、跟踪与生命周期 + +use serde::{Deserialize, Serialize}; + +use df_core::types::{BranchId, ProjectId, TaskId}; + +/// 分支状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BranchStatus { + /// 活跃开发中 + Active, + /// 待合并 + ReadyToMerge, + /// 已合并 + Merged, + /// 已删除 + Deleted, +} + +/// 分支实体 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Branch { + /// 唯一 ID + pub id: BranchId, + /// 分支名称 + pub name: String, + /// 所属项目 ID + pub project_id: ProjectId, + /// 关联任务 ID + pub task_id: Option, + /// 基于的分支(通常是 main) + pub base_branch: String, + /// 当前状态 + pub status: BranchStatus, + /// 创建时间 + pub created_at: chrono::DateTime, +} + +/// 分支管理器 +pub struct BranchManager; + +impl BranchManager { + /// 为任务创建分支 + /// + /// TODO: 接入 df-execute 的 git_ops 执行实际的 Git 操作 + pub fn create_for_task( + task_id: &TaskId, + project_id: &ProjectId, + base_branch: &str, + ) -> Branch { + // 生成分支名称:task/{task_id 前缀} + let short_id = &task_id[..8.min(task_id.len())]; + let branch_name = format!("task/{}", short_id); + + Branch { + id: df_core::types::new_id(), + name: branch_name, + project_id: project_id.clone(), + task_id: Some(task_id.clone()), + base_branch: base_branch.to_string(), + status: BranchStatus::Active, + created_at: chrono::Utc::now(), + } + } + + /// 列出项目的所有活跃分支 + /// + /// TODO: 接入 df-execute 的 git_ops + pub fn list_active(_project_id: &ProjectId) -> Vec { + // TODO: 实现 + Vec::new() + } +} diff --git a/crates/df-task/src/lib.rs b/crates/df-task/src/lib.rs new file mode 100644 index 0000000..951da33 --- /dev/null +++ b/crates/df-task/src/lib.rs @@ -0,0 +1,6 @@ +//! df-task: 任务/分支管理 — 任务 CRUD、分支管理、合并协调、发布计划 + +pub mod branch; +pub mod merge; +pub mod release; +pub mod task; diff --git a/crates/df-task/src/merge.rs b/crates/df-task/src/merge.rs new file mode 100644 index 0000000..59998ea --- /dev/null +++ b/crates/df-task/src/merge.rs @@ -0,0 +1,91 @@ +//! 合并协调器 — 分支合并、冲突检测、AI 辅助解决 + +use df_core::types::{BranchId, TaskId}; + +/// 合并状态 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MergeStatus { + /// 待合并 + Pending, + /// 自动合并中 + AutoMerging, + /// 存在冲突,等待解决 + Conflicted, + /// 已解决冲突 + Resolved, + /// 合并完成 + Completed, + /// 合并失败 + Failed, +} + +/// 合并请求 +#[derive(Debug, Clone)] +pub struct MergeRequest { + pub id: String, + pub source_branch: BranchId, + pub target_branch: String, + pub task_id: Option, + pub status: MergeStatus, + pub conflicts: Vec, +} + +/// 冲突信息 +#[derive(Debug, Clone)] +pub struct Conflict { + pub file_path: String, + pub conflict_type: ConflictType, + pub description: String, + pub auto_resolvable: bool, +} + +/// 冲突类型 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConflictType { + /// 文件级别冲突(双方修改了同一文件) + FileModified, + /// 删除/修改冲突 + DeleteModify, + /// 二进制文件冲突 + Binary, +} + +/// 合并协调器 +pub struct MergeCoordinator; + +impl MergeCoordinator { + /// 创建合并请求 + /// + /// TODO: 接入 Git 操作执行实际合并 + pub fn create_request( + source_branch: BranchId, + target_branch: String, + task_id: Option, + ) -> MergeRequest { + MergeRequest { + id: df_core::types::new_id(), + source_branch, + target_branch, + task_id, + status: MergeStatus::Pending, + conflicts: Vec::new(), + } + } + + /// 检测冲突 + /// + /// TODO: 调用 git merge --no-commit --no-ff 检测 + pub fn detect_conflicts(_merge_request: &MergeRequest) -> Vec { + // TODO: 实现冲突检测 + Vec::new() + } + + /// AI 辅助解决冲突 + /// + /// TODO: 调用 df-ai 的 LLM 分析冲突并提供解决方案 + pub fn ai_resolve_conflict(_conflict: &Conflict) -> anyhow::Result { + // TODO: 实现 AI 辅助冲突解决 + tracing::warn!("AI 冲突解决尚未实现"); + Ok(String::new()) + } +} diff --git a/crates/df-task/src/release.rs b/crates/df-task/src/release.rs new file mode 100644 index 0000000..d21d7fc --- /dev/null +++ b/crates/df-task/src/release.rs @@ -0,0 +1,132 @@ +//! 发布计划 — 多任务合并、版本号管理、发布流程 + +use serde::{Deserialize, Serialize}; + +use df_core::types::{ProjectId, ReleaseId, TaskId}; + +/// 发布状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ReleaseStatus { + /// 计划中 + Planned, + /// 准备中(收集已完成的任务) + Preparing, + /// 构建中 + Building, + /// 测试中 + Testing, + /// 待发布 + Ready, + /// 发布中 + Releasing, + /// 已发布 + Released, + /// 已回滚 + RolledBack, + /// 已取消 + Cancelled, +} + +/// 版本号 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SemanticVersion { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub pre_release: Option, +} + +impl std::fmt::Display for SemanticVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.pre_release { + Some(pre) => write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre), + None => write!(f, "{}.{}.{}", self.major, self.minor, self.patch), + } + } +} + +/// 发布计划 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Release { + /// 唯一 ID + pub id: ReleaseId, + /// 所属项目 ID + pub project_id: ProjectId, + /// 版本号 + pub version: SemanticVersion, + /// 当前状态 + pub status: ReleaseStatus, + /// 包含的任务 ID 列表 + pub task_ids: Vec, + /// 变更日志 + pub changelog: Option, + /// 创建时间 + pub created_at: chrono::DateTime, + /// 发布时间 + pub released_at: Option>, +} + +/// 版本号递增类型 +#[derive(Debug, Clone, Copy)] +pub enum VersionBump { + Major, + Minor, + Patch, +} + +/// 发布管理器 +pub struct ReleaseManager; + +impl ReleaseManager { + /// 创建发布计划 + pub fn create( + project_id: &ProjectId, + version: SemanticVersion, + task_ids: Vec, + ) -> Release { + Release { + id: df_core::types::new_id(), + project_id: project_id.clone(), + version, + status: ReleaseStatus::Planned, + task_ids, + changelog: None, + created_at: chrono::Utc::now(), + released_at: None, + } + } + + /// 自动生成变更日志 + /// + /// TODO: 从 Git 提交历史、任务描述中生成 + pub fn generate_changelog(_release: &mut Release) -> anyhow::Result<()> { + // TODO: 实现 + tracing::warn!("自动变更日志生成尚未实现"); + Ok(()) + } + + /// 自动递增版本号 + pub fn bump_version(current: &SemanticVersion, bump: VersionBump) -> SemanticVersion { + match bump { + VersionBump::Major => SemanticVersion { + major: current.major + 1, + minor: 0, + patch: 0, + pre_release: None, + }, + VersionBump::Minor => SemanticVersion { + major: current.major, + minor: current.minor + 1, + patch: 0, + pre_release: None, + }, + VersionBump::Patch => SemanticVersion { + major: current.major, + minor: current.minor, + patch: current.patch + 1, + pre_release: None, + }, + } + } +} diff --git a/crates/df-task/src/task.rs b/crates/df-task/src/task.rs new file mode 100644 index 0000000..c3c2d55 --- /dev/null +++ b/crates/df-task/src/task.rs @@ -0,0 +1,87 @@ +//! 任务管理 — 任务实体与 CRUD + +use serde::{Deserialize, Serialize}; + +use df_core::types::{BranchId, Priority, ProjectId, TaskId, TaskStatus}; + +/// 任务实体 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Task { + /// 唯一 ID + pub id: TaskId, + /// 所属项目 ID + pub project_id: ProjectId, + /// 任务标题 + pub title: String, + /// 任务描述 + pub description: String, + /// 当前状态 + pub status: TaskStatus, + /// 优先级 + pub priority: Priority, + /// 关联分支名称 + pub branch_name: Option, + /// 关联分支 ID + pub branch_id: Option, + /// 指派人 + pub assignee: Option, + /// 标签 + pub tags: Vec, + /// 预估工时(小时) + pub estimate_hours: Option, + /// 实际工时(小时) + pub actual_hours: Option, + /// 创建时间 + pub created_at: chrono::DateTime, + /// 更新时间 + pub updated_at: chrono::DateTime, +} + +/// 创建任务的输入 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateTaskInput { + pub project_id: ProjectId, + pub title: String, + pub description: String, + #[serde(default)] + pub priority: Priority, + pub assignee: Option, + pub tags: Vec, + pub estimate_hours: Option, +} + +/// 任务管理器 +pub struct TaskManager; + +impl TaskManager { + /// 创建新任务 + /// + /// TODO: 接入存储层持久化 + pub fn create(input: CreateTaskInput) -> Task { + let now = chrono::Utc::now(); + Task { + id: df_core::types::new_id(), + project_id: input.project_id, + title: input.title, + description: input.description, + status: TaskStatus::Todo, + priority: input.priority, + branch_name: None, + branch_id: None, + assignee: input.assignee, + tags: input.tags, + estimate_hours: input.estimate_hours, + actual_hours: None, + created_at: now, + updated_at: now, + } + } + + /// 更新任务状态 + pub fn transition_status(task: &mut Task, new_status: TaskStatus) -> anyhow::Result<()> { + // TODO: 校验状态转换合法性 + task.status = new_status; + task.updated_at = chrono::Utc::now(); + Ok(()) + } +} diff --git a/crates/df-traceability/Cargo.toml b/crates/df-traceability/Cargo.toml new file mode 100644 index 0000000..3bfb9c3 --- /dev/null +++ b/crates/df-traceability/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "df-traceability" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/df-traceability/src/annotation.rs b/crates/df-traceability/src/annotation.rs new file mode 100644 index 0000000..b1a96fe --- /dev/null +++ b/crates/df-traceability/src/annotation.rs @@ -0,0 +1,130 @@ +//! 标注系统:FIXME/TODO/QUESTION 等标记,批量收集后交给 AI 处理 + +use df_core::types::ProjectId; +use serde::{Deserialize, Serialize}; + +/// 标注标记类型 +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AnnotationMarker { + /// 需要修复 + Fixme, + /// 待办事项 + Todo, + /// 疑问待确认 + Question, + /// 风险标记 + Risk, + /// 决策标记 + Decision, + /// 优化建议 + Optimize, +} + +/// 标注状态 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AnnotationStatus { + /// 未处理 + Open, + /// AI 处理中 + AiProcessing, + /// 已解决 + Resolved, + /// 不处理 + WontFix, +} + +/// 标注可附加的实体类型 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum EntityType { + Requirement, + Feature, + Code, + TestCase, + TestReport, + DesignDoc, +} + +/// 标注 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Annotation { + pub id: String, + pub project_id: ProjectId, + /// 附加的实体类型 + pub entity_type: EntityType, + /// 附加的实体 ID + pub entity_id: String, + /// 标记类型 + pub marker: AnnotationMarker, + /// 标注内容 + pub content: String, + /// 位置(文件路径+行号 / 文档段落) + pub location: Option, + /// 状态 + pub status: AnnotationStatus, + /// 处理者 + pub resolved_by: Option, + /// 处理结果 + pub resolution: Option, + pub created_at: i64, + pub resolved_at: Option, +} + +impl Annotation { + pub fn new( + project_id: ProjectId, + entity_type: EntityType, + entity_id: String, + marker: AnnotationMarker, + content: String, + ) -> Self { + Self { + id: df_core::types::new_id(), + project_id, + entity_type, + entity_id, + marker, + content, + location: None, + status: AnnotationStatus::Open, + resolved_by: None, + resolution: None, + created_at: chrono::Utc::now().timestamp(), + resolved_at: None, + } + } +} + +/// 标注批量处理器 +pub struct AnnotationBatchProcessor; + +impl AnnotationBatchProcessor { + /// 收集项目中所有未处理的标注 + pub fn collect_open_annotations(_project_id: &ProjectId) -> Vec { + // TODO: 从 SQLite 查询所有 status = Open 的标注 + vec![] + } + + /// 按类型分组 + pub fn group_by_marker(annotations: &[Annotation]) -> std::collections::HashMap> { + let mut groups: std::collections::HashMap> = std::collections::HashMap::new(); + for ann in annotations { + groups.entry(ann.marker.clone()).or_default().push(ann); + } + groups + } + + /// 将分组后的标注交给 AI 批量处理 + /// + /// AI 会逐条处理每个标注,更新内容和状态 + pub async fn batch_process(_annotations: &[Annotation]) -> anyhow::Result> { + // TODO: 调用 df-ai 编排器,构建批量处理 prompt + // 1. 按类型分组 + // 2. 每组构建一个 AI 请求 + // 3. AI 返回处理结果 + // 4. 更新标注状态和 resolution + Ok(vec![]) + } +} diff --git a/crates/df-traceability/src/decision.rs b/crates/df-traceability/src/decision.rs new file mode 100644 index 0000000..da17758 --- /dev/null +++ b/crates/df-traceability/src/decision.rs @@ -0,0 +1,95 @@ +//! 决策留痕:所有关键决策的记录、检索和审计 + +use df_core::types::{DecisionId, ProjectId}; +use serde::{Deserialize, Serialize}; + +/// 决策记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Decision { + pub id: DecisionId, + pub project_id: ProjectId, + /// 决策背景 + pub context: String, + /// 需要决定的问题 + pub question: String, + /// 考虑过的方案 + pub alternatives: Vec, + /// 最终决定 + pub decision: String, + /// 决策原因 + pub reason: String, + /// 决策者 + pub decided_by: DecidedBy, + /// 关联实体类型 + pub entity_type: Option, + /// 关联实体 ID + pub entity_id: Option, + /// 影响范围 + pub impact: Option, + /// 所处阶段 + pub stage: Option, + pub created_at: i64, +} + +/// 决策者 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DecidedBy { + /// AI 自动决策 + Ai, + /// 人工决策 + Human, + /// AI 建议后人工确认 + AiSuggested, +} + +/// 决策日志 +pub struct DecisionJournal; + +impl DecisionJournal { + /// 记录新决策 + pub fn record( + project_id: ProjectId, + context: String, + question: String, + alternatives: Vec, + decision: String, + reason: String, + decided_by: DecidedBy, + ) -> Decision { + Decision { + id: df_core::types::new_id(), + project_id, + context, + question, + alternatives, + decision, + reason, + decided_by, + entity_type: None, + entity_id: None, + impact: None, + stage: None, + created_at: chrono::Utc::now().timestamp(), + } + } + + /// 查询项目的决策历史 + pub fn query_by_project(_project_id: &ProjectId) -> Vec { + // TODO: SQLite 查询 + vec![] + } + + /// 按阶段筛选 + pub fn filter_by_stage<'a>(decisions: &'a [Decision], stage: &str) -> Vec<&'a Decision> { + decisions.iter().filter(|d| d.stage.as_deref() == Some(stage)).collect() + } + + /// 生成决策时间线 + /// + /// 按时间顺序展示项目的所有决策,用于复盘和审计 + pub fn timeline(_project_id: &ProjectId) -> Vec { + // TODO: 查询并按 created_at 排序 + vec![] + } +} diff --git a/crates/df-traceability/src/lib.rs b/crates/df-traceability/src/lib.rs new file mode 100644 index 0000000..8491270 --- /dev/null +++ b/crates/df-traceability/src/lib.rs @@ -0,0 +1,9 @@ +//! 可追溯性引擎:标注系统、决策留痕、需求-测试映射 + +pub mod annotation; +pub mod decision; +pub mod traceability; + +pub use annotation::{Annotation, AnnotationMarker, AnnotationStatus}; +pub use decision::{Decision, DecisionJournal}; +pub use traceability::TraceabilityMatrix; diff --git a/crates/df-traceability/src/traceability.rs b/crates/df-traceability/src/traceability.rs new file mode 100644 index 0000000..5e1b230 --- /dev/null +++ b/crates/df-traceability/src/traceability.rs @@ -0,0 +1,78 @@ +//! 需求-功能-测试可追溯矩阵 +//! +//! 建立需求 → 功能 → 测试用例 → 测试报告 的双向追溯关系 + +use df_core::types::ProjectId; +use serde::{Deserialize, Serialize}; + +/// 追溯关系 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TraceLink { + /// 上游实体类型 (Requirement / Feature) + pub source_type: String, + pub source_id: String, + /// 下游实体类型 (Feature / TestCase / TestRun) + pub target_type: String, + pub target_id: String, +} + +/// 覆盖率统计 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CoverageReport { + /// 功能总数 + pub total_features: usize, + /// 已选中功能数 + pub selected_features: usize, + /// 有测试用例的功能数 + pub features_with_tests: usize, + /// 测试覆盖率百分比 + pub coverage_percent: f32, + /// 测试通过率 + pub pass_rate: f32, +} + +/// 可追溯矩阵 +pub struct TraceabilityMatrix; + +impl TraceabilityMatrix { + /// 创建追溯关系 + pub fn link( + source_type: &str, + source_id: &str, + target_type: &str, + target_id: &str, + ) -> TraceLink { + TraceLink { + source_type: source_type.to_string(), + source_id: source_id.to_string(), + target_type: target_type.to_string(), + target_id: target_id.to_string(), + } + } + + /// 查询功能的完整追溯链 + /// + /// Feature → [TestCases] → [TestRuns] → [TestReports] + pub fn trace_feature(_feature_id: &str) -> Vec { + // TODO: SQLite 查询完整链路 + vec![] + } + + /// 生成覆盖率报告 + pub fn coverage_report(_project_id: &ProjectId) -> CoverageReport { + // TODO: 统计功能-测试用例覆盖情况 + CoverageReport { + total_features: 0, + selected_features: 0, + features_with_tests: 0, + coverage_percent: 0.0, + pass_rate: 0.0, + } + } + + /// 查找未覆盖的功能(有功能但无测试用例) + pub fn uncovered_features(_project_id: &ProjectId) -> Vec { + // TODO: 找出没有关联测试用例的功能 + vec![] + } +} diff --git a/crates/df-workflow/Cargo.toml b/crates/df-workflow/Cargo.toml new file mode 100644 index 0000000..31ccaa9 --- /dev/null +++ b/crates/df-workflow/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "df-workflow" +version = "0.1.0" +edition = "2021" + +[dependencies] +df-core = { path = "../df-core" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +async-trait = { workspace = true } +futures = "0.3" +thiserror = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } diff --git a/crates/df-workflow/src/conditions.rs b/crates/df-workflow/src/conditions.rs new file mode 100644 index 0000000..eaf1398 --- /dev/null +++ b/crates/df-workflow/src/conditions.rs @@ -0,0 +1,33 @@ +//! 条件表达式引擎 — 用于 DAG 边上的条件判断 +//! +//! TODO: 实现完整的条件表达式解析与求值 + +use serde_json::Value; + +/// 条件表达式求值器 +pub struct ConditionEngine; + +impl ConditionEngine { + /// 求值条件表达式 + /// + /// TODO: 实现完整的表达式解析,当前仅支持简单的 JSON Path 比较 + pub fn evaluate(expr: &str, _context: &Value) -> anyhow::Result { + // 骨架:简单的 true/false 字面量 + let trimmed = expr.trim(); + if trimmed == "true" { + return Ok(true); + } + if trimmed == "false" { + return Ok(false); + } + + // TODO: 实现如下语法: + // - "$.status == 'completed'" — JSON Path 比较 + // - "$.count > 10" — 数值比较 + // - "$.tags contains 'ai'" — 包含检查 + // - "and/or/not" — 逻辑组合 + + tracing::warn!("条件表达式引擎尚未完整实现,表达式: {}", expr); + Ok(true) + } +} diff --git a/crates/df-workflow/src/dag.rs b/crates/df-workflow/src/dag.rs new file mode 100644 index 0000000..0241e81 --- /dev/null +++ b/crates/df-workflow/src/dag.rs @@ -0,0 +1,141 @@ +//! DAG(有向无环图)定义与拓扑排序 + +use std::collections::{HashMap, HashSet, VecDeque}; + +use df_core::error::Result; +use df_core::types::NodeId; + +use crate::node::Node; + +/// 图的边:从 source 到 target +#[derive(Debug, Clone)] +pub struct Edge { + pub source: NodeId, + pub target: NodeId, + /// 边的条件表达式(可选) + pub condition: Option, +} + +/// DAG 结构 +pub struct Dag { + /// 节点集合 + pub nodes: HashMap>, + /// 边集合 + pub edges: Vec, +} + +impl Dag { + /// 创建空 DAG + pub fn new() -> Self { + Self { + nodes: HashMap::new(), + edges: Vec::new(), + } + } + + /// 添加节点 + pub fn add_node(&mut self, id: NodeId, node: Box) { + self.nodes.insert(id, node); + } + + /// 添加边 + pub fn add_edge(&mut self, source: NodeId, target: NodeId) { + self.edges.push(Edge { + source, + target, + condition: None, + }); + } + + /// 添加带条件的边 + pub fn add_edge_with_condition( + &mut self, + source: NodeId, + target: NodeId, + condition: String, + ) { + self.edges.push(Edge { + source, + target, + condition: Some(condition), + }); + } + + /// 获取指定节点的所有上游节点 ID + pub fn predecessors(&self, node_id: &NodeId) -> Vec { + self.edges + .iter() + .filter(|e| &e.target == node_id) + .map(|e| e.source.clone()) + .collect() + } + + /// 获取指定节点的所有下游节点 ID + pub fn successors(&self, node_id: &NodeId) -> Vec { + self.edges + .iter() + .filter(|e| &e.source == node_id) + .map(|e| e.target.clone()) + .collect() + } + + /// 拓扑排序 — 返回按执行顺序排列的节点 ID 层级 + /// + /// 返回 Vec>,每层内的节点可以并行执行 + pub fn topological_layers(&self) -> Result>> { + let node_ids: HashSet = self.nodes.keys().cloned().collect(); + let mut in_degree: HashMap = HashMap::new(); + + // 初始化入度 + for id in &node_ids { + in_degree.insert(id.clone(), 0); + } + for edge in &self.edges { + if node_ids.contains(&edge.source) && node_ids.contains(&edge.target) { + *in_degree.entry(edge.target.clone()).or_insert(0) += 1; + } + } + + // BFS 分层 + let mut layers = Vec::new(); + let mut queue: VecDeque = in_degree + .iter() + .filter(|(_, °)| deg == 0) + .map(|(id, _)| id.clone()) + .collect(); + + let mut processed = 0usize; + while !queue.is_empty() { + let mut layer: Vec = Vec::new(); + let layer_size = queue.len(); + for _ in 0..layer_size { + if let Some(id) = queue.pop_front() { + layer.push(id.clone()); + for succ in self.successors(&id) { + let deg = in_degree.get_mut(&succ).unwrap(); + *deg -= 1; + if *deg == 0 { + queue.push_back(succ); + } + } + processed += 1; + } + } + layers.push(layer); + } + + if processed != node_ids.len() { + return Err(df_core::error::Error::Workflow( + "DAG 中存在环,无法进行拓扑排序".to_string(), + )); + } + + Ok(layers) + } +} + +impl Default for Dag { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/df-workflow/src/dag_def.rs b/crates/df-workflow/src/dag_def.rs new file mode 100644 index 0000000..e5a3a39 --- /dev/null +++ b/crates/df-workflow/src/dag_def.rs @@ -0,0 +1,104 @@ +//! DAG 定义 — 可序列化/反序列化,用于持久化和模板 + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::dag::Dag; + +/// DAG 定义 — 可序列化/反序列化,用于持久化和模板 +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DagDef { + pub nodes: HashMap, + pub edges: Vec, +} + +/// 节点定义 +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct NodeDef { + pub id: String, + pub node_type: String, + pub config: serde_json::Value, + #[serde(default)] + pub label: Option, +} + +/// 边定义 +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct EdgeDef { + pub source: String, + pub target: String, + #[serde(default)] + pub condition: Option, +} + +impl DagDef { + /// 创建空的 DAG 定义 + pub fn new() -> Self { + Self { + nodes: HashMap::new(), + edges: Vec::new(), + } + } + + /// 添加节点定义 + pub fn add_node(&mut self, id: impl Into, node_type: impl Into, config: serde_json::Value) { + let id = id.into(); + self.nodes.insert(id.clone(), NodeDef { + id, + node_type: node_type.into(), + config, + label: None, + }); + } + + /// 添加边定义 + pub fn add_edge(&mut self, source: impl Into, target: impl Into) { + self.edges.push(EdgeDef { + source: source.into(), + target: target.into(), + condition: None, + }); + } + + /// 添加带条件的边定义 + pub fn add_edge_with_condition( + &mut self, + source: impl Into, + target: impl Into, + condition: impl Into, + ) { + self.edges.push(EdgeDef { + source: source.into(), + target: target.into(), + condition: Some(condition.into()), + }); + } + + /// 从已有的运行时 Dag 提取定义(忽略节点实例,仅保留元数据) + /// + /// 注意:此方法只能提取边的定义,节点配置无法从 trait object 反推 + pub fn from_dag_edges(dag: &Dag) -> Self { + let edges: Vec = dag.edges.iter().map(|e| EdgeDef { + source: e.source.clone(), + target: e.target.clone(), + condition: e.condition.clone(), + }).collect(); + + let nodes: HashMap = dag.nodes.iter().map(|(id, node)| { + (id.clone(), NodeDef { + id: id.clone(), + node_type: node.node_type().to_string(), + config: serde_json::Value::Null, + label: None, + }) + }).collect(); + + Self { nodes, edges } + } +} + +impl Default for DagDef { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/df-workflow/src/eventbus.rs b/crates/df-workflow/src/eventbus.rs new file mode 100644 index 0000000..d708591 --- /dev/null +++ b/crates/df-workflow/src/eventbus.rs @@ -0,0 +1,68 @@ +//! 事件总线 — 基于 tokio::sync::broadcast 的发布/订阅 + +use tokio::sync::broadcast; +use tokio::sync::broadcast::error::SendError; +use df_core::events::{WorkflowEvent, HumanApprovalResponse}; + +/// 事件总线 +#[derive(Debug)] +pub struct EventBus { + sender: broadcast::Sender, +} + +/// 事件订阅者 +pub type EventSubscriber = broadcast::Receiver; + +/// 默认事件通道容量 +const DEFAULT_CAPACITY: usize = 256; + +impl EventBus { + /// 创建事件总线 + pub fn new() -> Self { + let (sender, _) = broadcast::channel(DEFAULT_CAPACITY); + Self { sender } + } + + /// 创建指定容量的事件总线 + pub fn with_capacity(capacity: usize) -> Self { + let (sender, _) = broadcast::channel(capacity); + Self { sender } + } + + /// 发送事件 + pub async fn send(&self, event: WorkflowEvent) { + // broadcast::send 是同步的,忽略接收者已关闭的错误 + let _ = self.sender.send(event); + } + + /// 订阅事件 + pub fn subscribe(&self) -> EventSubscriber { + self.sender.subscribe() + } + + /// 发送人工审批请求 + pub fn emit_human_approval_request(&self, event: WorkflowEvent) -> Result> { + self.sender.send(event) + } + + /// 尝试获取人工审批响应(简化版本,实际需要实现状态存储) + pub fn try_recv_human_approval(&self, _execution_id: &str, _node_id: &str) -> Option { + // TODO: 实现审批响应的存储和检索 + // 目前返回 None,需要配合前端实现 + None + } +} + +impl Default for EventBus { + fn default() -> Self { + Self::new() + } +} + +impl Clone for EventBus { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + } + } +} diff --git a/crates/df-workflow/src/executor.rs b/crates/df-workflow/src/executor.rs new file mode 100644 index 0000000..8e05f19 --- /dev/null +++ b/crates/df-workflow/src/executor.rs @@ -0,0 +1,235 @@ +//! DAG 执行器 — 按拓扑层级调度执行 + +use std::collections::HashMap; + +use df_core::events::WorkflowEvent; +use df_core::types::NodeId; + +use crate::dag::Dag; +use crate::eventbus::EventBus; +use crate::node::{NodeContext, NodeOutput}; +use crate::state::StateMachine; + +/// DAG 执行器 +pub struct DagExecutor { + /// 事件总线 + event_bus: EventBus, + /// 节点状态机 + state_machine: StateMachine, +} + +impl DagExecutor { + /// 创建执行器 + pub fn new(event_bus: EventBus) -> Self { + Self { + event_bus, + state_machine: StateMachine::new(), + } + } + + /// 执行 DAG 工作流 + /// + /// 同一拓扑层内的节点并发执行,层与层之间串行; + /// 本层全部节点完成后统一更新状态,任一失败则中止后续层。 + pub async fn run( + &mut self, + dag: &Dag, + initial_config: serde_json::Value, + ) -> anyhow::Result> { + let layers = dag.topological_layers()?; + let mut outputs: HashMap = HashMap::new(); + let start = std::time::Instant::now(); + + tracing::info!("DAG 执行开始,共 {} 层", layers.len()); + + for (layer_idx, layer) in layers.iter().enumerate() { + tracing::info!("执行第 {} 层,共 {} 个节点", layer_idx, layer.len()); + + // 阶段一:逐节点发 NodeStarted、置为运行中,并构建执行 future + let mut node_futures = Vec::with_capacity(layer.len()); + for node_id in layer { + let node = dag.nodes.get(node_id).ok_or_else(|| { + anyhow::anyhow!("节点 {} 不存在于 DAG 中", node_id) + })?; + + // 发送 NodeStarted 事件 + self.event_bus + .send(WorkflowEvent::NodeStarted { + node_id: node_id.clone(), + }) + .await; + + self.state_machine.set_running(node_id.clone())?; + + // 构建节点上下文 + let mut inputs = HashMap::new(); + for pred_id in dag.predecessors(node_id) { + if let Some(out) = outputs.get(&pred_id) { + inputs.insert(pred_id, out.clone()); + } + } + + let ctx = NodeContext { + node_id: node_id.clone(), + inputs, + config: initial_config.clone(), + execution_id: "dummy-execution-id".to_string(), // TODO: 应该从 workflow_execution 获取 + event_bus: self.event_bus.clone(), + node_status: StateMachine::new(), + }; + + // 仅捕获节点共享引用与所有权上下文,避免与 self 借用冲突 + let id = node_id.clone(); + node_futures.push(async move { + let node_start = std::time::Instant::now(); + let result = node.execute(ctx).await; + (id, result, node_start.elapsed().as_millis() as u64) + }); + } + + // 阶段二:同层节点并发执行,等待全部完成 + let results = futures::future::join_all(node_futures).await; + + // 阶段三:统一更新状态并发送事件,任一失败则整体返回 Err + let mut first_err: Option = None; + for (node_id, result, duration) in results { + match result { + Ok(output) => { + self.state_machine.set_completed(node_id.clone())?; + self.event_bus + .send(WorkflowEvent::NodeCompleted { + node_id: node_id.clone(), + duration_ms: duration, + }) + .await; + outputs.insert(node_id, output); + } + Err(e) => { + self.state_machine.set_failed(node_id.clone())?; + self.event_bus + .send(WorkflowEvent::NodeFailed { + node_id: node_id.clone(), + error: e.to_string(), + }) + .await; + // 同层多个失败时只报告第一个 + if first_err.is_none() { + first_err = Some(e.context(format!("节点 {} 执行失败", node_id))); + } + } + } + } + + if let Some(e) = first_err { + return Err(e); + } + } + + let total = start.elapsed().as_millis() as u64; + self.event_bus + .send(WorkflowEvent::WorkflowCompleted { + total_duration_ms: total, + }) + .await; + + tracing::info!("DAG 执行完成,耗时 {}ms", total); + Ok(outputs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::node::{Node, NodeResult, NodeSchema}; + use async_trait::async_trait; + use std::time::Duration; + + /// 测试节点:sleep 指定毫秒后返回空输出 + struct SleepNode { + sleep_ms: u64, + } + + #[async_trait] + impl Node for SleepNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + tokio::time::sleep(Duration::from_millis(self.sleep_ms)).await; + Ok(NodeOutput::empty()) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::Value::Null, + output: serde_json::Value::Null, + } + } + + fn node_type(&self) -> &str { + "sleep" + } + } + + /// 测试节点:直接返回错误 + struct FailNode; + + #[async_trait] + impl Node for FailNode { + async fn execute(&self, _ctx: NodeContext) -> NodeResult { + Err(anyhow::anyhow!("故意失败")) + } + + fn schema(&self) -> NodeSchema { + NodeSchema { + params: serde_json::Value::Null, + output: serde_json::Value::Null, + } + } + + fn node_type(&self) -> &str { + "fail" + } + } + + #[tokio::test] + async fn test_same_layer_runs_in_parallel() { + // 两个无依赖节点位于同一层,各 sleep 100ms + let mut dag = Dag::new(); + dag.add_node("a".to_string(), Box::new(SleepNode { sleep_ms: 100 })); + dag.add_node("b".to_string(), Box::new(SleepNode { sleep_ms: 100 })); + + let mut executor = DagExecutor::new(EventBus::new()); + let start = std::time::Instant::now(); + let outputs = executor.run(&dag, serde_json::Value::Null).await.unwrap(); + let elapsed = start.elapsed(); + + assert_eq!(outputs.len(), 2); + // 串行需要约 200ms,并行应明显小于 180ms + assert!( + elapsed < Duration::from_millis(180), + "同层节点应并行执行,实际耗时 {:?}", + elapsed + ); + } + + #[tokio::test] + async fn test_layer_failure_aborts_following_layers() { + // a(失败) 与 b(成功) 同层,c 依赖 a,失败后 c 不应执行 + let mut dag = Dag::new(); + dag.add_node("a".to_string(), Box::new(FailNode)); + dag.add_node("b".to_string(), Box::new(SleepNode { sleep_ms: 10 })); + dag.add_node("c".to_string(), Box::new(SleepNode { sleep_ms: 10 })); + dag.add_edge("a".to_string(), "c".to_string()); + + let mut executor = DagExecutor::new(EventBus::new()); + let err = executor + .run(&dag, serde_json::Value::Null) + .await + .unwrap_err(); + assert!(err.to_string().contains("节点 a 执行失败")); + + // 同层成功节点状态正常更新,下游节点保持 Pending + use df_core::types::NodeStatus; + assert_eq!(executor.state_machine.get(&"a".to_string()), NodeStatus::Failed); + assert_eq!(executor.state_machine.get(&"b".to_string()), NodeStatus::Completed); + assert_eq!(executor.state_machine.get(&"c".to_string()), NodeStatus::Pending); + } +} diff --git a/crates/df-workflow/src/lib.rs b/crates/df-workflow/src/lib.rs new file mode 100644 index 0000000..fc6a3f2 --- /dev/null +++ b/crates/df-workflow/src/lib.rs @@ -0,0 +1,10 @@ +//! df-workflow: 工作流引擎 — DAG 定义、节点抽象、执行器、状态机、事件总线 + +pub mod conditions; +pub mod dag; +pub mod dag_def; +pub mod eventbus; +pub mod executor; +pub mod node; +pub mod registry; +pub mod state; diff --git a/crates/df-workflow/src/node.rs b/crates/df-workflow/src/node.rs new file mode 100644 index 0000000..a92828d --- /dev/null +++ b/crates/df-workflow/src/node.rs @@ -0,0 +1,83 @@ +//! 节点抽象:Node trait、NodeContext、NodeOutput + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use df_core::types::NodeId; + +use super::eventbus::EventBus; +use super::state::StateMachine; + +/// 节点输入/输出上下文 +#[derive(Debug, Clone)] +pub struct NodeContext { + /// 节点 ID + pub node_id: NodeId, + /// 上游节点输出(key 为上游节点 ID) + pub inputs: std::collections::HashMap, + /// 节点配置参数 + pub config: serde_json::Value, + /// 工作流执行 ID + pub execution_id: String, + /// 事件总线 + pub event_bus: EventBus, + /// 节点状态机(用于检查取消状态) + pub node_status: StateMachine, +} + +/// 节点执行输出 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeOutput { + /// 输出数据 + pub data: serde_json::Value, + /// 输出元数据 + pub metadata: std::collections::HashMap, +} + +impl NodeOutput { + /// 创建空输出 + pub fn empty() -> Self { + Self { + data: serde_json::Value::Null, + metadata: std::collections::HashMap::new(), + } + } + + /// 从 JSON 值创建输出 + pub fn from_value(data: serde_json::Value) -> Self { + Self { + data, + metadata: std::collections::HashMap::new(), + } + } +} + +/// 节点执行结果 +pub type NodeResult = anyhow::Result; + +/// 节点参数 Schema(JSON Schema 格式) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeSchema { + /// 参数 JSON Schema + pub params: serde_json::Value, + /// 输出 JSON Schema + pub output: serde_json::Value, +} + +/// 节点 trait — 所有工作流节点必须实现 +#[async_trait] +pub trait Node: Send + Sync { + /// 执行节点逻辑 + async fn execute(&self, ctx: NodeContext) -> NodeResult; + + /// 返回节点的参数 Schema + fn schema(&self) -> NodeSchema; + + /// 该节点是否为阻塞节点(阻塞工作流,等待外部输入) + fn is_blocking(&self) -> bool { + false + } + + /// 节点类型名称 + fn node_type(&self) -> &str; +} diff --git a/crates/df-workflow/src/registry.rs b/crates/df-workflow/src/registry.rs new file mode 100644 index 0000000..0bca197 --- /dev/null +++ b/crates/df-workflow/src/registry.rs @@ -0,0 +1,87 @@ +//! 节点注册表 — 根据 node_type 字符串创建节点实例 + +use std::collections::HashMap; + +use crate::dag::Dag; +use crate::dag_def::DagDef; +use crate::node::Node; + +/// 节点工厂函数类型 +type NodeFactory = Box Box + Send + Sync>; + +/// 节点注册表 — 根据 node_type 字符串创建节点实例 +pub struct NodeRegistry { + factories: HashMap, +} + +impl NodeRegistry { + /// 创建空的注册表 + pub fn new() -> Self { + Self { + factories: HashMap::new(), + } + } + + /// 注册一个节点工厂 + pub fn register(&mut self, type_name: &str, factory: F) + where + F: Fn(&serde_json::Value) -> Box + Send + Sync + 'static, + { + self.factories.insert(type_name.to_string(), Box::new(factory)); + } + + /// 根据 node_type 创建节点实例 + pub fn create(&self, type_name: &str, config: &serde_json::Value) -> anyhow::Result> { + self.factories + .get(type_name) + .ok_or_else(|| anyhow::anyhow!("未注册的节点类型: {}", type_name)) + .map(|factory| factory(config)) + } + + /// 从 DagDef 构建完整的运行时 Dag + pub fn build_dag(&self, def: &DagDef) -> anyhow::Result { + let mut dag = Dag::new(); + + // 创建所有节点实例 + for (id, node_def) in &def.nodes { + let node = self.create(&node_def.node_type, &node_def.config)?; + dag.add_node(id.clone(), node); + } + + // 添加所有边 + for edge_def in &def.edges { + match &edge_def.condition { + Some(cond) => dag.add_edge_with_condition( + edge_def.source.clone(), + edge_def.target.clone(), + cond.clone(), + ), + None => dag.add_edge(edge_def.source.clone(), edge_def.target.clone()), + } + } + + Ok(dag) + } + + /// 检查是否已注册指定类型 + pub fn is_registered(&self, type_name: &str) -> bool { + self.factories.contains_key(type_name) + } + + /// 列出所有已注册的节点类型 + pub fn registered_types(&self) -> Vec<&str> { + self.factories.keys().map(|s| s.as_str()).collect() + } +} + +impl Default for NodeRegistry { + fn default() -> Self { + let mut registry = Self::new(); + registry.register("script", |_config| { + // ScriptNode 的工厂 — 需要 df-nodes 依赖后才可用 + // 这里返回一个占位实现,实际项目中由 df-nodes crate 注册 + unimplemented!("ScriptNode 需要通过 df-nodes 注册") + }); + registry + } +} diff --git a/crates/df-workflow/src/state.rs b/crates/df-workflow/src/state.rs new file mode 100644 index 0000000..4309202 --- /dev/null +++ b/crates/df-workflow/src/state.rs @@ -0,0 +1,147 @@ +//! 节点状态机 — 管理节点的状态转换 +//! +//! 合法转换:Pending → Running,Running → Completed / Failed, +//! 非法转换返回错误,避免状态被随意覆盖。 + +use std::collections::HashMap; + +use df_core::types::{NodeId, NodeStatus}; + +/// 节点状态机 +#[derive(Debug, Clone)] +pub struct StateMachine { + /// 节点状态映射 + states: HashMap, +} + +impl StateMachine { + /// 创建状态机 + pub fn new() -> Self { + Self { + states: HashMap::new(), + } + } + + /// 获取节点状态 + pub fn get(&self, node_id: &NodeId) -> NodeStatus { + self.states + .get(node_id) + .cloned() + .unwrap_or(NodeStatus::Pending) + } + + /// 判断状态转换是否合法 + fn is_legal(from: &NodeStatus, to: &NodeStatus) -> bool { + matches!( + (from, to), + (NodeStatus::Pending, NodeStatus::Running) + | (NodeStatus::Running, NodeStatus::Completed) + | (NodeStatus::Running, NodeStatus::Failed) + ) + } + + /// 状态转换 — 校验合法性后更新,非法转换返回错误 + pub fn transition(&mut self, node_id: NodeId, target: NodeStatus) -> anyhow::Result<()> { + let current = self.get(&node_id); + if !Self::is_legal(¤t, &target) { + anyhow::bail!( + "节点 {} 状态转换非法:{} -> {}", + node_id, + current.as_str(), + target.as_str() + ); + } + self.states.insert(node_id, target); + Ok(()) + } + + /// 设置节点为运行中(仅允许 Pending -> Running) + pub fn set_running(&mut self, node_id: NodeId) -> anyhow::Result<()> { + self.transition(node_id, NodeStatus::Running) + } + + /// 设置节点为已完成(仅允许 Running -> Completed) + pub fn set_completed(&mut self, node_id: NodeId) -> anyhow::Result<()> { + self.transition(node_id, NodeStatus::Completed) + } + + /// 设置节点为失败(仅允许 Running -> Failed) + pub fn set_failed(&mut self, node_id: NodeId) -> anyhow::Result<()> { + self.transition(node_id, NodeStatus::Failed) + } + + /// 设置节点为等待中(暂未纳入转换校验) + pub fn set_waiting(&mut self, node_id: NodeId) { + self.states.insert(node_id, NodeStatus::Waiting); + } + + /// 设置节点为已跳过(暂未纳入转换校验) + pub fn set_skipped(&mut self, node_id: NodeId) { + self.states.insert(node_id, NodeStatus::Skipped); + } + + /// 获取所有状态快照 + pub fn snapshot(&self) -> &HashMap { + &self.states + } + + /// 检查节点是否被取消 + pub fn is_cancelled(&self, node_id: &NodeId) -> bool { + matches!(self.get(node_id), NodeStatus::Cancelled) + } +} + +impl Default for StateMachine { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_legal_transitions() { + let mut sm = StateMachine::new(); + let id = "node-a".to_string(); + + // Pending -> Running -> Completed 全程合法 + assert!(sm.set_running(id.clone()).is_ok()); + assert_eq!(sm.get(&id), NodeStatus::Running); + assert!(sm.set_completed(id.clone()).is_ok()); + assert_eq!(sm.get(&id), NodeStatus::Completed); + + // Running -> Failed 合法 + let id2 = "node-b".to_string(); + sm.set_running(id2.clone()).unwrap(); + assert!(sm.set_failed(id2.clone()).is_ok()); + assert_eq!(sm.get(&id2), NodeStatus::Failed); + } + + #[test] + fn test_illegal_transitions_rejected() { + let mut sm = StateMachine::new(); + let id = "node-a".to_string(); + + // Pending -> Completed 非法 + let err = sm.set_completed(id.clone()).unwrap_err(); + assert!(err.to_string().contains("状态转换非法")); + assert!(err.to_string().contains("node-a")); + // 状态保持不变 + assert_eq!(sm.get(&id), NodeStatus::Pending); + + // Pending -> Failed 非法 + assert!(sm.set_failed(id.clone()).is_err()); + + // Completed 为终态,不允许再转 Running + sm.set_running(id.clone()).unwrap(); + sm.set_completed(id.clone()).unwrap(); + assert!(sm.set_running(id.clone()).is_err()); + + // Running -> Running 重复置位非法 + let id2 = "node-b".to_string(); + sm.set_running(id2.clone()).unwrap(); + assert!(sm.set_running(id2.clone()).is_err()); + } +} diff --git a/docs/01-技术文档/SQLite-CRUD模式.md b/docs/01-技术文档/SQLite-CRUD模式.md new file mode 100644 index 0000000..3c95c21 --- /dev/null +++ b/docs/01-技术文档/SQLite-CRUD模式.md @@ -0,0 +1,38 @@ +# SQLite CRUD 模式 + +> 创建: 2026-06-10 | 状态: 初稿 + +--- + +## 概述 + +DevFlow 使用 SQLite (rusqlite) 作为本地存储引擎。df-storage 负责 SQLite 连接管理、Schema 迁移和 CRUD 操作。 + +## 当前状态 + +- SQLite 连接池: 已实现 +- Schema 迁移 (6 张表 + 4 索引): 已实现 +- CRUD 层: **待实施** + +## 设计要点 + +### 待实施内容 + +1. **泛型 CRUD trait** — 定义统一的 `Repository` 接口 +2. **SQL 构建** — 参数化查询,防止 SQL 注入 +3. **事务支持** — 跨表操作的原子性保证 +4. **批量操作** — `insert_batch` / `update_batch` +5. **查询构建器** — 条件查询、分页、排序 + +### 约定 + +- ID 字段统一使用 `TEXT` (UUID v4) +- 时间字段使用 `INTEGER` (Unix timestamp) +- JSON 字段使用 `TEXT` 存储 JSON 字符串 +- 所有写操作返回 `Result<(), df_core::error::Error>` + +## 相关文件 + +- `crates/df-storage/src/lib.rs` — 存储层入口 +- `crates/df-storage/src/schema.rs` — Schema 定义与迁移 +- `crates/df-core/src/error.rs` — 统一错误类型 diff --git a/docs/01-技术文档/Tauri-IPC模式.md b/docs/01-技术文档/Tauri-IPC模式.md new file mode 100644 index 0000000..d9df52f --- /dev/null +++ b/docs/01-技术文档/Tauri-IPC模式.md @@ -0,0 +1,47 @@ +# Tauri IPC 模式 + +> 创建: 2026-06-10 | 状态: 初稿 + +--- + +## 概述 + +Tauri v2 的 IPC 机制是 DevFlow 前后端通信的核心桥梁。本文档描述 DevFlow 中 Tauri IPC 的设计模式。 + +## 当前状态 + +- Tauri Commands: 仅 `greet` 示例命令 +- 业务 IPC: **待实施** + +## 设计要点 + +### 待实施内容 + +1. **Command 定义** — 在 `src-tauri/src/commands/` 下按模块组织 +2. **序列化约定** — Rust 结构体 derive `Serialize`/`Deserialize` +3. **错误传递** — `Result` 返回给前端 +4. **流式支持** — AI 输出、Shell 输出的流式推送 +5. **状态注入** — 通过 `tauri::State` 共享 Rust 运行时状态 + +### IPC 层次 + +``` +Vue Component + → Pinia Store (Action) + → @tauri-apps/api (invoke) + → Tauri Command (Rust) + → Crate 业务逻辑 + → df-storage (SQLite) +``` + +### 命名约定 + +- Rust command: `#[tauri::command] fn get_projects(...)` +- 前端调用: `invoke("get_projects", { ... })` +- Store action: `async fetchProjects()` + +## 相关文件 + +- `src-tauri/src/main.rs` — Tauri 入口 +- `src-tauri/src/commands/` — IPC 命令目录 +- `frontend/src/stores/` — Pinia Store diff --git a/docs/02-架构设计/Phase1架构决策.md b/docs/02-架构设计/Phase1架构决策.md new file mode 100644 index 0000000..1025b76 --- /dev/null +++ b/docs/02-架构设计/Phase1架构决策.md @@ -0,0 +1,46 @@ +# Phase 1 架构决策 + +> 创建: 2026-06-10 | 状态: 初稿 + +--- + +## 概述 + +Phase 1 是 DevFlow 的引擎骨架阶段,聚焦于核心数据流打通。本文记录此阶段的关键架构决策。 + +## 决策记录 + +### ADR-001: 引擎不绑定业务 + +- **决策**: Workflow Engine (df-workflow) 只做 DAG 执行,不感知具体业务语义 +- **原因**: 保持引擎通用性,阶段逻辑通过 df-stages 插件化注入 +- **影响**: df-workflow 的 Node trait 是纯接口,业务逻辑在 df-nodes / df-stages 实现 + +### ADR-002: 本地优先架构 + +- **决策**: 使用 SQLite 嵌入式数据库,不依赖云服务 +- **原因**: DevFlow 定位为桌面工具,零运维,离线可用 +- **影响**: 无网络层、无认证系统,数据全部本地存储 + +### ADR-003: 多 Crate Workspace + +- **决策**: 拆分为 13 个独立 crate +- **原因**: 模块解耦、独立编译、按需引用 +- **影响**: 依赖关系需严格管控,避免循环依赖 + +### ADR-004: 无 panic 原则 + +- **决策**: 所有占位代码返回空/默认值,不使用 `todo!`/`unimplemented!` +- **原因**: 保证应用不会因为未实现功能而崩溃 +- **影响**: 未实现的方法返回 `Ok(default)` 而非 panic + +### ADR-005: Phase 1 最小可用路径 + +- **决策**: 优先打通 `df-core → df-workflow → df-storage → Tauri IPC → Vue` 链路 +- **原因**: 验证架构可行性,尽早发现集成问题 +- **影响**: Phase 1 不实现 AI、想法池、多项目等高级功能 + +## 参考文档 + +- `ARCHITECTURE.md` — 完整架构设计 +- `PROGRESS.md` — 当前进度与全局性问题 diff --git a/docs/02-架构设计/业务系统设计.md b/docs/02-架构设计/业务系统设计.md new file mode 100644 index 0000000..d3bab24 --- /dev/null +++ b/docs/02-架构设计/业务系统设计.md @@ -0,0 +1,349 @@ +# DevFlow 业务系统设计 + +> 创建: 2026-06-10 | 状态: 设计中 | 基于: 功能审查结论 + +--- + +## 一、产品定位 + +| 维度 | 定义 | +|------|------| +| **一句话** | AI 原生的产研操作系统,从想法到上线的全流程编排 | +| **目标用户** | 个人开发者优先,后续扩展到小团队 | +| **核心价值** | 全流程编排 — 想法池 → 项目 → 任务 → 工作流 → 发布,AI 贯穿每个环节 | +| **差异化** | 想法第一公民 + AI 全程参与 + 本地优先(零运维) | + +--- + +## 二、用户旅程设计 + +### 2.1 核心旅程 + +``` +💡 想法池 📂 项目 🔀 任务 🚀 发布 +───────────────────────────────────────────────────────────────────────────────────── +捕捉想法 ──→ AI评估评分 ──→ 晋升立项 ──→ 创建任务 ──→ 绑定分支 ──→ 执行工作流 ──→ 合并发布 + │ │ │ │ │ + └── 淘汰/归档 └── 多任务 └── DAG └── AI辅助 └── 自动化 + 并行推进 自动执行 冲突解决 发布流程 +``` + +### 2.2 五个阶段详细设计 + +#### 阶段一:💡 想法池 (Idea Pool) + +**用户场景**:开发者日常产生大量想法(看到新技术、遇到痛点、产生产品灵感),需要一个地方快速捕捉、评估、筛选。 + +| 操作 | 描述 | AI 参与 | +|------|------|---------| +| **捕捉** | 文本输入、快捷键快速记录、剪贴板导入 | 无 | +| **评估** | AI 分析可行性、市场潜力、技术难度 | ⭐ 核心场景:LLM 评估报告 | +| **评分** | 多维打分 (可行性/影响力/紧迫性) | AI 给出建议分 | +| **关联** | 相似想法自动发现,可合并 | AI 语义相似度 | +| **晋升** | 高分想法晋升为项目 | AI 生成项目初始化建议 | +| **淘汰** | 低分想法归档或删除 | 无 | + +**状态机**: +``` +Draft → Evaluating → Scored → Approved → Promoted + → Rejected → Archived +``` + +**关键问题**: +- ✅ 想法是独立于项目的第一公民,不需要先有项目 +- ✅ AI 评估是核心差异化功能 +- ⚠️ 评分维度需要与实际对齐(当前有3套不同的维度定义) + +#### 阶段二:📂 项目 (Project) + +**用户场景**:从想法晋升或手动创建项目,管理项目全生命周期。 + +| 操作 | 描述 | AI 参与 | +|------|------|---------| +| **创建** | 从想法晋升 或 手动创建 | AI 生成项目描述/技术栈建议 | +| **阶段管理** | 5阶段管线:想法→需求→编码→测试→发布 | 阶段推进时 AI 检查前置条件 | +| **上下文** | 项目代码结构、依赖、规范 | AI 自动分析项目结构 | +| **暂停/恢复** | 项目可暂停后恢复 | 无 | + +**状态机**: +``` +Planning → InProgress → Testing → Releasing → Completed + → Paused → InProgress (恢复) + → Cancelled +``` + +**阶段管线**(current_stage,独立于 status): +``` +Idea → Requirement → Coding → Testing → Release +``` + +**关键问题**: +- ⚠️ 当前 `status`(项目生命周期)和 `current_stage`(当前阶段)是两个维度,前端混用了 +- ⚠️ 数据库 projects 表缺少 `current_stage`、`repo_path`、`priority`、`tags` 字段 + +#### 阶段三:🔀 任务 (Task) + +**用户场景**:项目内创建多个并行任务,每个任务绑定一个 Git 分支,独立工作流。 + +| 操作 | 描述 | AI 参与 | +|------|------|---------| +| **创建任务** | 标题+描述,自动创建分支 | AI 从需求拆解任务 | +| **绑定分支** | 每个任务一个独立分支 | 自动生成分支名 | +| **执行工作流** | 触发 DAG 工作流(编码→测试→审查) | AI 参与每个节点 | +| **审查** | 代码审查、质量检查 | AI 自动审查 | +| **合并** | 合并到主分支,冲突解决 | AI 辅助冲突解决 | + +**状态机**: +``` +Todo → InProgress → InReview → Testing → Done + → Blocked → InProgress (解除阻塞) + → Cancelled +``` + +**关键问题**: +- ⚠️ 缺少 `branches` 表,分支信息无法持久化 +- ⚠️ 任务到工作流的关联 (`workflow_def_id`) 缺失 +- ⚠️ 前端 TaskStatus 有 4 套不同的值 + +#### 阶段四:⚙️ 工作流 (Workflow) + +**用户场景**:DAG 驱动的工作流自动执行,支持条件分支、并行、人工审批。 + +| 组件 | 描述 | +|------|------| +| **DAG 定义** | 可序列化的节点+边定义,持久化到 SQLite | +| **节点类型** | Script / AI / Docker / Git / HTTP / Human / Notify / Subflow | +| **执行器** | 按拓扑层并行执行,支持暂停/恢复 | +| **事件总线** | 实时推送节点状态到前端 | +| **NodeRegistry** | 根据类型字符串动态创建节点实例 | + +**工作流执行生命周期**: +``` +Pending → Running → Completed + → Paused → Running (恢复) + → Failed → Running (重试) + → Cancelled +``` + +**关键问题**: +- ✅ DAG 拓扑排序算法正确 +- ✅ DagDef/NodeRegistry 已实现 +- ⚠️ Executor 同层节点尚未并行化 +- ⚠️ 条件分支引擎未实现 +- ⚠️ HumanNode(人工审批)暂停/恢复未连通 + +#### 阶段五:🚀 发布 (Release) + +**用户场景**:选择多个已完成任务,编排发布流程。 + +| 操作 | 描述 | AI 参与 | +|------|------|---------| +| **选择任务** | 选择要发布的 Done 状态任务 | 无 | +| **创建发布** | 合并分支到 release 分支 | AI 生成 changelog | +| **集成测试** | 运行完整测试工作流 | 自动 | +| **发布** | 部署 + 健康检查 | 自动 | +| **回滚** | 发布失败回滚 | AI 分析失败原因 | + +**状态机**: +``` +Planning → Integrating → Testing → Ready → Published + → RolledBack + → Cancelled +``` + +**关键问题**: +- ⚠️ 前端完全缺少发布入口 +- ⚠️ releases 表缺少 `branch_name`、`workflow_def_id` + +--- + +## 三、跨领域功能设计 + +### 3.1 标注系统 (Annotation) + +**设计理念**:任何内容(代码/文档/需求/测试报告)都可插入标注,统一收集后交给 AI 批量处理。 + +| 标记 | 含义 | AI 处理方式 | +|------|------|-----------| +| FIXME | 需要修复 | AI 定位问题并生成修复建议 | +| TODO | 待办 | AI 拆解为任务 | +| QUESTION | 疑问 | AI 尝试回答 | +| RISK | 风险 | AI 评估风险等级 | +| DECISION | 决策 | 自动记录到决策日志 | +| OPTIMIZE | 优化 | AI 给出优化方案 | + +**批量处理流程**: +``` +收集所有 Open 标注 → 按类型分组 → AI 逐条处理 → 标记为 Resolved +``` + +### 3.2 决策留痕 (Decision Journal) + +**设计理念**:所有关键决策自动或半自动记录,全程可追溯。 + +**自动记录的决策场景**: +- 想法评估结果(为什么批准/拒绝) +- 功能标记为"不做"时(为什么不做) +- AI 选择了方案 A 而非方案 B 时 +- 代码审查中发现风险时的处理决策 +- 发布前的检查点决策 + +### 3.3 经验进化 (Evolution) + +**设计理念**:开发过程自动沉淀知识,越用越聪明。 + +| 知识类型 | 来源 | 复用场景 | +|---------|------|---------| +| 审查规则 | 代码审查结论 | 后续审查自动应用 | +| Prompt 模板 | 成功的 AI 对话 | 类似场景复用 | +| 踩坑经验 | 错误修复过程 | 遇到类似问题时提醒 | +| 架构模式 | 项目结构分析 | 新项目初始化建议 | + +### 3.4 AI 编排 + +**多模型策略**: +``` +任务类型 → ModelRouter → 最优模型 + 代码生成 → Claude/GPT-4 + 代码审查 → Claude (长上下文) + 文档生成 → GLM/DeepSeek (性价比) + 快速问答 → DeepSeek (低成本) +``` + +**Agent 协作模式**(Phase 2+): +``` +Planner Agent → 拆解任务 +Coder Agent → 编码实现 +Reviewer Agent → 代码审查 +Fixer Agent → 修复问题 +``` + +--- + +## 四、数据模型设计(按阶段) + +### Phase 1 最小表集(当前 + 补全) + +| 表 | 用途 | 状态 | +|----|------|------| +| ideas | 想法池 | ✅ 已有,需补字段 | +| projects | 项目管理 | ✅ 已有,需补字段 | +| tasks | 任务管理 | ✅ 已有,需补字段 | +| releases | 发布管理 | ✅ 已有,需补字段 | +| workflow_defs | 工作流定义 | ❌ 缺失 | +| workflow_executions | 工作流执行 | ✅ 已有,需补字段 | +| node_executions | 节点执行记录 | ✅ 已有 | +| branches | 分支管理 | ❌ 缺失 | + +### Phase 2 扩展表 + +| 表 | 用途 | +|----|------| +| ai_providers | AI 模型配置 | +| connections | 连接配置 | +| artifacts | 产出物 | + +### Phase 3+ 完整表 + +| 表 | 用途 | +|----|------| +| annotations | 标注系统 | +| decisions | 决策留痕 | +| features | 需求功能清单 | +| test_cases | 测试用例 | +| test_runs | 测试执行记录 | +| knowledge | 经验知识库 | +| merge_requests | 合并请求 | + +--- + +## 五、关键设计决策 + +### D1: 想法是第一公民 +- 想法池独立于项目,可以独立运转 +- 想法不需要关联项目即可被评估和打分 +- 晋升是单向操作(想法→项目),但保留追溯 + +### D2: 多任务/分支并行 +- 同一项目内多个任务同时开发 +- 每个任务绑定独立 Git 分支 +- 任务间互不干扰,完成后合并 + +### D3: 引擎不绑定业务 +- DAG 引擎纯粹做编排,不知道"想法"/"项目"等概念 +- 阶段是 DAG 模板,可自定义 +- 节点通过 Node trait 扩展 + +### D4: 本地优先 +- SQLite 嵌入,不依赖云服务 +- 所有数据存储在本地 +- 零运维,安装即用 + +### D5: AI 贯穿全程 +- 不是"加了 AI 功能",而是"AI 是系统的一部分" +- 每个阶段都有 AI 参与 +- AI 输出作为决策依据,最终决策权在人 + +### D6: 决策必留痕 +- 所有关键决策自动记录 +- 决策可追溯到具体上下文(哪个想法、哪个功能、哪次审查) +- 未来可回溯"为什么这么做" + +--- + +## 六、审查发现的设计问题与决策 + +| # | 问题 | 设计决策 | 优先级 | +|---|------|---------|--------| +| 1 | 状态枚举三套不一致 | **以 types.rs 为准**,ARCHITECTURE.md 和 SQL 同步 | Phase 1 | +| 2 | projects 缺 status vs stage | **status 和 current_stage 分开**:status 管生命周期,stage 管进度 | Phase 1 | +| 3 | ideas.promoted_to 缺失 | **V2 补字段**,晋升时回写 | Phase 1 | +| 4 | branches 表不存在 | **V2 新增表**,分支管理需要持久化 | Phase 1 | +| 5 | workflow_executions 缺 project_id | **V2 补字段**,执行记录必须关联业务 | Phase 1 | +| 6 | DAG 不可序列化 | **DagDef/Dag 分离**(已完成) | Phase 1 | +| 7 | Executor 串行 | **同层并行化**(待实现) | Phase 1 | +| 8 | 前端 id 类型不对 | **统一为 string (UUID)** | Phase 1 | +| 9 | Store 未接入 View | **先建 API 层再接 Store** | Phase 1 | +| 10 | 标注/决策表缺失 | Phase 3 再建表,当前 UI 标注 "Coming Soon" | Phase 3 | +| 11 | 需求-测试追溯 | Phase 4 再建表 | Phase 4 | +| 12 | 经验进化 | Phase 5 实现 | Phase 5 | + +--- + +## 七、MVP 验证场景 + +**Phase 1 目标**:跑通"创建想法 → 晋升项目 → 创建任务 → 执行 3 节点工作流 → 查看结果" + +``` +1. 用户在想法池输入"做一个 Markdown 编辑器" +2. AI 评估可行性,给出评分和建议 +3. 用户点击"晋升为项目" +4. 系统创建项目,进入编码阶段 +5. 用户创建任务"实现基础编辑功能" +6. 系统创建分支 task/abc123 +7. 用户点击"运行工作流" +8. DAG 执行: [Shell: 环境检查] → [Shell: 运行测试] → [Shell: 构建产物] +9. 前端实时展示执行日志 +10. 执行完成,结果持久化到 SQLite +11. 用户刷新页面,数据仍在 +``` + +--- + +## 八、已确认的设计决策 + +### Q1: 想法评分维度 ✅ 已确认 +**决策**:采用 C 方案 — 可行性/影响力/紧迫性 + 综合分 (3+1 维) +- 与 `evaluator.rs` 已实现的 `EvalDimension` 对齐 +- `IdeaScores { feasibility, impact, urgency, overall }` 保留 + +### Q2: 发布模块 Phase 1 范围 ✅ 已确认 +**决策**:Phase 1 简化 — 只做 Release 记录 + 手动标记任务 +- releases 表保留,支持 CRUD +- 不做自动化发布流程(合并→测试→部署) +- 前端在 ProjectDetail 中添加简单 Release 面板 + +### Q3: AI 评估 Phase 1 范围 ✅ 已确认 +**决策**:Phase 1 用固定算法评分,延后接入 AI +- `ScoringEngine` 当前返回固定 5.0,改为基于启发式规则的简单算法 +- Phase 2 接入 LLM 后替换为 AI 评分 diff --git a/docs/02-架构设计/产品定位调整.md b/docs/02-架构设计/产品定位调整.md new file mode 100644 index 0000000..e7ff6cb --- /dev/null +++ b/docs/02-架构设计/产品定位调整.md @@ -0,0 +1,145 @@ +# 产品定位调整报告 + +> 从"想法到代码"到"想法到创作"的升级转型 + +## 📊 背景分析 + +### 原定位的问题 +- 过于狭窄:只面向开发者 +- 竞争激烈:代码工具市场已饱和 +- 限制场景:无法满足多元化的创作需求 + +### 新定位的优势 +- **普适性强**:覆盖所有创作者 +- **场景多样**:技术、商业、教育、创意 +- **价值更大**:从单一工具到创作平台 + +## 🎯 新产品定位 + +### 核心价值主张 +**"从想法到创作成果的全流程管理平台"** + +### 目标用户画像 +| 用户类型 | 创作内容 | 使用场景 | +|---------|---------|---------| +| **开发者** | 代码、技术文档 | 项目开发、技术分享 | +| **产品经理** | 需求文档、原型方案 | 产品规划、项目提案 | +| **设计师** | 设计方案、创意文档 | 设计展示、概念提案 | +| **研究者** | 学术论文、分析报告 | 研究、报告撰写 | +| **内容创作者** | 博客、课程、培训 | 知识分享、教育 | + +### 核心功能矩阵 + +| 功能模块 | 代码创作 | 文档创作 | 演示创作 | 内容创作 | +|---------|---------|---------|---------|---------| +| **想法池** | ✓ | ✓ | ✓ | ✓ | +| **对抗评估** | ✓ | ✓ | ✓ | ✓ | +| **任务管理** | ✓ | ✓ | ✓ | ✓ | +| **工作流引擎** | ✓ | ✓ | ✓ | ✓ | +| **模板系统** | ✓ | ✓ | ✓ | ✓ | +| **协作功能** | ✓ | ✓ | ✓ | ✓ | + +## 🔄 功能升级规划 + +### Phase 1: 基础创作支持 +- [x] 代码创作(现有) +- [ ] Markdown 文档编辑器 +- [ ] PPT 演示文稿生成器 +- [ ] 文档模板库 + +### Phase 2: 智能创作辅助 +- [ ] AI 内容生成 +- [ ] 自动格式化 +- [ ] 多格式导出 +- [ ] 版本管理 + +### Phase 3: 创作生态 +- [ ] 创作市场 +- [ ] 团队协作 +- [ ] 知识库集成 +- [ ] 发布平台 + +## 📈 产品差异化 + +### 竞争优势 +1. **全流程覆盖**:从想法到发布 +2. **对抗式评估**:独特的质量控制机制 +3. **本地优先**:数据安全、隐私保护 +4. **跨平台支持**:桌面端 + Web 端 + +### 差异化场景 +- **学术研究**:论文写作 + 代码实现 +- **产品设计**:需求文档 + 原型设计 +- **技术培训**:课程内容 + 示例代码 +- **创意策划**:概念方案 + 可视化展示 + +## 🎨 视觉识别调整 + +### 品牌口号 +- **原**:"本地优先的开发流程工具" +- **新**:"创意工作流的私人助手" + +### 颜色系统 +保持现有的紫色系作为主色,增加: +- 橙色:创意和活力 +- 绿色:成长和产出 +- 蓝色:专业和可靠 + +### 图标体系 +- 🧠 思维导图 +- ✍️ 创作工具 +- 📊 产出展示 +- 🚀 发布分享 + +## 📋 实施计划 + +### 短期(1个月) +- 更新文档和营销材料 +- 完成文档创作基础功能 +- 发布 V2.0 版本 + +### 中期(3个月) +- 完成演示文稿生成器 +- 实现模板系统 +- 上线创作市场 Beta + +### 长期(6个月) +- 建立创作者生态 +- 集成协作功能 +- 考虑 SaaS 服务 + +## 🎯 成功指标 + +### 用户增长 +- 月活跃用户:1000+ +- 创作者留存率:>60% +- 日均创作次数:>3/用户 + +### 内容质量 +- 作品发布率:>40% +- 用户满意度:>4.5/5 +- 重复使用率:>50% + +### 业务指标 +- 付费转化率:>5% +- 平均客单价:$50 +- 年营收目标:$100K + +## 🔮 未来展望 + +### 5年愿景 +成为**创作者的首选工作平台**,连接想法与世界的桥梁。 + +### 技术演进 +- AI 驱动的智能创作 +- 虚拟现实创作空间 +- 区块链确权和版权保护 + +### 社会价值 +- 降低创作门槛 +- 促进知识分享 +- 支持创意经济 + +--- + +**总结**:从"想法到代码"到"想法到创作"不是功能减少,而是价值升级。我们不再局限于技术工具,而是成为所有创作者的伙伴。 \ No newline at end of file diff --git a/docs/02-架构设计/前后端类型对齐.md b/docs/02-架构设计/前后端类型对齐.md new file mode 100644 index 0000000..a720250 --- /dev/null +++ b/docs/02-架构设计/前后端类型对齐.md @@ -0,0 +1,82 @@ +# 前后端类型对齐 + +> 创建: 2026-06-10 | 状态: 初稿 + +--- + +## 概述 + +DevFlow 前端 (TypeScript) 和后端 (Rust) 通过 Tauri IPC 和 JSON 序列化通信。两侧的类型定义必须保持一致。 + +## 类型映射 + +### 基础类型 + +| Rust | TypeScript | 说明 | +|------|-----------|------| +| `String` | `string` | 文本字段 | +| `i64` | `number` | 时间戳 (Unix timestamp) | +| `Option` | `string \| null` | 可空字段 | +| `Vec` | `string[]` | 数组 (JSON 序列化) | +| `bool` | `boolean` | 布尔值 | + +### ID 类型 + +| 实体 | Rust | TypeScript | 格式 | +|------|------|-----------|------| +| 所有 ID | `String` | `string` | UUID v4 | + +所有 ID 统一使用 UUID v4 字符串,便于前后端传递。 + +### 枚举对齐 + +#### IdeaStatus (8 值) + +| Rust | TypeScript | +|------|-----------| +| `Draft` | `"Draft"` | +| `Evaluating` | `"Evaluating"` | +| `Scored` | `"Scored"` | +| `Hot` | `"Hot"` | +| `Promoted` | `"Promoted"` | +| `Parked` | `"Parked"` | +| `Merged` | `"Merged"` | +| `Discarded` | `"Discarded"` | + +#### TaskStatus (6 值) + +| Rust | TypeScript | +|------|-----------| +| `Created` | `"Created"` | +| `BranchCreated` | `"BranchCreated"` | +| `InProgress` | `"InProgress"` | +| `ReviewReady` | `"ReviewReady"` | +| `Merged` | `"Merged"` | +| `Abandoned` | `"Abandoned"` | + +#### WorkflowRunStatus (6 值) + +| Rust | TypeScript | +|------|-----------| +| `Pending` | `"Pending"` | +| `Running` | `"Running"` | +| `Paused` | `"Paused"` | +| `Completed` | `"Completed"` | +| `Failed` | `"Failed"` | +| `Cancelled` | `"Cancelled"` | + +#### ProjectStatus (4 值) + +| Rust | TypeScript | +|------|-----------| +| `Active` | `"Active"` | +| `Paused` | `"Paused"` | +| `Completed` | `"Completed"` | +| `Archived` | `"Archived"` | + +## 约定 + +1. 枚举值使用 PascalCase 字符串,前后端保持一致 +2. JSON 字段(如 `scores`、`tags`)在 Rust 侧用 `TEXT` 存储 JSON 字符串,前端解析为对象/数组 +3. 时间字段统一用 `i64` (Unix timestamp),前端用 `new Date(ts * 1000)` 转换 +4. 新增枚举值时,Rust 和 TypeScript 两侧必须同步更新 diff --git a/docs/02-架构设计/对抗论证裁决报告.md b/docs/02-架构设计/对抗论证裁决报告.md new file mode 100644 index 0000000..4139b9c --- /dev/null +++ b/docs/02-架构设计/对抗论证裁决报告.md @@ -0,0 +1,143 @@ +# DevFlow 对抗论证裁决报告 + +> 创建: 2026-06-11 | 方法: 三路对抗论证(市场/技术/需求) | 结论: 方向有价值,scope 必须砍 + +--- + +## 一、论证方法 + +用"魔法打败魔法":三个独立 AI 代理分别从不同立场攻击这个项目,互不可见,最后综合裁决。 + +| 代理 | 立场 | 综合评分 | +|------|------|---------| +| 市场分析师 | 竞品全景 + 市场数据(带外部信源) | **4/10 — 不建议以当前形态推进** | +| 技术架构师 | 13 crate / Tauri / 引擎 / AI 可行性 | **2.7/5 — 可行但必须砍 scope** | +| 恶魔代言人 | 逐功能质疑需求真实性 | **核心成立,60% 功能该砍** | + +--- + +## 二、三方共识(站不住脚的地方) + +### 共识 1:🔴 Scope 失控是最大风险 + +- 8 个核心功能横跨 4-5 个产品类别(PM + 工作流 + AI 编排 + 代码分析 + 知识库) +- 22,745 字架构文档、16 张表、13 crate —— **这是操作系统的野心,不是 MVP 的规划** +- 现实工时:v1.0 全功能需全职 8-12 个月 / 业余 1.5-2 年 +- 历史教训:Firebase/Heroku/全生命周期 API 平台都被"组件化组合"打败 + +### 共识 2:🔴 身份危机 — 无法一句话说清 + +- Cursor = "AI 编辑器",Linear = "快的 issue 追踪器",DevFlow = ? +- "全流程"不是定位,是定位的缺失 + +### 共识 3:🔴 部分功能是伪需求(对个人开发者) + +| 功能 | 需求方评分 | 市场方评分 | 裁决 | +|------|-----------|-----------|------| +| 插件系统 (WASM) | 1/5 | — | **砍掉**(为 0 个用户设计生态) | +| 经验进化(自动沉淀) | 1/5 | 3/10 | **砍掉**(技术上无法落地,连骨架都是空的) | +| 需求-测试追溯 | 1/5 | — | **砍掉**(企业需求硬塞给个人) | +| 多模型路由 + Agent 协作 | — | 4/10 | **砍掉**(OpenClaw/大厂赛道,无法竞争) | +| 想法池(完整版) | 2/5 | 2/10 | **降级**(简化为列表+对抗式评估) | +| 决策留痕(独立子系统) | 2/5 | 4/10 | **降级**(字段级方案,不建独立体系) | + +### 共识 4:🟡 AI 信任危机的时代背景 + +- Stack Overflow 2025:开发者对 AI 信任度从 40% 跌至 29% +- 66% 开发者花更多时间修复 AI 的"差不多对"代码 +- **定位必须从"AI 帮你做事"转向"AI 帮你控制流程"**——AI 是副驾驶,不是自动驾驶 + +--- + +## 三、三方冲突点的调和 + +### 冲突 1:DAG 工作流引擎 + +- 需求方:**5/5 必须有**(产品的灵魂,本地工作流有空白) +- 市场方:**2/10 伪需求**(GitHub Actions 已免费解决) + +**裁决**:两者都对,但说的是不同的东西。 +- GitHub Actions 解决的是 **仓库内 CI/CD**(push 触发、云端跑) +- DevFlow 的空隙是 **本地的、跨项目的、AI 参与的交互式流程**(不依赖 push、可以有人工审批节点、能调用本地资源) +- **保留 DAG 引擎,但作为内部基础设施,不作为对外卖点**。用户看到的是"一键执行编码→审查→测试流程",而不是"DAG 编辑器" + +### 冲突 2:标注系统 + +- 需求方:**1/5 砍掉**(IDE 已解决) +- 市场方:**6.5/10 全场最高差异化机会**("扫描代码库 FIXME/TODO → AI 批量处理 → 关联任务"是真空地带) + +**裁决**:需求方批的是"在文档/测试报告上加标注"的重模式(确实没人用);市场方挺的是"代码库 TODO 扫描器 + AI 处理"的轻模式。 +- **采用轻模式**:扫描代码 TODO/FIXME → 汇总 → AI 生成处理建议 → 一键转任务 +- 砍掉"任意实体标注"的重设计 + +--- + +## 四、站得住脚的部分 + +1. **本地优先** — Obsidian 证明了本地优先工具有大市场,订阅疲劳(52% 用户因此退订)反推一次性付费 +2. **Tauri 技术栈** — 比 Electron 小 96%,选型正确(4/5) +3. **引擎代码质量** — 拓扑排序算法正确、trait 设计合理、依赖树无环 +4. **任务绑 Git 分支** — 6/10,最有价值的业务功能,做深"任务→分支→提交→合并→关单"全链路有空间 +5. **对抗式想法评估** — 这是本次论证方法本身的产品化,市场上没有工具这么做 + +--- + +## 五、调整方案 + +### 5.1 产品定位调整 + +**旧**:AI 原生的产研操作系统,从想法到上线的全流程编排(❌ 无法一句话说清) + +**新**:**本地优先的个人开发流程驾驶舱 — 把想法、任务、分支和 AI 流程放进一个不联网也能跑的桌面应用** + +一句话版本:「你的项目流程,本地跑,AI 辅助,数据不出门」 + +### 5.2 功能调整清单 + +| 功能 | 原计划 | 调整后 | +|------|--------|--------| +| DAG 工作流引擎 | 对外核心卖点 | ✅ 保留为内部引擎,UI 上呈现为"流程模板一键执行" | +| 任务+分支 | 一般功能 | ✅ **升级为核心**:任务→分支→工作流→合并全链路 | +| 想法池 | 完整漏斗系统 | ⬇️ 简化:列表 + **对抗式评估**(正方/反方/分析师三路论证,差异化卖点) | +| 标注系统 | 任意实体标注 | ⬇️ 简化:代码库 TODO/FIXME 扫描器 + AI 批处理 | +| 决策留痕 | 独立子系统 | ⬇️ 降级:关键操作自动写决策字段,无独立 UI | +| AI 编排 | 多模型路由+四 Agent | ⬇️ 砍:单 Provider (Claude) + AI 节点,无 Agent 协作 | +| 阶段插件 | 5 阶段 | ⬇️ 3 个内置流程模板(编码/测试/发布) | +| 8 种节点 | 全部实现 | ⬇️ Phase 1 只做 Shell/AI/Subflow,Git 用 Shell 调 CLI | +| 需求-测试追溯 | 完整体系 | ❌ 砍掉(v2.0 团队版再说) | +| 经验进化 | 自动沉淀引擎 | ❌ 砍掉(降为手动 Snippet 收藏,Phase 5 再议) | +| 插件系统 | WASM/动态加载 | ❌ 砍掉(v2.0 再说) | + +**砍掉比例:约 60%,与三方建议一致。** + +### 5.3 架构调整 + +- **13 crate 保留目录结构**(已建好,删除反而费工),但 Phase 1 只激活 6 个: + `df-core / df-workflow / df-storage / df-execute / df-nodes / src-tauri` +- 其余 7 个 crate 标记为 `[预留]`,从 workspace 默认构建中保留但不再投入开发 +- Git 操作走 Shell CLI,不引入 libgit2(技术报告建议,省 1-2 周) + +### 5.4 Phase 重排 + +| Phase | 旧目标 | 新目标 | +|-------|--------|--------| +| 1 | 引擎骨架(含一切基础) | **跑通一条 Shell→AI→Shell 工作流 + 任务/分支 CRUD + 前端真数据** | +| 2 | AI 集成(多模型) | Claude 单 Provider + AI 节点 + 想法对抗式评估 | +| 3 | 想法池+多项目 | TODO 扫描器 + 3 个流程模板 | +| 4 | 节点丰富+阶段插件 | 任务→分支→合并全链路(Git 深度集成) | +| 5 | 体验打磨 v1.0 | 打磨 + 自用验证 3 个月 → 决定是否对外 | + +### 5.5 验证策略调整 + +市场报告的最重要建议:**先验证再深投**。 +- DevFlow 首先是**自用工具**(管理 wk-* 工作空间的真实项目) +- 自用 3 个月,记录每天真实打开次数 +- 如果自己都不用,停止投入;如果离不开它,再考虑对外 + +--- + +## 六、裁决结论 + +> **方向有价值,形态要收敛。** "本地优先 + 任务分支驱动 + AI 辅助流程"是真空隙;"全流程操作系统"是幻觉。砍掉 60% 的功能不是失败,是论证的胜利——它们本来会消耗 6-9 个月却没人用。 +> +> 同时,本次论证方法本身(三路对抗)被产品化为想法池的"对抗式评估"功能,详见 `docs/03-模块文档/想法探索-对抗式评估.md`。这是 DevFlow 吃自己的狗粮的第一个案例。 diff --git a/docs/03-模块文档/df-ai-AI集成模块.md b/docs/03-模块文档/df-ai-AI集成模块.md new file mode 100644 index 0000000..ebf78b7 --- /dev/null +++ b/docs/03-模块文档/df-ai-AI集成模块.md @@ -0,0 +1,298 @@ +# df-ai - AI 集成模块 + +> Provider 抽象层与流式响应处理 + +## 📋 模块概览 + +`df-ai` 负责 AI 功能的核心集成,支持多个 AI Provider,提供统一的接口和流式响应处理。 + +### 主要特性 +- 多 Provider 支持(OpenAI、Anthropic、DeepSeek) +- 流式响应处理 +- 工具调用支持 +- 错误处理和重试机制 + +## 🏗️ 架构设计 + +### 核心组件 +```rust +// Provider trait 定义 +pub trait AIProvider: Send + Sync { + async fn chat_completion(&self, request: ChatRequest) -> Result; + async fn create_embedding(&self, text: &str) -> Result>; +} + +// 流式响应处理 +pub struct StreamProcessor { + event_sender: mpsc::UnboundedSender, +} + +// 统一的 AI 服务 +pub struct AIService { + providers: HashMap>, + default_provider: String, +} +``` + +### Provider 实现 +- **OpenAIProvider**: OpenAI GPT 系列模型 +- **AnthropicProvider**: Claude 系列模型 +- **DeepSeekProvider**: DeepSeek 模型 + +## 🔧 使用方法 + +### 基础聊天 +```rust +use df_ai::AIService; + +let ai_service = AIService::new(config); +let request = ChatRequest { + model: "gpt-4".to_string(), + messages: vec![Message { + role: "user".to_string(), + content: "Hello, world!".to_string(), + }], +}; + +let response = ai_service.chat_completion(request).await?; +``` + +### 流式响应 +```rust +use df_ai::stream_chat; + +let (mut receiver, mut stream) = stream_chat(&ai_service, request).await?; + +while let Some(event) = receiver.recv().await { + match event { + StreamEvent::Content(chunk) => { + print!("{}", chunk); + } + StreamEvent::Done => { + println!("\n完成"); + } + StreamEvent::Error(e) => { + eprintln!("错误: {}", e); + } + } +} +``` + +### 工具调用 +```rust +let request = ChatRequest { + model: "gpt-4".to_string(), + messages: vec![Message { + role: "user".to_string(), + content: "创建一个文件".to_string(), + }], + tools: vec![Tool { + r#type: "function".to_string(), + function: FunctionDef { + name: "create_file".to_string(), + description: "创建文件".to_string(), + parameters: Parameters { + r#type: "object".to_string(), + properties: serde_json::json!({ + "path": {"type": "string"}, + "content": {"type": "string"} + }), + required: vec!["path".to_string()], + }, + }, + }], + tool_choice: "auto".to_string(), +}; +``` + +## ⚙️ 配置 + +### Provider 配置 +```yaml +# config/ai.yaml +providers: + openai: + api_key: ${OPENAI_API_KEY} + base_url: "https://api.openai.com/v1" + model: "gpt-4" + max_tokens: 4000 + temperature: 0.7 + + anthropic: + api_key: ${ANTHROPIC_API_KEY} + model: "claude-3-sonnet-20240229" + max_tokens: 4000 + temperature: 0.7 + + deepseek: + api_key: ${DEEPSEEK_API_KEY} + model: "deepseek-chat" + max_tokens: 4000 + temperature: 0.7 + +default_provider: "openai" +``` + +### 环境变量 +```bash +export OPENAI_API_KEY="sk-your-key" +export ANTHROPIC_API_KEY="sk-ant-key" +export DEEPSEEK_API_KEY="your-key" +``` + +## 🔄 错误处理 + +### 错误类型 +```rust +pub enum AIError { + APIError(String), // API 调用失败 + Timeout, // 请求超时 + RateLimit, // 达到速率限制 + InvalidResponse, // 响应格式错误 + ProviderNotFound, // Provider 不存在 + ConfigurationError, // 配置错误 +} +``` + +### 重试机制 +```rust +let config = RetryConfig { + max_attempts: 3, + backoff: ExponentialBackoff::from_millis(1000), + retryable_errors: vec![ + AIError::Timeout, + AIError::RateLimit, + ], +}; + +let response = ai_service.chat_with_retry(request, &config).await?; +``` + +## 📊 性能优化 + +### 缓存机制 +```rust +pub struct CachedAIService { + inner: AIService, + cache: Arc>>, +} + +// 缓存键生成 +fn cache_key(request: &ChatRequest) -> String { + format!("{:?}-{:?}", request.model, request.messages) +} +``` + +### 连接池 +```rust +pub struct ConnectionPool { + connections: HashMap>, + max_connections: usize, +} + +pub async fn get_client(&self, provider: &str) -> Result { + // 从连接池获取或创建新连接 +} +``` + +## 🔍 监控与日志 + +### 请求追踪 +```rust +pub struct RequestTracer { + request_id: String, + start_time: Instant, + metrics: RequestMetrics, +} + +impl RequestTracer { + pub fn log_request(&self, provider: &str, duration: Duration) { + metrics.record_request(provider, duration); + } +} +``` + +### 指标收集 +```rust +pub struct RequestMetrics { + total_requests: AtomicU64, + successful_requests: AtomicU64, + failed_requests: AtomicU64, + average_duration: AtomicDuration, +} +``` + +## 🧪 测试 + +### 单元测试 +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_chat_completion() { + let ai_service = AIService::new(test_config()); + let request = test_request(); + let response = ai_service.chat_completion(request).await; + assert!(response.is_ok()); + } +} +``` + +### 集成测试 +```rust +#[tokio::test] +async fn test_multiple_providers() { + let providers = vec!["openai", "anthropic"]; + for provider in providers { + let ai_service = AIService::new(config_for_provider(provider)); + let response = test_chat(&ai_service).await; + assert!(response.is_ok(), "Provider {} failed", provider); + } +} +``` + +## 🚨 最佳实践 + +### 1. 错误处理 +```rust +// ✅ 正确 +match ai_service.chat_completion(request).await { + Ok(response) => handle_response(response), + Err(AIError::RateLimit) => wait_and_retry(), + Err(e) => log_error_and_notify(e), +} + +// ❌ 错误 - 忽略错误 +let _ = ai_service.chat_completion(request).await; +``` + +### 2. 资源管理 +```rust +// ✅ 正确 - 使用连接池 +let client = connection_pool.get_client("openai").await?; + +// ❌ 错误 - 每次创建新连接 +let client = Client::new(config); +``` + +### 3. 并发控制 +```rust +// ✅ 正确 - 使用信号量 +let semaphore = Arc::new(Semaphore::new(10)); +let permit = semaphore.acquire().await?; +let response = ai_service.chat_completion(request).await; + +// ❌ 错误 - 无限制并发 +let handles: Vec<_> = requests.into_iter().map(|req| { + tokio::spawn(ai_service.chat_completion(req)) +}).collect(); +``` + +--- + +**相关文档**: +- [df-storage - 存储层](./df-storage-存储层.md) +- [df-workflow - 工作流引擎](./df-workflow-工作流引擎.md) +- [df-nodes - 节点集合](./df-nodes-节点集合.md) \ No newline at end of file diff --git a/docs/03-模块文档/df-nodes-节点集合.md b/docs/03-模块文档/df-nodes-节点集合.md new file mode 100644 index 0000000..3f771ee --- /dev/null +++ b/docs/03-模块文档/df-nodes-节点集合.md @@ -0,0 +1,74 @@ +# df-nodes 节点集合 + +> 创建: 2026-06-10 | 状态: 初稿 + +--- + +## 概述 + +df-nodes 提供 DevFlow 工作流引擎的 8 种内置节点。所有节点实现 df-workflow 的 `Node` trait。 + +## 当前状态 + +所有 8 种节点 Schema 已定义完整,`execute()` 方法均为空实现。 + +## 节点清单 + +| 节点 | 功能 | 阻塞 | 实现状态 | +|------|------|------|---------| +| AINode | 调用 LLM,流式输出,工具调用 | 否 | 骨架 | +| ScriptNode | Shell/脚本执行 | 否 | 骨架 | +| DockerNode | Docker 容器操作 | 否 | 骨架 | +| GitNode | Git 操作 (libgit2) | 否 | 骨架 | +| HumanNode | 人工审批/确认 | 是 | 骨架 | +| NotifyNode | 通知 (桌面/飞书/Webhook) | 否 | 骨架 | +| HTTPNode | HTTP 请求 | 否 | 骨架 | +| SubflowNode | 嵌套子工作流 | 否 | 骨架 | + +## 实现优先级 + +Phase 1 阶段优先实现: + +1. **ScriptNode** — 依赖 df-execute 的 Shell 执行器 (已可用) +2. **HumanNode** — 阻塞节点,工作流审批需要 + +Phase 2 实现: + +3. **AINode** — 依赖 df-ai Provider 实现 + +Phase 4 实现: + +4. **DockerNode** — 依赖 bollard crate +5. **GitNode** — 依赖 libgit2 +6. **HTTPNode** — HTTP 客户端 +7. **NotifyNode** — 通知渠道对接 +8. **SubflowNode** — 嵌套工作流引擎 + +## 依赖关系 + +``` +df-core + ← df-workflow (Node trait) + ← df-nodes + ← df-ai (AINode) + ← df-execute (ScriptNode) +``` + +## 文件结构 + +``` +crates/df-nodes/src/ +├── lib.rs — 模块入口,注册所有节点 +├── ai_node.rs — AI 节点 +├── script_node.rs — 脚本节点 +├── docker_node.rs — Docker 节点 +├── git_node.rs — Git 节点 +├── human_node.rs — 人工审批节点 +├── notify_node.rs — 通知节点 +├── http_node.rs — HTTP 节点 +└── subflow_node.rs — 子工作流节点 +``` + +## 相关文档 + +- [df-workflow 工作流引擎](./df-workflow-工作流引擎.md) diff --git a/docs/03-模块文档/df-storage-存储层.md b/docs/03-模块文档/df-storage-存储层.md new file mode 100644 index 0000000..0b820a6 --- /dev/null +++ b/docs/03-模块文档/df-storage-存储层.md @@ -0,0 +1,53 @@ +# df-storage 存储层 + +> 创建: 2026-06-10 | 状态: 初稿 + +--- + +## 概述 + +df-storage 是 DevFlow 的数据持久化层,基于 SQLite (rusqlite),负责连接管理、Schema 迁移和 CRUD 操作。 + +## 当前状态 + +| 功能 | 状态 | +|------|------| +| SQLite 连接管理 | ✅ 已实现 | +| Schema 迁移 (6 张表) | ✅ 已实现 | +| CRUD 操作 | ⬜ 待实施 | +| 事务支持 | ⬜ 待实施 | + +## 数据表 + +Phase 1 已创建的 6 张核心表: + +1. **ideas** — 想法池 +2. **projects** — 项目 +3. **tasks** — 任务 +4. **workflow_defs** — 工作流定义 +5. **workflow_runs** — 工作流执行 +6. **artifacts** — 产出物 + +完整表结构见 `ARCHITECTURE.md` 数据模型章节。 + +## 依赖关系 + +``` +df-core (错误类型、ID 生成) + ← df-storage +``` + +## 文件结构 + +``` +crates/df-storage/src/ +├── lib.rs — 模块入口,导出公共 API +├── connection.rs — SQLite 连接管理 +├── schema.rs — Schema 定义与迁移 +└── crud.rs — CRUD 操作 (待创建) +``` + +## 相关文档 + +- [SQLite CRUD 模式](../01-技术文档/SQLite-CRUD模式.md) +- [前后端类型对齐](../02-架构设计/前后端类型对齐.md) diff --git a/docs/03-模块文档/df-workflow-工作流引擎.md b/docs/03-模块文档/df-workflow-工作流引擎.md new file mode 100644 index 0000000..ecaaa4f --- /dev/null +++ b/docs/03-模块文档/df-workflow-工作流引擎.md @@ -0,0 +1,81 @@ +# df-workflow 工作流引擎 + +> 创建: 2026-06-10 | 状态: 初稿 + +--- + +## 概述 + +df-workflow 是 DevFlow 的核心引擎,负责 DAG 定义、拓扑排序、节点执行、状态流转和事件广播。引擎不感知具体业务逻辑。 + +## 当前状态 + +| 功能 | 状态 | +|------|------| +| DAG 数据结构 | ✅ 已实现 | +| 拓扑排序 | ✅ 已实现 | +| DagExecutor (顺序执行) | ✅ 已实现 | +| 同层节点并行执行 | ⬜ 有 TODO 注释 | +| Node trait 定义 | ✅ 已实现 | +| 状态机 (WorkflowRunStatus) | ✅ 已实现 | +| EventBus (broadcast) | ✅ 已实现 | +| 条件表达式引擎 | ⚡ 仅支持 true/false | +| 断点续跑 | ⬜ 待实施 | + +## 核心设计 + +### Node trait + +```rust +pub trait Node: Send + Sync { + fn execute(&self, ctx: &NodeContext) -> Result; + fn schema(&self) -> NodeSchema; + fn is_blocking(&self) -> bool { true } +} +``` + +### DAG 执行流程 + +``` +1. 接收 WorkflowDef (DAG 定义) +2. 拓扑排序 → 得到执行层 (layers) +3. 逐层执行: + - 同层节点并行 (TODO) + - 阻塞节点等待人工操作 + - 非阻塞节点异步完成 +4. 状态变更通过 EventBus 广播 +5. 每个节点完成后持久化快照 +``` + +### 事件类型 + +- `WorkflowStarted` / `WorkflowCompleted` / `WorkflowFailed` +- `NodeStarted` / `NodeCompleted` / `NodeFailed` +- `WorkflowPaused` / `WorkflowResumed` + +## 依赖关系 + +``` +df-core (类型、事件、错误) + ← df-workflow + ← df-nodes (节点实现) + ← df-stages (阶段节点) +``` + +## 文件结构 + +``` +crates/df-workflow/src/ +├── lib.rs — 模块入口 +├── dag.rs — DAG 数据结构与拓扑排序 +├── executor.rs — DagExecutor +├── node.rs — Node trait + NodeContext/Output +├── state.rs — 状态机 +├── event.rs — EventBus +└── condition.rs — 条件表达式引擎 +``` + +## 相关文档 + +- [df-nodes 节点集合](./df-nodes-节点集合.md) +- [Phase1 架构决策](../02-架构设计/Phase1架构决策.md) diff --git a/docs/03-模块文档/想法探索-对抗式评估.md b/docs/03-模块文档/想法探索-对抗式评估.md new file mode 100644 index 0000000..0e03e9a --- /dev/null +++ b/docs/03-模块文档/想法探索-对抗式评估.md @@ -0,0 +1,488 @@ +# 想法探索模块 — 对抗式评估设计 + +> 创建: 2026-06-10 | 状态: 设计中 + +--- + +## 一、核心理念 + +### 魔法打败魔法 + +传统 AI 评估是"AI 打分 → 给建议",问题是 **AI 倾向于说好话**。大模型天生乐观,给它一个想法,它会说"这个想法很有潜力",因为训练数据里充满了创业成功故事。 + +DevFlow 的做法:**让 AI 同时扮演正方和反方,对抗辩论,最终综合裁决。** + +``` +传统评估: 想法 → AI评分 → "8/10,建议推进" ← 一言堂,容易乐观 +DevFlow: 想法 → 正方论证 vs 反方质疑 → 裁决 ← 对抗制,逼出真问题 +``` + +### 设计灵感 + +这正是我们验证 DevFlow 自身的方式——三路代理同时论证: +1. 竞品与市场可行性(外部威胁) +2. 技术可行性(内部能力) +3. 需求真实性(用户价值) + +**把这个方法论产品化**,每个想法都经历同样的三路对抗论证。 + +--- + +## 二、评估流程设计 + +### 2.1 三阶段对抗评估 + +``` +💡 想法输入 + │ + ▼ +┌─────────────────────────────────────────┐ +│ 第一轮: 三路独立论证 (并行) │ +│ │ +│ 🟢 正方辩护者 — 为什么值得做 │ +│ 🔴 反方质疑者 — 为什么会失败 │ +│ 🔵 冷眼分析师 — 客观数据和事实 │ +│ │ +│ 三路独立输出,互不可见 │ +└─────────────┬───────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ 第二轮: 交叉质询 │ +│ │ +│ 正方看到反方论点 → 尝试反驳或承认弱点 │ +│ 反方看到正方辩护 → 寻找更多漏洞 │ +│ 分析师汇总双方 → 补充客观数据 │ +└─────────────┬───────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ 第三轮: 裁决 │ +│ │ +│ 📊 综合评分 (不再是简单的 1-10) │ +│ ├── 价值分: 解决的问题有多痛? │ +│ ├── 可行分: 个人能力能否实现? │ +│ ├── 差异分: 有没有别人没做的? │ +│ └── 风险分: 最坏情况有多坏? │ +│ │ +│ 📝 最终建议: 强烈推荐 / 值得探索 / │ +│ 需要更多信息 / 建议暂缓 │ +│ │ +│ ⚠️ 致命风险清单: 必须回答的问题 │ +└─────────────────────────────────────────┘ +``` + +### 2.2 三个角色详细设计 + +#### 🟢 正方辩护者 (Advocate) + +**目标**:找出这个想法值得做的所有理由。 + +| 论证维度 | Prompt 方向 | +|---------|-----------| +| 用户痛点 | 这个问题真实存在吗?有多痛?谁在痛? | +| 解决方案 | 想法的核心价值主张是什么?解决了什么? | +| 市场时机 | 为什么是现在做而不是一年前或一年后? | +| 个人优势 | 以你的技术栈和能力,做这个有什么独特优势? | +| 最小可行 | 最简版本可以多简单?MVP 的核心功能是什么? | + +**输出格式**: +``` +## 正方论证 + +### 核心价值 +[一段话描述这个想法为什么有价值] + +### 支撑论据 +1. [论据1 + 证据] +2. [论据2 + 证据] +3. [论据3 + 证据] + +### 个人匹配度 +[技术栈/经验/资源的匹配分析] + +### MVP 建议 +[最小可行产品的核心功能清单] +``` + +#### 🔴 反方质疑者 (Devil's Advocate) + +**目标**:找出这个想法会失败的所有可能原因。**越狠越好。** + +| 质疑维度 | Prompt 方向 | +|---------|-----------| +| 伪需求 | 用户真的会为此付费/花时间吗?还是"我觉得需要"? | +| 竞品碾压 | 现有工具是否已经解决了这个问题? | +| 技术幻觉 | 技术上真的可行吗?还是被新技术蒙蔽了判断? | +| 资源黑洞 | 需要多少时间/精力?机会成本是什么? | +| 维护陷阱 | 做出来之后,维护成本有多高? | +| 规模天花板 | 个人开发者工具的市场有多大? | + +**输出格式**: +``` +## 反方质疑 + +### 致命风险 (任意一条成立则建议放弃) +1. [风险1 + 为什么是致命的] +2. [风险2 + 为什么是致命的] + +### 严重问题 (不致命但会显著增加难度) +1. [问题1 + 影响评估] +2. [问题2 + 影响评估] + +### 竞品威胁 +[列出已有的解决方案,它们做了什么,差距在哪] + +### 历史教训 +[类似产品/项目失败的原因] +``` + +#### 🔵 冷眼分析师 (Analyst) + +**目标**:提供客观事实和数据,不偏不倚。 + +| 分析维度 | Prompt 方向 | +|---------|-----------| +| 市场数据 | 这类工具的市场规模、增长趋势 | +| 技术趋势 | 依赖的核心技术成熟度、发展方向 | +| 成功案例 | 类似产品的成功经验 | +| 失败案例 | 类似产品的失败教训 | +| 工时估算 | 粗略的工时估算(MVP/完整版) | + +**输出格式**: +``` +## 客观分析 + +### 事实清单 +1. [事实 + 来源] +2. [事实 + 来源] + +### 竞品对照表 +| 特性 | DevFlow想法 | 竞品A | 竞品B | +|------|-----------|-------|-------| + +### 工时估算 +- MVP: ~X 周 +- 完整版: ~X 月 + +### 关键假设 +[这个想法成立所依赖的关键假设清单] +``` + +--- + +## 三、裁决机制 + +### 3.1 四维评分(替代单一分数) + +传统评分:"可行性 8/10" — 过于笼统。 + +DevFlow 四维评分: + +| 维度 | 含义 | 权重 | 来源 | +|------|------|------|------| +| **价值分** (Value) | 解决的痛点有多强?用户愿意花多少时间? | 30% | 正方 + 分析师 | +| **可行分** (Feasibility) | 以个人能力,技术上能否实现? | 25% | 反方质疑 + 分析师 | +| **差异分** (Differentiation) | 与现有方案的差异化有多大? | 25% | 分析师竞品对照 | +| **风险分** (Risk, 反向) | 最坏情况有多坏?失败概率多大? | 20% | 反方致命风险 | + +**综合分** = Value×0.3 + Feasibility×0.25 + Diff×0.25 + (10-Risk)×0.2 + +### 3.2 建议等级 + +不再给"推荐/不推荐"二元判断,而是分级: + +| 等级 | 条件 | 含义 | +|------|------|------| +| 🟢 **强烈推荐** | 综合分 ≥ 8 且无致命风险 | 痛点明确,差异化清晰,技术可行 | +| 🟡 **值得探索** | 综合分 6-7.9 或有 1 个可控风险 | 方向对,但需要验证关键假设 | +| 🟠 **需要更多信息** | 关键假设无法验证 | 先做小实验验证假设,再评估 | +| 🔴 **建议暂缓** | 综合分 < 6 或有 ≥2 个致命风险 | 痛点不清晰或竞品已覆盖 | + +### 3.3 致命风险清单 + +无论综合分多高,只要有"致命风险"就高亮警告: + +``` +⚠️ 致命风险 (必须回答) +━━━━━━━━━━━━━━━━━━━━━━ +❓ 竞品 X 已经实现了 80% 的功能,你的差异化在哪? +❓ 个人开发者市场验证过吗?有多少人表达过需求? +❓ 维护 13 个 Rust crate 的长期成本你考虑过吗? +``` + +--- + +## 四、数据模型 + +### 想法评估记录 + +```sql +-- 对抗式评估记录 +CREATE TABLE idea_evaluations ( + id TEXT PRIMARY KEY, + idea_id TEXT NOT NULL REFERENCES ideas(id), + + -- 正方 + advocate_args TEXT, -- JSON: 正方论据列表 + advocate_score REAL, -- 正方给的分数 + + -- 反方 + devil_args TEXT, -- JSON: 反方质疑列表 + devil_risks TEXT, -- JSON: 致命风险列表 + devil_score REAL, -- 反方给的分数 + + -- 分析师 + analyst_facts TEXT, -- JSON: 客观事实 + analyst_competitors TEXT, -- JSON: 竞品对照 + analyst_estimate_hours REAL, -- 工时估算 + + -- 裁决 + value_score REAL, -- 价值分 + feasibility_score REAL, -- 可行分 + differentiation_score REAL, -- 差异分 + risk_score REAL, -- 风险分 + overall_score REAL, -- 综合分 + + recommendation TEXT, -- 强烈推荐/值得探索/需要更多信息/建议暂缓 + fatal_risks TEXT, -- JSON: 致命风险清单 + key_assumptions TEXT, -- JSON: 关键假设清单 + + evaluated_at TEXT NOT NULL +); +``` + +### Rust 数据结构 + +```rust +pub struct IdeaEvaluation { + pub id: String, + pub idea_id: String, + + // 三路论证 + pub advocate: AdvocateReport, + pub devil: DevilReport, + pub analyst: AnalystReport, + + // 裁决 + pub verdict: Verdict, +} + +pub struct AdvocateReport { + pub core_value: String, + pub arguments: Vec, + pub personal_fit: String, + pub mvp_suggestion: Vec, + pub score: f64, +} + +pub struct DevilReport { + pub fatal_risks: Vec, + pub serious_issues: Vec, + pub competitor_threats: Vec, + pub historical_lessons: Vec, + pub score: f64, +} + +pub struct AnalystReport { + pub facts: Vec, + pub competitor_comparison: Vec, + pub estimate_hours_mvp: f64, + pub key_assumptions: Vec, +} + +pub struct Verdict { + pub value_score: f64, + pub feasibility_score: f64, + pub differentiation_score: f64, + pub risk_score: f64, + pub overall: f64, + pub recommendation: Recommendation, + pub fatal_risks: Vec, +} + +pub enum Recommendation { + StronglyRecommended, + WorthExploring, + NeedMoreInfo, + Defer, +} +``` + +--- + +## 五、AI Prompt 设计 + +### 正方 Prompt 模板 + +``` +你是一个技术创业导师。你的任务是为一项软件开发想法找出所有【值得做】的理由。 +你要像这个想法的联合创始人一样思考,积极寻找它的价值。 + +想法: {{title}} +描述: {{description}} + +请从以下角度论证: +1. 用户痛点 — 这个问题真实存在吗?有多痛? +2. 核心价值 — 一句话说明这个产品做什么 +3. 市场时机 — 为什么是现在? +4. 技术可行性 — 实现这个想法的核心技术栈是什么? +5. 个人优势 — 做这个想法的人有什么独特优势? +6. MVP 建议 — 最简版本只需要哪些功能? + +你要有理有据,不要空喊口号。每个论点都要有具体的证据或类比。 +``` + +### 反方 Prompt 模板 + +``` +你是一个严厉的技术投资人,你的工作是【找出这个想法会失败的原因】。 +你要像在投资评审会上一样苛刻,不放过任何潜在问题。 + +想法: {{title}} +描述: {{description}} + +请从以下角度质疑: +1. 伪需求检查 — 用户真的需要这个吗?还是"我觉得需要"? +2. 竞品碾压 — 有没有现有工具已经解决了这个问题?搜索并列出至少 3 个竞品 +3. 技术幻觉 — 技术上真的可行吗?有没有低估的技术难点? +4. 资源黑洞 — 需要多少时间?机会成本是什么? +5. 维护陷阱 — 做出来后维护成本有多高? +6. 规模天花板 — 这个领域的市场天花板有多高? + +区分"致命风险"(任何一个成立就该放弃)和"严重问题"(增加难度但不致命)。 +越是致命的问题,越要给出具体的证据和案例。 +``` + +### 分析师 Prompt 模板 + +``` +你是一个中立的行业分析师。你的任务是提供【客观事实】,不偏袒任何一方。 + +想法: {{title}} +描述: {{description}} + +请提供: +1. 事实清单 — 列出你确认为真的相关事实(市场规模、技术趋势、用户数据等) +2. 竞品对照 — 搜索并列出同类产品,对比功能差异 +3. 工时估算 — 粗略估算 MVP 和完整版的开发时间 +4. 关键假设 — 这个想法成立依赖哪些假设?哪些假设最可能不成立? + +不要给建议,只给事实。如果你不确定某个事实,标注"待验证"。 +``` + +### 裁决 Prompt 模板 + +``` +你是一个公正的裁决者。你收到了三份报告,请综合裁决。 + +【正方论证】 +{{advocate_report}} + +【反方质疑】 +{{devil_report}} + +【客观分析】 +{{analyst_report}} + +请给出: +1. 四维评分 (每项 1-10): + - 价值分: 解决的痛点有多强? + - 可行分: 个人开发者能否实现? + - 差异分: 与现有方案的差异化有多大? + - 风险分: (反向) 失败风险有多大? + +2. 综合建议: 强烈推荐 / 值得探索 / 需要更多信息 / 建议暂缓 + +3. 致命风险清单: 列出所有"如果这个问题成立,就不该做"的风险 + +4. 关键假设: 这个想法成立所依赖的关键假设 +``` + +--- + +## 六、前端展示设计 + +### 想法详情页 — 对抗评估面板 + +``` +┌─────────────────────────────────────────────────┐ +│ 💡 想法: AI 原生产研操作系统 │ +│ 状态: Scored │ 综合分: 7.2 🟡 值得探索 │ +├─────────────────────────────────────────────────┤ +│ │ +│ ┌─── 📊 四维雷达图 ──────────────────────┐ │ +│ │ 价值 9.2 │ │ +│ │ ╱ ╲ │ │ +│ │ 差异 8.1 可行 6.5 │ │ +│ │ ╲ ╱ │ │ +│ │ 风险 5.8 │ │ +│ └────────────────────────────────────────┘ │ +│ │ +│ ┌── 🟢 正方辩护 ─────── 📊 8.5/10 ──────┐ │ +│ │ 核心价值: 个人开发者缺少全流程工具 │ │ +│ │ ✓ 痛点真实 — 工具碎片化严重 │ │ +│ │ ✓ AI 原生 — 现有工具未深度整合 AI │ │ +│ │ ✓ 技术匹配 — Rust+Vue 正好是强项 │ │ +│ └────────────────────────────────────────┘ │ +│ │ +│ ┌── 🔴 反方质疑 ─────── 📊 4.2/10 ──────┐ │ +│ │ ⚠️ 致命: Claude Code 已在做全流程 │ │ +│ │ ⚠️ 致命: 个人开发者工具付费意愿极低 │ │ +│ │ ⚠️ 严重: 13 crate 维护成本高 │ │ +│ │ ⚠️ 严重: 桌面应用市场天花板明显 │ │ +│ └────────────────────────────────────────┘ │ +│ │ +│ ┌── 🔵 客观分析 ─────────────────────────┐ │ +│ │ 📊 竞品: n8n(工作流) Linear(项目管理) │ │ +│ │ 🕐 MVP 估算: 4-6 周 │ │ +│ │ 📝 关键假设: AI 编排是否真比手动高效 │ │ +│ └────────────────────────────────────────┘ │ +│ │ +│ ⚠️ 致命风险清单 │ +│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ +│ ❓ Claude Code/Cursor 等工具已覆盖大部分场景 │ +│ ❓ 个人开发者愿意为一个工具投入多少学习成本? │ +│ │ +│ [晋升为项目] [重新评估] [归档] │ +└─────────────────────────────────────────────────┘ +``` + +--- + +## 七、与其他模块的联动 + +### 晋升为项目时 + +当想法晋升为项目时,评估结论自动携带: +- 正方的 MVP 建议变成项目的初始任务 +- 反方的风险清单变成项目的待解决风险 +- 分析师的关键假设变成需要验证的检查点 + +### 决策留痕 + +对抗评估本身就是一次重大决策,自动进入决策日志: +- 决策:是否推进这个想法 +- 正方论据 / 反方论据 / 最终决定 +- 如果后续项目失败,可以回溯到评估阶段看"当时反方说了什么" + +### 经验进化 + +评估的 Prompt 模板会持续优化: +- 如果一个想法评估为"强烈推荐"但最终失败了,反方 Prompt 需要加强 +- 如果一个想法被"建议暂缓"但后来被证明是好的,正方 Prompt 需要加强 +- 形成"评估校准"的反馈闭环 + +--- + +## 八、实现计划 + +| Phase | 内容 | 依赖 | +|-------|------|------| +| Phase 2 | 实现正方+反方+分析师三路 LLM 调用 | df-ai LlmProvider 实现 | +| Phase 2 | 新增 idea_evaluations 表 | Migrations V3 | +| Phase 2 | 前端对抗评估面板 | 前端 Store 对接 | +| Phase 3 | 交叉质询(第二轮)| 多轮对话能力 | +| Phase 5 | 评估校准(经验进化)| df-evolve 模块 | + +Phase 1 的想法评估继续使用固定算法评分,但数据模型预留 evaluation 字段。 diff --git a/docs/04-功能迭代/DEVFLOW-1.CRUD层实施.md b/docs/04-功能迭代/DEVFLOW-1.CRUD层实施.md new file mode 100644 index 0000000..3c83804 --- /dev/null +++ b/docs/04-功能迭代/DEVFLOW-1.CRUD层实施.md @@ -0,0 +1,37 @@ +# DEVFLOW-1 CRUD 层实施 + +> 创建: 2026-06-10 | 状态: 待实施 + +--- + +## 概述 + +为 df-storage 实现完整的 CRUD 操作层,使所有 crate 能够持久化数据。这是 Phase 1 的首要任务,所有后续功能的前置条件。 + +## 目标 + +- [ ] 定义泛型 `Repository` trait +- [ ] 实现 ideas 表 CRUD +- [ ] 实现 projects 表 CRUD +- [ ] 实现 tasks 表 CRUD +- [ ] 实现 workflow_defs 表 CRUD +- [ ] 实现 workflow_runs 表 CRUD +- [ ] 实现 artifacts 表 CRUD +- [ ] 事务支持 +- [ ] 单元测试 + +## 实施记录 + +*(待填写)* + +## 变更文件 + +*(待填写)* + +## 遗留问题 + +*(待填写)* + +## 下一步 + +*(待填写)* diff --git a/docs/04-功能迭代/DEVFLOW-2.IPC桥接实施.md b/docs/04-功能迭代/DEVFLOW-2.IPC桥接实施.md new file mode 100644 index 0000000..ce6d761 --- /dev/null +++ b/docs/04-功能迭代/DEVFLOW-2.IPC桥接实施.md @@ -0,0 +1,35 @@ +# DEVFLOW-2 IPC 桥接实施 + +> 创建: 2026-06-10 | 状态: 待实施 + +--- + +## 概述 + +建立 Tauri IPC 命令层,将 Rust 后端业务逻辑暴露给 Vue 前端。依赖 DEVFLOW-1 (CRUD 层) 完成。 + +## 目标 + +- [ ] 按 CRUD 模块组织 commands 目录结构 +- [ ] 实现 projects 相关 commands +- [ ] 实现 tasks 相关 commands +- [ ] 实现 workflow 相关 commands +- [ ] 统一错误处理 (Rust Error → 前端字符串) +- [ ] 状态注入 (AppState) +- [ ] 前端 invoke 封装 + +## 实施记录 + +*(待填写)* + +## 变更文件 + +*(待填写)* + +## 遗留问题 + +*(待填写)* + +## 下一步 + +*(待填写)* diff --git a/docs/04-功能迭代/DEVFLOW-3.Store对接实施.md b/docs/04-功能迭代/DEVFLOW-3.Store对接实施.md new file mode 100644 index 0000000..3926003 --- /dev/null +++ b/docs/04-功能迭代/DEVFLOW-3.Store对接实施.md @@ -0,0 +1,35 @@ +# DEVFLOW-3 Store 对接实施 + +> 创建: 2026-06-10 | 状态: 待实施 + +--- + +## 概述 + +将 Vue 前端的硬编码数据替换为 Pinia Store,通过 Tauri IPC 连接真实后端数据。依赖 DEVFLOW-2 (IPC 桥接) 完成。 + +## 目标 + +- [ ] Store 添加 async actions (调用 invoke) +- [ ] ProjectsView 接入 projectStore +- [ ] TasksView 接入 taskStore +- [ ] WorkflowView 接入 workflowStore +- [ ] IdeasView 接入 ideaStore (如有) +- [ ] 加载状态 / 错误状态 UI 处理 +- [ ] 按钮事件绑定真实操作 + +## 实施记录 + +*(待填写)* + +## 变更文件 + +*(待填写)* + +## 遗留问题 + +*(待填写)* + +## 下一步 + +*(待填写)* diff --git a/docs/04-功能迭代/DEVFLOW-4.端到端验证.md b/docs/04-功能迭代/DEVFLOW-4.端到端验证.md new file mode 100644 index 0000000..a0155e4 --- /dev/null +++ b/docs/04-功能迭代/DEVFLOW-4.端到端验证.md @@ -0,0 +1,41 @@ +# DEVFLOW-4 端到端验证 + +> 创建: 2026-06-10 | 状态: 待实施 + +--- + +## 概述 + +端到端验证 Phase 1 骨架:在 DevFlow 中跑通一个 3 节点的简单工作流。依赖 DEVFLOW-1/2/3 全部完成。 + +## 验证目标 + +- [ ] 创建项目 → SQLite 持久化 → 前端显示 +- [ ] 创建任务 → 绑定工作流 → 前端显示 +- [ ] 启动工作流 → 3 节点顺序执行 → 状态实时更新到前端 +- [ ] 工作流完成 → 产出物记录 → 前端查看 + +## 验证工作流设计 + +``` +3 节点简单工作流: + ScriptNode (echo "start") + → ScriptNode (echo "processing") + → ScriptNode (echo "done") +``` + +## 实施记录 + +*(待填写)* + +## 变更文件 + +*(待填写)* + +## 遗留问题 + +*(待填写)* + +## 下一步 + +*(待填写)* diff --git a/docs/06-前端开发/View改造指南.md b/docs/06-前端开发/View改造指南.md new file mode 100644 index 0000000..a5616eb --- /dev/null +++ b/docs/06-前端开发/View改造指南.md @@ -0,0 +1,97 @@ +# View 改造指南 — 硬编码到 Store 对接 + +> 创建: 2026-06-10 | 状态: 初稿 + +--- + +## 概述 + +当前所有 Vue 页面使用硬编码数据 (`ref([...])`),Pinia Store 和 View 完全独立。本文档指导如何将 View 改造为通过 Store → IPC 获取真实数据。 + +## 当前问题 + +1. **数据硬编码** — 每个 View 用 `ref([...])` 定义假数据 +2. **Store 未接入** — Pinia Store 有 state/getter 但无 action +3. **事件处理空函数** — 按钮点击绑定 `() => {}` +4. **Arco Design 未使用** — 仅 CSS 变量覆盖,未引入组件 + +## 改造步骤 + +### Step 1: Store 添加 Actions + +```typescript +// stores/projectStore.ts +import { invoke } from '@tauri-apps/api/core' + +export const useProjectStore = defineStore('project', () => { + const projects = ref([]) + + // 新增 action + async function fetchProjects() { + projects.value = await invoke('get_projects') + } + + async function createProject(input: CreateProjectInput) { + const project = await invoke('create_project', { input }) + projects.value.push(project) + } + + return { projects, fetchProjects, createProject } +}) +``` + +### Step 2: View 接入 Store + +```typescript +// views/ProjectsView.vue — 改造前 +const projects = ref([ + { id: '1', name: '项目A', ... }, // 硬编码 +]) + +// views/ProjectsView.vue — 改造后 +const store = useProjectStore() +const { projects } = storeToRefs(store) + +onMounted(() => { + store.fetchProjects() +}) +``` + +### Step 3: 事件绑定真实操作 + +```typescript +// 改造前 +const handleCreate = () => {} + +// 改造后 +const handleCreate = async () => { + await store.createProject(form) + modalVisible.value = false +} +``` + +### Step 4: 加载与错误状态 + +```vue + +``` + +## 涉及页面 + +| 页面 | Store | 改造优先级 | +|------|-------|-----------| +| ProjectsView | projectStore | P0 | +| TasksView | taskStore | P0 | +| WorkflowView | workflowStore | P0 | +| IdeasView | ideaStore | P1 | +| DashboardView | 多个 Store | P2 | + +## 相关文档 + +- [Tauri IPC 模式](../01-技术文档/Tauri-IPC模式.md) +- [前后端类型对齐](../02-架构设计/前后端类型对齐.md) +- [DEVFLOW-3 Store 对接实施](../04-功能迭代/DEVFLOW-3.Store对接实施.md) diff --git a/docs/07-项目管理/Phase1任务清单.md b/docs/07-项目管理/Phase1任务清单.md new file mode 100644 index 0000000..d212b49 --- /dev/null +++ b/docs/07-项目管理/Phase1任务清单.md @@ -0,0 +1,65 @@ +# Phase 1 任务清单 + +> 创建: 2026-06-10 | 状态: 进行中 + +--- + +## 概述 + +Phase 1 目标:**引擎骨架**,打通 `df-core → df-workflow → df-storage → Tauri IPC → Vue` 最小可用路径。 + +预计周期:4-6 周 + +## 任务总览 + +### 已完成 + +| # | 任务 | 状态 | 说明 | +|---|------|------|------| +| 1 | df-core 类型系统 | ✅ 完成 | 错误/事件/状态枚举/ID 生成 | +| 2 | df-workflow DAG 引擎 | ✅ 完成 | 拓扑排序/执行器/状态机/EventBus | +| 3 | df-storage SQLite 基础表 | ✅ 完成 | 6 张表 + 4 索引 | +| 4 | df-execute Shell 执行 | ✅ 完成 | tokio::process 实现 | +| 5 | 13 个 Crate 骨架 | ✅ 完成 | 类型/接口/Schame 定义完整 | +| 6 | 9 个 Vue 页面 UI | ✅ 完成 | 设计系统 + 硬编码数据 | + +### 待实施 (按优先级排序) + +| # | 任务 | 优先级 | 依赖 | 文档 | +|---|------|--------|------|------| +| 7 | df-storage CRUD 层 | P0 | 无 | [DEVFLOW-1](../04-功能迭代/DEVFLOW-1.CRUD层实施.md) | +| 8 | Tauri IPC 命令层 | P0 | #7 | [DEVFLOW-2](../04-功能迭代/DEVFLOW-2.IPC桥接实施.md) | +| 9 | Store 接入 View | P0 | #8 | [DEVFLOW-3](../04-功能迭代/DEVFLOW-3.Store对接实施.md) | +| 10 | 端到端验证 (3 节点工作流) | P0 | #7, #8, #9 | [DEVFLOW-4](../04-功能迭代/DEVFLOW-4.端到端验证.md) | +| 11 | 首次 Git Commit | P0 | 无 | 建立版本基线 | + +### 已知问题 + +| # | 问题 | 优先级 | 说明 | +|---|------|--------|------| +| 1 | 同层节点未并行执行 | P1 | DagExecutor 有 TODO 注释 | +| 2 | 条件表达式引擎 | P2 | 仅支持 true/false 字面量 | +| 3 | i18n 未注册 | P2 | 翻译文件存在但未挂载 | +| 4 | AI Provider 无实现 | P1 | Phase 2 任务 | + +## 依赖关系 + +``` +#7 CRUD 层 ──→ #8 IPC 桥接 ──→ #9 Store 对接 ──→ #10 端到端验证 + ↑ + #7 + #8 + #9 +``` + +## 代码规模 (当前) + +| 类别 | 文件数 | 行数 | +|------|--------|------| +| Rust 后端 | 71 | ~4,108 | +| Vue 前端 | ~20 | ~3,896 | +| **总计** | ~91 | **~8,000** | + +## 参考文档 + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — 完整架构设计 +- [PROGRESS.md](../../PROGRESS.md) — 工作进展与交接 +- [Phase1 架构决策](../02-架构设计/Phase1架构决策.md) diff --git a/docs/07-项目管理/Phase2计划.md b/docs/07-项目管理/Phase2计划.md new file mode 100644 index 0000000..22e74aa --- /dev/null +++ b/docs/07-项目管理/Phase2计划.md @@ -0,0 +1,108 @@ +# Phase 2 开发计划 + +> 本地优先开发流程验证 - 2026-06-11 至 2026-09-11 + +## 🎯 阶段目标 + +### 核心目标 +- **验证核心价值链**:任务→分支→工作流→合并的完整流程 +- **自用验证 3 个月**:每天使用,记录真实场景 +- **聚焦差异化**:对抗式评估作为核心卖点 + +### 成功标准 +- 自己每天都在用 +- 解决实际问题 +- 流程顺畅无卡点 + +## 📋 任务清单 + +### Phase 2.1: 核心链路验证 (2周) +- [x] 验证任务创建和分支管理 +- [x] 验证工作流执行和日志 +- [x] 验证 Git 集成 +- [x] 验证 AI Chat 功能 + +### Phase 2.2: 想法池完善 (3周) +- [ ] 基础 CRUD 功能 +- [ ] 简单评分系统 +- [ ] 标签和分类 +- [ ] 搜索和过滤 + +### Phase 2.3: 对抗式评估 (4周) +- [ ] 正方观点生成 +- [ ] 反方观点生成 +- [ ] 分析师综合分析 +- [ ] 评估报告生成 + +### Phase 2.4: 工作流增强 (3周) +- [ ] 自定义工作流编辑器 +- [ ] 更多节点类型 +- [ ] 条件分支支持 +- [ ] 并行执行优化 + +## 🔄 优先级排序 + +### P0 - 必须完成 +1. **核心链路验证**:确保基本流程可用 +2. **自用记录**:每天使用日志 +3. **对抗式评估 MVP**:基础三路论证 + +### P1 - 重要功能 +1. **想法池基础功能**:记录和管理想法 +2. **工作流自定义**:支持简单自定义 +3. **UI/UX 优化**:提升用户体验 + +### P2 - 可选增强 +1. **团队协作**:多用户支持 +2. **数据导出**:备份和分享 +3. **插件系统**:扩展功能 + +## 📊 关键指标 + +### 使用指标 +- **日活用户**:至少自己每天使用 +- **任务完成率**:> 80% 任务按计划完成 +- **工作流成功率**:> 90% 自动执行成功 + +### 质量指标 +- **响应时间**:< 1s(本地操作) +- **错误率**:< 5% +- **用户满意度**:自用评分 > 4/5 + +## 🚀 交付物 + +### 阶段性成果 +1. **第1个月**:核心功能可用,开始自用 +2. **第2个月**:想法池 + 对抗评估完善 +3. **第3个月**:工作流增强,评估是否对外 + +### 最终交付 +- 可用的 DevFlow 应用 +- 3 个月使用报告 +- 产品定位调整建议 + +## 🎯 风险与应对 + +### 主要风险 +1. **自用动力不足** + - 应对:设置使用提醒 + - 应对:建立使用习惯 + +2. **功能过于复杂** + - 应对:保持极简原则 + - 应对:砍掉非核心功能 + +3. **技术债务累积** + - 应对:定期重构 + - 应对:单元测试覆盖 + +### 成功退出条件 +如果满足以下条件,可以开始考虑对外发布: +- 连续 3 个月每天使用 +- 核心功能无重大 Bug +- 对抗式评估效果显著 +- 明确的产品差异化 + +--- + +**回顾**: Phase 1 已完成基础架构,Phase 2 专注于验证价值 \ No newline at end of file diff --git a/docs/08-用户指南/使用手册.html b/docs/08-用户指南/使用手册.html new file mode 100644 index 0000000..82cf08e --- /dev/null +++ b/docs/08-用户指南/使用手册.html @@ -0,0 +1,716 @@ + + + + + +DevFlow 使用手册 + + + + + +

🚀 DevFlow 使用手册

+

"本地优先的个人创作流程驾驶舱"

+ + +
+

核心流程

+
+

想法到创作成果的完整链路

+
+
+
想法池 → 对抗评估 → 创作(代码/文档/PPT) → 发布/分享
+  
+
+
+ 评估 + 创作 + 执行 + 发布 +
+
+ + +
+

🏃 快速开始

+
+ 💻 +
+ 运行应用
+ bun install
+ bun run tauri dev +
+
+
+ 💡 +
+ 一句话功能
+ 你的创作流程,本地跑,AI 辅助,数据不出门 +
+
+
+ + +
+

🎯 核心价值链

+

想法 → 评估 → 创作 → 发布

+ +
+ 💡 +
+ 想法管理
+ 收集、分类、评估创作想法
+ 支持多种类型的创作灵感 +
+
+ +
+ ⚖️ +
+ 对抗评估
+ 正方观点 + 反方观点 + 分析师建议
+ 确保创作质量和价值 +
+
+ +
+ 🎨 +
+ 创作执行
+ 代码、文档、PPT 等多种创作形式
+ AI 辅助和模板支持 +
+
+ +
+ 📤 +
+ 发布分享
+ 多格式导出、版本管理
+ 一键发布到各平台 +
+
+
+ + +
+

📋 功能模块

+

聚焦核心,拒绝过设计

+ +
+ 🤖 +
+ AI Chat
+ 单 Provider 简化配置
+ 工具调用:代码生成、文件操作、Git 操作
+ 流式响应:实时输出 +
+
+ +
+ 💡 +
+ 想法池
+ 轻量版:聚焦核心
+ 预留对抗评估:后续升级 +
+
+ +
+ 📂 +
+ 项目管理
+ 多项目支持
+ 与 Git 分支联动 +
+
+ +
+ 📚 +
+ 知识库(收藏夹)
+ 静态收集:个人经验碎片
+ 分类管理:审查规则、Prompt 模板、踩坑经验 +
+
+ +
+ ⚙️ +
+ 设置
+ AI 配置:Provider、模型选择
+ 通用设置:主题、语言 +
+
+
+ + +
+

🌟 特色功能

+ +
+ 🏠 +
+ 本地优先
+ SQLite 本地数据库
+ 基础功能离线可用
+ 代码不出本地 +
+
+ +
+ +
+ 实时监控
+ WebSocket 实时更新
+ 工作流执行进度
+ 错误原因分析 +
+
+ +
+ 🔧 +
+ 工作流节点
+ Script:执行 Shell 命令
+ Human:人工审批
+ Condition:条件判断
+ Parallel:并行执行 +
+
+
+ + +
+

🎨 界面说明

+ +
颜色系统
+
+
+ + 紫色(主色) +
+
+ + 青绿(成功) +
+
+ + 琥珀(警告) +
+
+ + 珊瑚(危险) +
+
+ +
图标含义
+
+ 📋 待开始 + 🔨 进行中 + 👀 待审查 + ✅ 已合并 + 🗑️ 已废弃 +
+
+ + +
+

⚠️ 注意事项

+ +
+ 🔗 +
+ Git 集成
+ 必须初始化 Git 仓库
+ 配置用户邮箱和姓名
+ 保持分支命名规范 +
+
+ +
+ 🔌 +
+ AI 配置
+ 需要有效的 API Key
+ 检查网络连接
+ 关注 Token 使用量 +
+
+ +
+ ⚙️ +
+ 工作流设计
+ 避免长时间阻塞操作
+ 设置合理的超时时间
+ 保留人工干预接口 +
+
+
+ + +
+

🔄 更新日志

+ +
+ +
+ v1.0 (2026-06-11)
+ 完成核心功能
+ 任务管理系统
+ 工作流引擎
+ AI Chat 集成
+ 本地数据存储 +
+
+ +
+ 🔄 +
+ 计划功能
+ 想法对抗式评估
+ 更多工作流节点
+ 团队协作功能 +
+
+
+ + +
+

⚙️ 配置指南

+ +
AI 配置
+
+ 🤖 +
+ Provider 设置
+ 支持 OpenAI、Anthropic、DeepSeek
+ 配置 API Key 和模型选择 +
+
+
+ ⚙️ +
+ 高级参数
+ Temperature: 0-2(创造性)
+ Max Tokens: 限制回答长度 +
+
+ +
Git 集成
+
+ 🔗 +
+ 基础配置
+ git config user.name/email
+ 自动分支创建:feature/任务名 +
+
+ +
工作流配置
+
+ 🔄 +
+ 节点类型
+ script: Shell 命令
+ human: 人工审批
+ condition: 条件判断 +
+
+
+ + +
+

❓ 常见问题

+ +
安装启动
+
+ 🚀 +
+ 无法启动
+ 检查 Node.js >= 18
+ bun clean + bun install
+ 确认端口 1420 可用 +
+
+ +
功能使用
+
+ 💡 +
+ 任务不显示
+ 确认选中项目
+ 刷新页面
+ 查看控制台错误 +
+
+
+ 🤖 +
+ AI 无响应
+ 检查 API Key
+ 确认网络连接
+ 切换 Provider 测试 +
+
+ +
数据管理
+
+ 💾 +
+ 存储位置
+ Windows: %APPDATA%\devflow\
+ macOS: ~/Library/Application Support/devflow/
+ Linux: ~/.config/devflow/ +
+
+
+ 🔧 +
+ 数据备份
+ 备份数据库:cp devflow.db backup/
+ 备份配置:cp config.json backup/ +
+
+
+ + +
+

💡 产品哲学

+ +
+ 🎯 +
+ 聚焦核心
+ 不是全流程操作系统
+ 专注于本地任务流程
+ 简单、实用、可靠 +
+
+ +
+ 👨‍💻 +
+ 开发者第一
+ 功能设计以开发者需求为中心
+ 避免过工程化的设计
+ 保留必要的扩展性 +
+
+ + +
+ + + + \ No newline at end of file diff --git a/docs/08-用户指南/使用手册.md b/docs/08-用户指南/使用手册.md new file mode 100644 index 0000000..98f9038 --- /dev/null +++ b/docs/08-用户指南/使用手册.md @@ -0,0 +1,225 @@ +# DevFlow 使用手册 + +> "本地优先的个人开发流程驾驶舱" + +```mermaid +graph LR + A[想法池] -->|评估| B[项目管理] + B -->|创建| C[任务队列] + C -->|执行| D[工作流引擎] + D -->|合并| E[Git仓库] +``` + +```mermaid +graph LR + A[想法] -->|收集| B[任务] + B -->|分配| C[分支] + C -->|自动化| D[工作流] + D -->|提交| E[代码] +``` + +## 🚀 快速开始 + +### 运行应用 +```bash +bun install +bun run tauri dev +``` + +### 一句话功能 +你的项目流程,本地跑,AI 辅助,数据不出门 + +## 🎯 核心价值链 + +### 任务 → 分支 → 工作流 → 合并 + +#### 1. 任务管理 +- **创建任务**:指定项目、标题、分支名 +- **状态流转**:待开始 → 进行中 → 待审查 → 已合并 +- **优先级**:P0(紧急)到 P3(低) + +#### 2. 分支绑定 +```bash +# 创建任务时自动生成分支 +# 例如:feature/devflow-improve +``` + +#### 3. 工作流执行 +- **环境检查**:依赖验证、端口检查 +- **运行测试**:单元测试、集成测试 +- **构建产物**:打包、优化、部署 + +#### 4. 实时监控 +- **事件日志**:每个节点执行状态 +- **错误处理**:失败重试、人工审批 + +## 📋 功能模块 + +### 🤖 AI Chat +- **单 Provider**:简化配置 +- **工具调用**:代码生成、分析 +- **流式响应**:实时输出 + +### 💡 想法池 +- **轻量版**:聚焦核心 +- **预留对抗评估**:后续升级 + +### 📂 项目管理 +- **多项目支持**:切换不同仓库 +- **状态同步**:与 Git 分支联动 + +### 📚 知识库(收藏夹) +- **静态收集**:个人经验碎片 +- **分类管理**:审查规则、Prompt模板、踩坑经验 +- **快速搜索**:标题、标签、内容 + +### ⚙️ 设置 +- **AI 配置**:Provider、模型选择 +- **通用设置**:主题、语言 + +## 🔧 核心功能详解 + +### 任务操作流程 + +#### 1. 创建任务 +```typescript +// 在项目详情页点击 "+ 新任务" +- 填写任务标题 +- 选择项目 +- (可选)指定分支名 +``` + +#### 2. 任务执行 +- **自动创建分支**:`git checkout -b feature/task-name` +- **绑定任务 ID**:Git commit 自动关联 +- **状态同步**:合并后任务标记为已完成 + +#### 3. 工作流运行 +```typescript +// 点击"运行测试工作流" +1. 环境检查 → 验证依赖 +2. 运行测试 → 执行单元测试 +3. 构建产物 → 生成发布包 +``` + +### AI Chat 使用 + +#### 基础对话 +- 直接提问 +- 代码审查 +- 问题诊断 + +#### 工具调用 +- **生成代码**:根据描述生成完整实现 +- **文件操作**:读取、编辑项目文件 +- **Git 操作**:提交、推送、合并 + +## 💡 想法池功能 + +### 创建想法 +```typescript +// 简单记录 +- 标题:一句话描述 +- 描述:详细说明(可选) +- 优先级:P1~P3 +- 标签:便于分类 +``` + +### 状态管理 +- **草稿**:初始想法 +- **活跃**:正在考虑 +- **已完成**:已实现或放弃 + +### 未来升级 +- **对抗式评估**:正方+反方+分析师 +- **智能推荐**:相关想法关联 + +## 🌟 特色功能 + +### 本地优先 +- **数据存储**:SQLite 本地数据库 +- **无需网络**:基础功能离线可用 +- **隐私保护**:代码不出本地 + +### 实时监控 +- **事件流**:WebSocket 实时更新 +- **进度显示**:工作流执行进度 +- **错误提示**:失败原因分析 + +### 工作流节点 +```typescript +// 支持的节点类型 +- Script:执行 Shell 命令 +- Human:人工审批 +- Condition:条件判断 +- Parallel:并行执行 +``` + +## 📊 使用统计 + +### 个人效能 +- **任务完成率**:按时完成任务比例 +- **分支管理**:活跃分支数量 +- **工作流成功率**:自动执行成功率 + +### 项目进度 +- **阶段分布**:规划/开发/测试/上线 +- **任务积压**:待处理任务数量 +- **开发速度**:每周完成任务数 + +## 🎨 界面说明 + +### 颜色系统 +- **主色**:紫色(#6B46C1) +- **成功色**:绿色(#10B981) +- **警告色**:黄色(#F59E0B) +- **错误色**:红色(#EF4444) + +### 图标含义 +- 📋:待开始 +- 🔨:进行中 +- 👀:待审查 +- ✅:已合并 +- 🗑️:已废弃 + +## ⚠️ 注意事项 + +### Git 集成 +- 必须初始化 Git 仓库 +- 配置用户邮箱和姓名 +- 保持分支命名规范 + +### AI 配置 +- 需要有效的 API Key +- 检查网络连接 +- 关注 Token 使用量 + +### 工作流设计 +- 避免长时间阻塞操作 +- 设置合理的超时时间 +- 保留人工干预接口 + +## 🔄 更新日志 + +### v1.0 (2026-06-11) +- ✅ 完成核心功能 +- ✅ 任务管理系统 +- ✅ 工作流引擎 +- ✅ AI Chat 集成 +- ✅ 本地数据存储 + +### 计划功能 +- 🔄 想法对抗式评估 +- 🔄 更多工作流节点 +- 🔄 团队协作功能 +- 🔄 数据导出功能 + +## 📞 支持 + +- 问题反馈:创建 Issue +- 功能建议:想法池提交 +- 使用交流:Discord 社区 + +--- + +**记住**:这不是全流程操作系统,而是专注于本地任务流程的工具。简单、实用、可靠。 \ No newline at end of file diff --git a/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 0000000..47253b4 --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,109 @@ +# DevFlow 文档索引 + +> 创建: 2026-06-10 | 当前阶段: Phase 2 本地优先开发流程验证 +> 更新: 2026-06-11 | 清理 60% 功能,聚焦核心链路 + +--- + +## 目录结构 + +``` +docs/ +├── README.md # 快速开始与文档导航 +├── 使用指南/ # 用户文档 +│ └── 使用手册.html # 完整使用指南(HTML) +├── INDEX.md # 本文件 — 文档导航 +├── 01-技术文档/ # 技术专题研究 +│ ├── SQLite-CRUD模式.md # CRUD 层设计模式 +│ └── Tauri-IPC模式.md # Tauri IPC 设计模式 +├── 02-架构设计/ # 架构方案、设计决策、迁移记录 +│ ├── Phase1架构决策.md # Phase 1 关键架构决策 +│ ├── 业务系统设计.md # 业务系统设计 +│ ├── 前后端类型对齐.md # Rust/TS 类型对齐规范 +│ ├── 对抗论证裁决报告.md # 60% 功能清理决策 +│ └── 产品定位调整.md # 新定位:本地优先个人开发流程驾驶舱 +├── 03-模块文档/ # 各功能模块实现文档 +│ ├── df-storage-存储层.md # 存储层概览 +│ ├── df-workflow-工作流引擎.md # 工作流引擎概览 +│ ├── df-nodes-节点集合.md # 8 种节点概览 +│ ├── df-ai-AI集成模块.md # AI Provider 集成 +│ └── 想法探索-对抗式评估.md # 想法池对抗评估设计 +├── 04-功能迭代/ # 功能开发过程记录 +│ ├── DEVFLOW-1.CRUD层实施.md # CRUD 层实施记录 +│ ├── DEVFLOW-2.IPC桥接实施.md # IPC 桥接实施记录 +│ ├── DEVFLOW-3.Store对接实施.md # Store 对接实施记录 +│ ├── DEVFLOW-4.端到端验证.md # 端到端验证记录 +│ ├── DEVFLOW-5.功能清理.md # 60% 功能清理记录 +│ └── DEVFLOW-6.想法池开发.md # 想法池基础功能开发 +├── 05-代码审查/ # 审查报告、代码质量 +│ └── 代码审查报告.md # 首轮代码审查结果 +├── 06-前端开发/ # 前端分析、优化 +│ ├── View改造指南.md # 硬编码 → Store → API 迁移指南 +│ └── 组件设计规范.md # Vue 3 组件设计规范 +├── 07-项目管理/ # 项目状态、功能清单、版本管理 +│ ├── Phase1任务清单.md # Phase 1 任务清单 +│ ├── Phase2计划.md # Phase 2 开发计划 +│ └── PROGRESS.md # 项目进展与交接记录 +└── 08-用户指南/ # 用户手册、配置指南 + ├── 快速上手.md # 5分钟快速上手 + ├── 配置指南.md # AI 配置、Git 集成 + └── 常见问题.md # FAQ 和故障排除 +``` + +--- + +## 快速导航 + +| 分类 | 入口 | 说明 | +|------|------|------| +| 🚀 快速开始 | [README.md](./README.md) | 安装、启动、快速上手 | +| 📖 使用指南 | [使用指南/](./使用指南/) | 完整交互式使用手册 | +| 📋 项目进度 | [PROGRESS.md](../PROGRESS.md) | 实时项目进展与状态 | +| ⚙️ 技术文档 | [01-技术文档/](./01-技术文档/) | SQLite CRUD、Tauri IPC 等技术专题 | +| 🏗️ 架构设计 | [02-架构设计/](./02-架构设计/) | 架构方案、设计决策、产品定位调整 | +| 🔧 模块文档 | [03-模块文档/](./03-模块文档/) | 各 Crate 模块实现与设计 | +| 📈 功能迭代 | [04-功能迭代/](./04-功能迭代/) | DEVFLOW-N 系列功能开发过程 | +| 🔍 代码审查 | [05-代码审查/](./05-代码审查/) | 审查报告、代码质量分析 | +| 🎨 前端开发 | [06-前端开发/](./06-前端开发/) | Vue 3 前端分析、优化、迁移指南 | +| 📊 项目管理 | [07-项目管理/](./07-项目管理/) | 任务清单、开发计划、进度跟踪 | +| 📚 用户指南 | [08-用户指南/](./08-用户指南/) | 快速上手、配置指南、FAQ | + +--- + +## 新文档放置规则 + +| 文档类型 | 放置位置 | +|----------|----------| +| 技术专题研究 | `01-技术文档/<主题名>.md` | +| 架构设计/改进方案 | `02-架构设计/` | +| Crate 模块实现文档 | `03-模块文档/` | +| 功能开发过程记录 | `04-功能迭代/DEVFLOW-N.<名称>.md` | +| 代码审查/走查报告 | `05-代码审查/` | +| 前端优化/迁移指南 | `06-前端开发/` | +| 项目状态/任务清单 | `07-项目管理/` | +| 用户手册/配置指南 | `08-用户指南/` | + +--- + +## 核心文档速查 + +| 文档 | 路径 | 说明 | +|------|------|------| +| 架构设计 | `../ARCHITECTURE.md` | 22,745 字完整架构文档 | +| 项目进展 | `../PROGRESS.md` | 工作进展与交接 | +| Crate 结构 | `../ARCHITECTURE.md#四crate-结构` | 13 个 Crate 概览 | +| 数据模型 | `../ARCHITECTURE.md#六数据模型` | SQLite 表结构定义 | +| Phase 规划 | `../ARCHITECTURE.md#八phase-规划` | 5 个 Phase 路线图 | + +--- + +## 技术栈 + +| 层 | 技术 | 说明 | +|----|------|------| +| Desktop | Tauri v2 | Rust 后端 + WebView 前端 | +| Frontend | Vue 3 + TypeScript + Pinia | Arco Design 组件库 | +| Engine | Rust Workspace (13 crate) | 多 crate 架构 | +| Storage | SQLite (rusqlite) | 本地优先,零运维 | +| AI | Multi-Provider | Claude/GLM/DeepSeek/OpenAI 兼容 | +| Build | Bun + Vite | 前端构建 | diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..0687c6c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,57 @@ +# DevFlow 文档 + +> **本地优先的个人创作流程驾驶舱** +> 从想法到创作(代码/文档/演示)的全流程管理 + +## 📚 文档目录 + +### 📖 使用指南 +- [DevFlow 使用手册](使用指南/使用手册.html) - 完整的使用指南,包含功能介绍、操作流程和最佳实践 + +### 📋 项目进度 +- [PROGRESS.md](PROGRESS.md) - 项目开发进展与交接记录 + +## 🎯 核心理念 + +DevFlow 帮助你将**想法转化为创作成果**,支持多种输出形式: + +### 创作类型 +- **代码实现**:应用程序、脚本、工具 +- **技术文档**:API 文档、用户手册、架构设计 +- **演示文稿**:项目提案、技术分享、培训材料 +- **内容创作**:博客文章、分析报告、学习笔记 +- **创意输出**:设计方案、概念图、原型 + +### 核心流程 +``` +想法池 → 对抗评估 → 创作(多样化) → 发布/分享 +``` + +## 🚀 快速开始 + +```bash +# 安装依赖 +bun install + +# 启动开发服务器 +bun run tauri dev +``` + +--- + +## 📖 文档说明 + +### 使用指南 +HTML 格式的交互式文档,包含: +- 核心功能介绍(想法管理、创作流程) +- 操作指南(代码/文档/PPT 创作) +- 界面说明 +- 最佳实践 +- 常见问题解答 + +### 项目进度 +Markdown 格式的开发记录: +- 功能模块完成情况 +- 重要决策记录(从代码到创作的定位升级) +- 技术难点攻克 +- 版本更新日志 \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..485c299 --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + + + DevFlow + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..a12081d --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "devflow", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@arco-design/web-vue": "^2.58.0", + "@tauri-apps/api": "^2", + "dompurify": "^3.4.9", + "marked": "^18.0.5", + "vue": "^3.5.13", + "vue-i18n": "9", + "vue-router": "4" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/dompurify": "^3.2.0", + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "~5.6.3", + "vite": "^6.0.7", + "vue-tsc": "^2.2.0" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..af14ee7 --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..b1cb37f --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,3 @@ +# Generated +target/ +Cargo.lock diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..f08df7e --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "devflow" +version = "0.1.0" +description = "DevFlow Desktop App" +authors = [""] +edition = "2021" + +[lib] +name = "devflow_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-opener = "2" +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +anyhow.workspace = true +tracing.workspace = true + +# 后端 crate +df-core = { path = "../crates/df-core" } +df-storage = { path = "../crates/df-storage" } +df-workflow = { path = "../crates/df-workflow" } +df-nodes = { path = "../crates/df-nodes" } +df-execute = { path = "../crates/df-execute" } +df-ai = { path = "../crates/df-ai" } +futures = "0.3" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..22ba806 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,20 @@ +{ + "identifier": "default", + "description": "DevFlow default permissions", + "windows": ["main", "ai-detached"], + "permissions": [ + "core:default", + "core:event:default", + "core:event:allow-listen", + "core:event:allow-emit", + "core:window:allow-create", + "core:window:allow-close", + "core:window:allow-set-always-on-top", + "core:window:allow-set-focus", + "core:window:allow-set-position", + "core:window:allow-set-size", + "core:window:allow-outer-position", + "core:window:allow-inner-size", + "core:webview:allow-create-webview-window" + ] +} diff --git a/src-tauri/docs/code-review-2025-06-11.md b/src-tauri/docs/code-review-2025-06-11.md new file mode 100644 index 0000000..e66fc10 --- /dev/null +++ b/src-tauri/docs/code-review-2025-06-11.md @@ -0,0 +1,89 @@ +# DevFlow 代码审查报告 + +**审查日期:** 2025-06-11 +**审查范围:** `src/` 目录全部 Rust 源码(6 个文件,约 1,700 行) +**审查版本:** v0.1.0 + +--- + +## 整体评分 + +| 维度 | 评分 | 说明 | +|------|------|------| +| 架构设计 | ⭐⭐⭐⭐ | 模块划分清晰,Tauri 命令层 + Repo 层 + 工作流引擎分层合理 | +| 代码质量 | ⭐⭐⭐⭐ | 注释详尽,命名规范,错误处理一致 | +| 安全性 | ⭐⭐⭐ | 有路径校验和风险分级,但存在改进空间 | +| 健壮性 | ⭐⭐⭐ | 异步并发处理有隐患,部分边界情况未覆盖 | +| 可维护性 | ⭐⭐⭐ | `ai.rs` 文件过大(44KB),职责过多 | + +--- + +## 🔴 严重问题(P0) + +### #1 API Key 明文存储在数据库 +- **文件:** `src/commands/ai.rs` — `ai_save_provider` +- **问题:** `api_key` 以明文存入 SQLite,任何能访问数据库文件的人都能读取 +- **建议:** 使用 OS 级密钥存储或 AES 加密后存储 + +### #2 工作流事件转发存在多执行实例串扰 +- **文件:** `src/commands/workflow.rs` — `run_workflow` +- **问题:** EventBus 全局共享,并发工作流事件会串扰 +- **建议:** 为每次执行创建独立事件通道,或转发时按 execution_id 过滤 + +--- + +## 🟡 中等问题(P1) + +### #3 ai_chat_send 锁释放竞态条件 +- **文件:** `src/commands/ai.rs` — `ai_chat_send` +- **问题:** drop 锁后重新获取锁之间,并发请求可能覆盖 conversation_id +- **建议:** 移到锁外预处理或使用 OnceLock + +### #4 ai.rs 文件过大(1129 行 / 44KB) +- **建议拆分为:** `chat.rs` / `agent.rs` / `tools.rs` / `prompt.rs` + +### #5 AI 工具注册为占位实现 +- **文件:** `src/state.rs` +- **问题:** 工具闭包全是占位,真正逻辑在 ai.rs 硬编码 match,两处定义易不同步 +- **建议:** 统一到 AiToolRegistry 或 trait 对象分发 + +### #6 缺少单元测试 +- **建议优先测试:** validate_path / execute_tool_on_repo / extract_title / replace_tool_result + +--- + +## 🟢 轻微问题(P2) + +### #7 now_millis() 隐藏时间异常 +- `unwrap_or_default()` 静默返回 0,建议加 tracing::warn + +### #8 Cargo.toml 缺少 authors 和 license +- 补充作者信息和开源许可证 + +### #9 .gitignore 忽略了 Cargo.lock +- 二进制应用应提交 lock 文件 + +### #10 update_field 使用字符串字段名 +- 考虑使用 enum 定义可更新字段,编译时保证安全 + +### #11 validate_path 黑名单不够全面 +- 建议改用白名单模式(只允许工作目录下的路径) + +### #12 tauri.conf.json CSP 设为 null +- 生产环境应配置严格 CSP 策略 + +### #13 WorkflowFailed 中 failed_node 始终为空 +- 应从执行器错误信息提取失败节点 ID + +### #14 greet 命令为脚手架残留 +- 确认前端不再使用后移除 + +--- + +## ✅ 代码亮点 + +1. 注释质量极高 — 解释"为什么"而非"是什么" +2. Agentic 循环设计精良 — 停止信号、idle timeout、断连检测、审批门控 +3. 风险分级机制 — Low/Medium/High 三级控制 +4. 错误处理一致 — 统一 Result +5. 对话持久化 — 自动保存,支持多对话切换 diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..d0085c5 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..d0085c5 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..d0085c5 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..d0085c5 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..d0085c5 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs new file mode 100644 index 0000000..1aa2a11 --- /dev/null +++ b/src-tauri/src/commands/ai.rs @@ -0,0 +1,1421 @@ +//! AI 聊天命令 — 流式对话、工具调用、审批门控、提供商管理 +//! +//! 事件协议:通过 app.emit("ai-chat-event", payload) 流式推送到前端 +//! - AiTextDelta: 流式文本片段 +//! - AiToolCallStarted/Completed: 工具调用生命周期 +//! - AiApprovalRequired: 需要人工审批 +//! - AiCompleted/AiError: 完成/错误 + +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Emitter, State}; +use tokio::sync::Mutex; +use futures::StreamExt; + +use df_ai::ai_tools::{AiToolRegistry, RiskLevel}; +use df_ai::anthropic_compat::AnthropicCompatProvider; +use df_ai::openai_compat::OpenAICompatProvider; +use df_ai::provider::{ + ChatMessage, CompletionRequest, CompletionResponse, LlmProvider, MessageRole, StreamChunk, +}; +use df_core::types::new_id; +use df_storage::crud::{AiConversationRepo, AiProviderRepo}; +use df_storage::db::Database; +use df_storage::models::{AiProviderRecord, ProjectRecord, TaskRecord, IdeaRecord}; + +use crate::state::AppState; +use super::now_millis; + +// ============================================================ +// 事件载荷类型 +// ============================================================ + +/// AI 聊天事件(推送到前端) +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type")] +pub enum AiChatEvent { + /// 流式文本片段 + AiTextDelta { delta: String, conversation_id: Option }, + /// 工具调用开始 + AiToolCallStarted { id: String, name: String, args: serde_json::Value, conversation_id: Option }, + /// 工具调用完成 + AiToolCallCompleted { id: String, result: serde_json::Value, conversation_id: Option }, + /// 需要人工审批 + AiApprovalRequired { id: String, name: String, args: serde_json::Value, reason: String, conversation_id: Option }, + /// 审批结果 + AiApprovalResult { id: String, approved: bool, conversation_id: Option }, + /// AI 响应完成 + AiCompleted { total_tokens: u32, conversation_id: Option }, + /// 错误 + AiError { error: String, conversation_id: Option }, + /// Agent 循环新一轮(前端需新建 assistant 消息) + AiAgentRound { round: u32, conversation_id: Option }, +} + +// ============================================================ +// 会话状态 +// ============================================================ + +/// AI 会话内状态(Mutex 保护) +pub struct AiSession { + /// 对话历史 + pub messages: Vec, + /// 当前提供商 ID + pub active_provider_id: Option, + /// 当前活跃对话 ID + pub active_conversation_id: Option, + /// 挂起的审批(tool_call_id → 审批信息) + pub pending_approvals: HashMap, + /// 是否正在生成 + pub generating: bool, + /// 当前 agent 循环的语言设置(用于审批后恢复循环) + pub agent_language: Option, + /// 停止信号:ai_chat_stop 置位,agentic loop / stream_llm 检测后尽快退出 + pub stop_flag: Arc, +} + +/// 待审批的工具调用 +#[derive(Debug, Clone)] +pub struct PendingApproval { + pub tool_call_id: String, + pub tool_name: String, + pub arguments: serde_json::Value, + pub risk_level: RiskLevel, + pub conversation_id: Option, +} + +impl AiSession { + pub fn new() -> Self { + Self { + messages: Vec::new(), + active_provider_id: None, + active_conversation_id: None, + pending_approvals: HashMap::new(), + generating: false, + agent_language: None, + stop_flag: Arc::new(AtomicBool::new(false)), + } + } +} + +/// 工具调用草稿(流式收集时的临时结构) +#[derive(Debug, Clone, Default)] +struct ToolCallDraft { + id: String, + name: String, + args: String, +} + +// ============================================================ +// IPC 命令 +// ============================================================ + +/// 发送消息并获取流式 AI 响应 +/// +/// 非阻塞:立即返回 "ok",通过 ai-chat-event 事件流式推送 +#[tauri::command] +pub async fn ai_chat_send( + app: AppHandle, + state: State<'_, AppState>, + message: String, + language: Option, + skill: Option, +) -> Result { + // 获取活跃提供商(只读,失败可直接返回,不影响生成标志) + let provider_config = get_active_provider(&state).await?; + + // 原子检查并占用生成标志,防止并发双发;同步追加用户消息,按需自动创建对话 + { + let mut session = state.ai_session.lock().await; + if session.generating { + return Err("AI 正在生成中,请等待完成".to_string()); + } + session.generating = true; + session.stop_flag.store(false, Ordering::SeqCst); + session.agent_language = language.clone(); + session.messages.push(ChatMessage::user(&message)); + + // 自动创建对话记录(首次发送时) + if session.active_conversation_id.is_none() { + let conv_id = new_id(); + let now = now_millis(); + let provider_id = session.active_provider_id.clone(); + drop(session); + + let record = df_storage::models::AiConversationRecord { + id: conv_id.clone(), + title: None, + messages: "[]".to_string(), + provider_id, + model: None, + created_at: now.clone(), + updated_at: now, + }; + state.ai_conversations.insert(record).await.map_err(|e| e.to_string())?; + + let mut session = state.ai_session.lock().await; + session.active_conversation_id = Some(conv_id); + } + } + + // 获取工具定义 + let tool_defs = state.ai_tools.tool_definitions(); + let lang = language.unwrap_or_else(|| "zh-CN".to_string()); + let mut system_prompt = build_system_prompt(&state, &lang).await; + // 技能注入:读 SKILL.md 全文拼到 system prompt 前作为指令 + if let Some(ref skill_name) = skill { + if let Some(content) = read_skill_content(skill_name) { + system_prompt = format!("# 技能指令: {}\n\n{}\n\n---\n{}", skill_name, content, system_prompt); + } + } + + // 快照当前对话 ID,spawn 后台 loop 不受切换影响 + let conv_id = { + let session = state.ai_session.lock().await; + session.active_conversation_id.clone().unwrap_or_default() + }; + + // 在后台任务中执行流式调用 + let session_arc = state.ai_session.clone(); + let tools_arc = state.ai_tools.clone(); + let db = state.db.clone(); + let app_handle = app.clone(); + + tauri::async_runtime::spawn(async move { + run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id).await; + }); + + Ok("ok".to_string()) +} + +/// 批准/拒绝挂起的工具调用 +#[tauri::command] +pub async fn ai_approve( + app: AppHandle, + state: State<'_, AppState>, + tool_call_id: String, + approved: bool, +) -> Result { + let mut session = state.ai_session.lock().await; + + let approval = session + .pending_approvals + .remove(&tool_call_id) + .ok_or_else(|| format!("未找到挂起的审批: {}", tool_call_id))?; + + if !approved { + // 替换占位 tool_result 为拒绝结果 + replace_tool_result(&mut session.messages, &tool_call_id, "用户拒绝了此操作"); + let conv_id = approval.conversation_id.clone(); + let _ = app.emit("ai-chat-event", AiChatEvent::AiApprovalResult { + id: tool_call_id, + approved: false, + conversation_id: conv_id.clone(), + }); + drop(session); + // 拒绝结果立即落库 + if let Some(ref cid) = conv_id { + save_conversation(&state.ai_session, &state.db, cid).await; + } + // 所有待审批处理完毕后恢复 agentic 循环 + try_continue_agent_loop(&app, &state).await; + return Ok("rejected".to_string()); + } + + // 执行工具(通过真实 repo 调用) + let args = approval.arguments.clone(); + let id = tool_call_id.clone(); + let conv_id = approval.conversation_id.clone(); + let db = state.db.clone(); + drop(session); // 释放锁后再执行 + + let result = execute_tool_on_repo(&approval.tool_name, args.clone(), &db) + .await + .map_err(|e| e.to_string())?; + + // 重新获取锁,替换占位 tool_result 为真实结果 + let mut session = state.ai_session.lock().await; + replace_tool_result(&mut session.messages, &id, &result.to_string()); + + let _ = app.emit("ai-chat-event", AiChatEvent::AiToolCallCompleted { + id: id.clone(), + result: result.clone(), + conversation_id: conv_id.clone(), + }); + let _ = app.emit("ai-chat-event", AiChatEvent::AiApprovalResult { + id, + approved: true, + conversation_id: conv_id.clone(), + }); + drop(session); + + // 审批执行结果立即落库,不依赖后续 agentic loop(避免 loop 异常退出时丢失真实结果) + if let Some(ref cid) = conv_id { + save_conversation(&state.ai_session, &state.db, cid).await; + } + + // 所有待审批处理完毕后恢复 agentic 循环 + try_continue_agent_loop(&app, &state).await; + + Ok("executed".to_string()) +} + +/// 清空对话历史 +#[tauri::command] +pub async fn ai_chat_clear(state: State<'_, AppState>) -> Result<(), String> { + let mut session = state.ai_session.lock().await; + session.messages.clear(); + session.pending_approvals.clear(); + Ok(()) +} + +/// 停止当前 AI 生成 +/// +/// 两种场景: +/// - loop 正在流式生成:置 stop_flag,stream_llm / 循环检查点尽快退出并 emit AiCompleted +/// - 有挂起审批(loop 已 return 等待中):stop_flag 无人读取,直接清审批 + 复位 generating, +/// 否则停止按钮表面无反应、会话卡在 generating=true +#[tauri::command] +pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result<(), String> { + let mut session = state.ai_session.lock().await; + if !session.generating { + return Ok(()); + } + if !session.pending_approvals.is_empty() { + // 审批等待态:loop 已退出,直接清理让会话立即可用 + session.pending_approvals.clear(); + session.generating = false; + session.stop_flag.store(true, Ordering::SeqCst); // 双保险:防 try_continue 误判重启 + let conv_id = session.active_conversation_id.clone(); + drop(session); + let _ = app.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: 0, conversation_id: conv_id }); + return Ok(()); + } + // 流式生成中:置位让 loop 自行收尾 + session.stop_flag.store(true, Ordering::SeqCst); + Ok(()) +} + +/// 列出所有已配置的 AI 提供商 +#[tauri::command] +pub async fn ai_list_providers(state: State<'_, AppState>) -> Result, String> { + state.ai_providers.list_all().await.map_err(|e| e.to_string()) +} + +/// 保存/更新 AI 提供商配置 +#[tauri::command] +pub async fn ai_save_provider( + state: State<'_, AppState>, + id: Option, + name: String, + base_url: String, + api_key: String, + default_model: String, + provider_type: String, +) -> Result { + // 编辑已有提供商时保留原 created_at,避免被覆盖 + let created_at = match &id { + Some(pid) => state.ai_providers.get_by_id(pid).await + .map_err(|e| e.to_string())? + .map(|p| p.created_at) + .unwrap_or_else(now_millis), + None => now_millis(), + }; + let record = AiProviderRecord { + id: id.unwrap_or_else(new_id), + name, + provider_type: if provider_type.is_empty() { "openai_compat".to_string() } else { provider_type }, + api_key, + base_url, + default_model, + models: None, + is_default: false, + config: None, + created_at, + updated_at: now_millis(), + }; + let id = record.id.clone(); + state + .ai_providers + .insert(record) + .await + .map_err(|e| e.to_string())?; + Ok(id) +} + +/// 设置活跃提供商 +#[tauri::command] +pub async fn ai_set_provider( + state: State<'_, AppState>, + provider_id: String, +) -> Result<(), String> { + // 验证提供商存在 + let provider = state + .ai_providers + .get_by_id(&provider_id) + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("提供商不存在: {}", provider_id))?; + + let mut session = state.ai_session.lock().await; + session.active_provider_id = Some(provider.id); + Ok(()) +} + +// ============================================================ +// 对话管理命令 +// ============================================================ + +/// 创建新对话 +#[tauri::command] +pub async fn ai_conversation_create(state: State<'_, AppState>) -> Result { + let provider_id = { + let session = state.ai_session.lock().await; + session.active_provider_id.clone() + }; + + let id = new_id(); + let now = now_millis(); + let record = df_storage::models::AiConversationRecord { + id: id.clone(), + title: None, + messages: "[]".to_string(), + provider_id, + model: None, + created_at: now.clone(), + updated_at: now, + }; + state.ai_conversations.insert(record).await.map_err(|e| e.to_string())?; + + let mut session = state.ai_session.lock().await; + session.active_conversation_id = Some(id.clone()); + session.messages.clear(); + session.pending_approvals.clear(); + + Ok(serde_json::json!({ "id": id })) +} + +/// 列出所有对话(仅摘要,不含 messages 全文) +#[tauri::command] +pub async fn ai_conversation_list(state: State<'_, AppState>) -> Result, String> { + let records = state.ai_conversations.list_all().await.map_err(|e| e.to_string())?; + let summaries: Vec = records.iter().rev().map(|r| { + serde_json::json!({ + "id": r.id, + "title": r.title, + "provider_id": r.provider_id, + "model": r.model, + "created_at": r.created_at, + "updated_at": r.updated_at, + }) + }).collect(); + Ok(summaries) +} + +/// 切换到指定对话(从 DB 加载 messages 到内存 + 返回 messages 给前端) +#[tauri::command] +pub async fn ai_conversation_switch( + state: State<'_, AppState>, + conversation_id: String, +) -> Result { + let record = state.ai_conversations.get_by_id(&conversation_id).await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("对话不存在: {}", conversation_id))?; + + let messages: Vec = serde_json::from_str(&record.messages) + .map_err(|e| format!("解析消息失败: {}", e))?; + + let messages_json = record.messages.clone(); + let title = record.title.clone(); + + let mut session = state.ai_session.lock().await; + // 生成中允许只读切换:返回目标对话的 messages 供前端展示,但不修改 session 状态 + // 后台 loop 持有快照的 conv_id,不受 active_conversation_id 变更影响 + if session.generating { + return Ok(serde_json::json!({ + "id": record.id, + "title": title, + "messages": messages_json, + "readonly": true, + })); + } + session.active_conversation_id = Some(conversation_id); + session.messages = messages; + session.pending_approvals.clear(); + + Ok(serde_json::json!({ + "id": record.id, + "title": title, + "messages": messages_json, + })) +} + +/// 删除对话 +#[tauri::command] +pub async fn ai_conversation_delete( + state: State<'_, AppState>, + conversation_id: String, +) -> Result<(), String> { + state.ai_conversations.delete(&conversation_id).await.map_err(|e| e.to_string())?; + + let mut session = state.ai_session.lock().await; + if session.active_conversation_id.as_deref() == Some(&conversation_id) { + session.active_conversation_id = None; + session.messages.clear(); + session.pending_approvals.clear(); + } + Ok(()) +} + +/// 重命名对话标题 +#[tauri::command] +pub async fn ai_conversation_rename( + state: State<'_, AppState>, + conversation_id: String, + title: String, +) -> Result<(), String> { + let title = title.trim().to_string(); + if title.is_empty() { + return Err("标题不能为空".to_string()); + } + state.ai_conversations.update_field(&conversation_id, "title", &title) + .await.map_err(|e| e.to_string())?; + Ok(()) +} + +/// 列出本机 Claude 技能(skills + commands + plugins 三类),供前端 `/` 联想 +#[tauri::command] +pub async fn ai_list_skills() -> Result, String> { + Ok(scan_skills()) +} + +// ============================================================ +// Agentic 循环 +// ============================================================ + +/// Agentic 循环最大迭代次数 +const MAX_AGENT_ITERATIONS: usize = 10; + +/// Agentic 循环:流式接收 → 工具执行 → 结果回传 LLM → 循环 +/// +/// 退出条件: +/// - LLM 只返回文本(无 tool_calls)→ 正常结束 +/// - 有工具需要审批 → 暂停循环(generating 保持 true),等 ai_approve 恢复 +/// - 达到最大迭代次数 → 正常结束 +async fn run_agentic_loop( + session_arc: Arc>, + tools_arc: Arc, + db: Arc, + app_handle: AppHandle, + provider_config: AiProviderRecord, + system_prompt: String, + conv_id: String, +) { + let provider: Box = match provider_config.provider_type.as_str() { + "anthropic" => Box::new(AnthropicCompatProvider::new( + &provider_config.base_url, + &provider_config.api_key, + &provider_config.default_model, + )), + _ => Box::new(OpenAICompatProvider::new( + &provider_config.base_url, + &provider_config.api_key, + &provider_config.default_model, + )), + }; + let tool_defs = tools_arc.tool_definitions(); + // 停止信号副本:stream_llm 与每轮迭代共享读取,避免重复加锁 + let stop_flag = session_arc.lock().await.stop_flag.clone(); + + for iteration in 0..MAX_AGENT_ITERATIONS { + // 用户请求停止 → 收尾退出(已生成文本已在上一轮入库) + if stop_flag.load(Ordering::SeqCst) { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: 0, conversation_id: Some(conv_id.clone()) }); + save_conversation(&session_arc, &db, &conv_id).await; + ensure_conversation_title(&*provider, &provider_config.default_model, &db, &conv_id, &app_handle, &session_arc).await; + let mut session = session_arc.lock().await; + session.generating = false; + return; + } + + // 新一轮通知前端(第二轮起),前端需新建 assistant 消息 + if iteration > 0 { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiAgentRound { + round: (iteration + 1) as u32, + conversation_id: Some(conv_id.clone()), + }); + } + + // 构建请求消息 + let messages = { + let session = session_arc.lock().await; + let mut msgs = vec![ChatMessage::system(&system_prompt)]; + msgs.extend(session.messages.clone()); + msgs + }; + + let request = CompletionRequest { + model: provider_config.default_model.clone(), + messages, + temperature: Some(0.7), + max_tokens: Some(8192), + stream: true, + tools: if tool_defs.is_empty() { None } else { Some(tool_defs.clone()) }, + tool_choice: None, + }; + + // 流式接收(内部处理 idle timeout / 断连检测 / 停止信号) + let (full_text, tool_calls_acc) = match stream_llm(&*provider, request, &app_handle, &stop_flag, &conv_id).await { + Some(result) => result, + None => { + // 错误已在 stream_llm 中 emit,直接结束 + let mut session = session_arc.lock().await; + session.generating = false; + return; + } + }; + + // 追加 assistant 消息到历史 + let has_tool_calls = !tool_calls_acc.is_empty(); + { + let mut session = session_arc.lock().await; + if has_tool_calls { + let mut order: Vec = tool_calls_acc.keys().copied().collect(); + order.sort_unstable(); + let ai_tool_calls: Vec = order.iter() + .map(|i| { + let draft = &tool_calls_acc[i]; + df_ai::provider::ToolCall::new(&draft.id, &draft.name, &draft.args) + }) + .collect(); + session.messages.push(ChatMessage::assistant_with_tools(&full_text, ai_tool_calls)); + } else if !full_text.is_empty() { + session.messages.push(ChatMessage::assistant(&full_text)); + } + } + + // 停止信号:已生成文本入库后退出,不再执行后续工具调用 + if stop_flag.load(Ordering::SeqCst) { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: 0, conversation_id: Some(conv_id.clone()) }); + save_conversation(&session_arc, &db, &conv_id).await; + ensure_conversation_title(&*provider, &provider_config.default_model, &db, &conv_id, &app_handle, &session_arc).await; + let mut session = session_arc.lock().await; + session.generating = false; + return; + } + + // 无工具调用 → 最终文本响应,循环结束 + if !has_tool_calls { break; } + + // 处理工具调用(Low 自动执行 / Medium+High 待审批) + let pending_count = { + let mut session = session_arc.lock().await; + process_tool_calls(&mut session, tool_calls_acc, &tools_arc, &db, &app_handle, &conv_id).await + }; + + // 有待审批 → 暂停循环,等待用户审批后通过 ai_approve → try_continue_agent_loop 恢复 + if pending_count > 0 { + save_conversation(&session_arc, &db, &conv_id).await; + return; // generating 保持 true + } + + // 全部自动执行完成 → 继续下一轮 + } + + // 正常完成 + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: 0, conversation_id: Some(conv_id.clone()) }); + save_conversation(&session_arc, &db, &conv_id).await; + ensure_conversation_title(&*provider, &provider_config.default_model, &db, &conv_id, &app_handle, &session_arc).await; + let mut session = session_arc.lock().await; + session.generating = false; +} + +/// 流式接收 LLM 响应,返回 (完整文本, 工具调用草稿) +/// +/// 三类异常处理: +/// - idle timeout(120s 无 chunk):判定连接静默断,emit AiError 返回 None +/// - 流尽但从未收到 finished 信号:判定异常中断,emit AiError 返回 None(丢弃残缺,不当完整入库) +/// - 用户停止(stop_flag):break 返回 Some(已收文本),由调用方入库展示后退出 +async fn stream_llm( + provider: &dyn LlmProvider, + request: CompletionRequest, + app_handle: &AppHandle, + stop_flag: &AtomicBool, + conv_id: &str, +) -> Option<(String, HashMap)> { + /// 流式读取空闲超时:超过此时长无任何 chunk 即判定连接已断 + const STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(120); + + match provider.stream(request).await { + Ok(mut stream) => { + let mut full_text = String::new(); + let mut tool_calls_acc: HashMap = HashMap::new(); + let mut finished_received = false; + let mut stopped = false; + + loop { + // 用户主动停止:保留已收文本退出 + if stop_flag.load(Ordering::SeqCst) { + stopped = true; + break; + } + + // idle timeout 防"连接存活但中途静默"无限 hang + match tokio::time::timeout(STREAM_IDLE_TIMEOUT, stream.next()).await { + Err(_elapsed) => { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError { + error: "流式响应超时(120 秒无数据,连接可能已断开)".to_string(), + conversation_id: Some(conv_id.to_string()), + }); + return None; + } + Ok(None) => break, // 流正常结束 + Ok(Some(chunk_result)) => match chunk_result { + Ok(chunk) => { + if !chunk.delta.is_empty() { + full_text.push_str(&chunk.delta); + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiTextDelta { + delta: chunk.delta, + conversation_id: Some(conv_id.to_string()), + }); + } + if let Some(tc_deltas) = &chunk.tool_calls { + for tc_delta in tc_deltas { + let draft = tool_calls_acc.entry(tc_delta.index).or_default(); + if let Some(id) = &tc_delta.id { draft.id = id.clone(); } + if let Some(name) = &tc_delta.function_name { draft.name.push_str(name); } + if let Some(args) = &tc_delta.function_arguments { draft.args.push_str(args); } + } + } + if chunk.finished { + finished_received = true; + break; + } + } + Err(e) => { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError { + error: e.to_string(), + conversation_id: Some(conv_id.to_string()), + }); + return None; + } + }, + } + } + + // 用户停止:已生成文本(可能残缺)交调用方入库展示 + if stopped { + return Some((full_text, tool_calls_acc)); + } + + // 断连检测:流尽但从未收到 finished 信号 = 异常中断,丢弃残缺不当完整入库 + if !finished_received && (!full_text.is_empty() || !tool_calls_acc.is_empty()) { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError { + error: "流式响应意外中断(未收到完成信号,已丢弃残缺响应)".to_string(), + conversation_id: Some(conv_id.to_string()), + }); + return None; + } + + Some((full_text, tool_calls_acc)) + } + Err(e) => { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError { + error: format!("AI 调用失败: {}", e), + conversation_id: Some(conv_id.to_string()), + }); + None + } + } +} + +/// 保存对话到数据库(按 conv_id 写库,不受 active_conversation_id 切换影响) +/// +/// 仅写 messages + updated_at;标题由 ensure_conversation_title 在对话完成后单独生成 +async fn save_conversation(session_arc: &Arc>, db: &Arc, conv_id: &str) { + let messages_json = { + let session = session_arc.lock().await; + serde_json::to_string(&session.messages).unwrap_or_else(|_| "[]".to_string()) + }; + + let conv_repo = AiConversationRepo::new(db); + if let Ok(Some(mut rec)) = conv_repo.get_by_id(conv_id).await { + rec.messages = messages_json; + rec.updated_at = now_millis(); + let _ = conv_repo.update_full(&rec).await; + } +} + +/// 对话完成后按需生成智能标题(仅 title 为空时触发一次,不覆盖用户改名) +/// +/// - 已有 title(用户改名或已生成)→ 跳过 +/// - 否则调 LLM 非流式总结生成 ≤15 字标题;LLM 失败回退 extract_title 截断 +/// - 完成后 emit ai-conversation-changed 通知前端侧栏刷新标题 +async fn ensure_conversation_title( + provider: &dyn LlmProvider, + model: &str, + db: &Arc, + conv_id: &str, + app_handle: &AppHandle, + session_arc: &Arc>, +) { + let conv_repo = AiConversationRepo::new(db); + + // 已有标题(用户改名或已生成)→ 不覆盖 + if let Ok(Some(rec)) = conv_repo.get_by_id(conv_id).await { + if rec.title.is_some() { + return; + } + } + + // 取对话文本(仅 user/assistant,跳过 tool 噪音),取前 6 条供 LLM 总结 + let (summary_msgs, all_msgs) = { + let session = session_arc.lock().await; + let summary: Vec = session.messages.iter() + .filter(|m| matches!(m.role, MessageRole::User | MessageRole::Assistant)) + .take(6) + .map(|m| ChatMessage { + role: m.role.clone(), + content: m.content.clone(), + tool_call_id: None, + tool_calls: None, + }) + .collect(); + (summary, session.messages.clone()) + }; + if summary_msgs.is_empty() { + return; + } + + let title = match generate_title_via_llm(provider, model, summary_msgs).await { + Some(t) => t, + None => extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string()), + }; + + let _ = conv_repo.update_field(conv_id, "title", &title).await; + let _ = app_handle.emit("ai-conversation-changed", ()); +} + +/// 调 LLM 非流式生成对话标题 +async fn generate_title_via_llm( + provider: &dyn LlmProvider, + model: &str, + msgs: Vec, +) -> Option { + let mut prompt = vec![ChatMessage::system( + "你是标题生成器。根据用户与助手的对话,生成一个简短中文标题。要求:不超过15字,纯文本,不加引号不加书名号不加标点不加 emoji,只输出标题本身,不要任何前缀或解释。" + )]; + prompt.extend(msgs); + let request = CompletionRequest { + model: model.to_string(), + messages: prompt, + temperature: Some(0.3), + max_tokens: Some(30), + stream: false, + tools: None, + tool_choice: None, + }; + let resp = provider.complete(request).await.ok()?; + Some(clean_title(&resp.text)) +} + +/// 清理 LLM 返回的标题:去首尾引号/书名号/空白/末尾标点,截 15 字,空则兜底"新对话" +fn clean_title(raw: &str) -> String { + let t = raw.trim() + .trim_matches(|c: char| matches!(c, '"' | '\'' | '「' | '」' | '《' | '》' | ' ' | '\n' | '\r')); + let t = t.trim_end_matches(|c: char| matches!(c, '。' | '.' | ',' | ',' | '!' | '!' | '?' | '?' | ':' | ':')); + let cleaned: String = t.chars().take(15).collect(); + if cleaned.is_empty() { "新对话".to_string() } else { cleaned } +} + +/// 检查是否所有待审批已处理,如果是则恢复 agentic 循环 +async fn try_continue_agent_loop(app: &AppHandle, state: &AppState) { + let should_continue = { + let session = state.ai_session.lock().await; + session.generating && session.pending_approvals.is_empty() + }; + + if !should_continue { return; } + + let provider_config = match get_active_provider(state).await { + Ok(p) => p, + Err(_) => return, + }; + let (lang, conv_id) = { + let session = state.ai_session.lock().await; + let lang = session.agent_language.clone().unwrap_or_else(|| "zh-CN".to_string()); + let conv_id = session.active_conversation_id.clone().unwrap_or_default(); + (lang, conv_id) + }; + let system_prompt = build_system_prompt(state, &lang).await; + + let session_arc = state.ai_session.clone(); + let tools_arc = state.ai_tools.clone(); + let db = state.db.clone(); + let app_handle = app.clone(); + + tauri::async_runtime::spawn(async move { + run_agentic_loop(session_arc, tools_arc, db, app_handle, provider_config, system_prompt, conv_id).await; + }); +} + +// ============================================================ +// 辅助函数 +// ============================================================ + +/// 处理流式接收的工具调用:逐个分发、执行/审批、添加到消息历史 +/// 返回待审批的工具数量(0 = 全部自动执行完成) +async fn process_tool_calls( + session: &mut AiSession, + tool_calls_acc: HashMap, + tools_arc: &Arc, + db: &Arc, + app_handle: &AppHandle, + conv_id: &str, +) -> usize { + let mut tc_list: Vec<_> = tool_calls_acc.into_iter().collect(); + tc_list.sort_unstable_by_key(|(i, _)| *i); + let mut pending_count = 0usize; + + // 逐个处理工具调用 + for (_, draft) in &tc_list { + let args = serde_json::from_str(&draft.args).unwrap_or(serde_json::Value::Object(Default::default())); + + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiToolCallStarted { + id: draft.id.clone(), + name: draft.name.clone(), + args: args.clone(), + conversation_id: Some(conv_id.to_string()), + }); + + // 获取工具风险级别 + let risk_level = tools_arc.get(&draft.name).map(|t| t.risk_level).unwrap_or(RiskLevel::High); + match risk_level { + RiskLevel::Low => { + // 低风险:直接执行 + let result = execute_tool_on_repo(&draft.name, args.clone(), &db).await; + match result { + Ok(val) => { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiToolCallCompleted { + id: draft.id.clone(), + result: val.clone(), + conversation_id: Some(conv_id.to_string()), + }); + session.messages.push(ChatMessage::tool_result(&draft.id, val.to_string())); + } + Err(e) => { + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError { + error: format!("工具 {} 执行失败: {}", draft.name, e), + conversation_id: Some(conv_id.to_string()), + }); + session.messages.push(ChatMessage::tool_result(&draft.id, format!("错误: {}", e))); + } + } + } + RiskLevel::Medium | RiskLevel::High => { + // 中/高风险:需要人工审批 + pending_count += 1; + session.pending_approvals.insert(draft.id.clone(), PendingApproval { + tool_call_id: draft.id.clone(), + tool_name: draft.name.clone(), + arguments: args.clone(), + risk_level, + conversation_id: Some(conv_id.to_string()), + }); + session.messages.push(ChatMessage::tool_result(&draft.id, "需要用户审批,等待确认")); + let reason = match risk_level { + RiskLevel::High => "高风险操作,必须人工批准".to_string(), + _ => "创建操作,请确认是否执行".to_string(), + }; + let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiApprovalRequired { + id: draft.id.clone(), + name: draft.name.clone(), + args: args.clone(), + reason, + conversation_id: Some(conv_id.to_string()), + }); + } + } + } + + pending_count +} + +/// 获取当前活跃提供商配置 +async fn get_active_provider(state: &AppState) -> Result { + let session = state.ai_session.lock().await; + if let Some(ref pid) = session.active_provider_id { + let provider = state + .ai_providers + .get_by_id(pid) + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("活跃提供商不存在: {}", pid))?; + Ok(provider) + } else { + // 查找默认提供商 + drop(session); + let providers = state + .ai_providers + .list_all() + .await + .map_err(|e| e.to_string())?; + let default = providers.iter().find(|p| p.is_default).cloned(); + default + .or_else(|| providers.into_iter().next()) + .ok_or_else(|| "未配置 AI 提供商,请先在设置中添加".to_string()) + } +} + +/// 按语言返回系统提示词的 (固定前缀, 项目上下文标题) +fn system_prompt_parts(lang: &str) -> (&'static str, &'static str) { + match lang { + "en" => ( + "You are DevFlow's AI assistant. You help users manage projects, tasks, ideas, and workflows.\n\ + Please respond in English.\n\n\ + ## Capabilities\n\ + You can perform the following actions via tool calls:\n\ + - Create/query projects, tasks, and ideas\n\ + - Run workflows\n\ + - Read file contents, list directories, create/write files\n\n\ + ## Guidelines\n\ + - Briefly explain your intent before executing actions\n\ + - Ask for clarification if the user's intent is unclear\n\ + - Prefer using tools to complete actions rather than just describing steps\n", + "\n## Current Projects\n", + ), + _ => ( + "你是 DevFlow 的 AI 助手。你帮助用户管理项目、任务、想法和工作流。\n\ + 必须使用简体中文回复,禁止使用繁体中文字符。\n\n\ + ## 当前能力\n\ + 你可以通过工具调用执行以下操作:\n\ + - 创建/查询项目、任务、想法\n\ + - 运行工作流\n\ + - 读取文件内容、列出目录、创建/写入文件\n\n\ + ## 行为准则\n\ + - 执行操作前简要说明你的意图\n\ + - 如果不确定用户意图,先提问\n\ + - 优先使用工具完成操作,而不是只描述步骤\n", + "\n## 当前项目\n", + ), + } +} + +/// 构建系统提示词(固定前缀 + 当前项目上下文) +async fn build_system_prompt(state: &AppState, lang: &str) -> String { + let (prefix, ctx_label) = system_prompt_parts(lang); + let mut prompt = String::from(prefix); + + // 附加当前数据上下文 + if let Ok(projects) = state.projects.list_all().await { + if !projects.is_empty() { + prompt.push_str(ctx_label); + for p in &projects { + prompt.push_str(&format!("- {} ({}): {}\n", p.name, p.status, p.description)); + } + } + } + + prompt +} + +// ============================================================ +// 技能扫描 — 本机 Claude 技能(skills / commands / plugins) +// ============================================================ + +/// 技能元信息(前端 `/` 联想 + 后端注入用) +#[derive(Debug, Clone, Serialize)] +pub struct SkillInfo { + pub name: String, + pub description: String, + pub argument_hint: Option, + /// skill | command | plugin + pub source: String, + /// SKILL.md 绝对路径(注入时读全文) + pub path: String, +} + +/// ~/.claude 目录(跨平台:USERPROFILE / HOME) +fn claude_home() -> Option { + std::env::var_os("USERPROFILE") + .or_else(|| std::env::var_os("HOME")) + .map(PathBuf::from) + .map(|h| h.join(".claude")) +} + +/// 解析 markdown frontmatter 的 name / description / argument-hint(简易,按行匹配,容错缩进与 CRLF) +fn parse_frontmatter(md: &str) -> Option<(String, String, Option)> { + let mut lines = md.lines(); + if lines.next()?.trim() != "---" { + return None; + } + let mut name = None; + let mut desc = None; + let mut hint = None; + for line in lines { + if line.trim() == "---" { + break; + } + let l = line.trim_start(); + if let Some(v) = l.strip_prefix("name:") { + name = Some(v.trim().to_string()); + } else if let Some(v) = l.strip_prefix("description:") { + desc = Some(v.trim().to_string()); + } else if let Some(v) = l.strip_prefix("argument-hint:") { + hint = Some(v.trim().to_string()); + } + } + name.map(|n| (n, desc.unwrap_or_default(), hint)) +} + +/// 解析单个 SKILL.md / command md 为 SkillInfo(排除 user_invocable: false) +fn parse_skill_file(path: &Path, source: &str) -> Option { + let md = fs::read_to_string(path).ok()?; + if md.contains("user_invocable: false") { + return None; + } + let (name, description, argument_hint) = parse_frontmatter(&md).unwrap_or_else(|| { + // 无 frontmatter(部分 commands):用文件名兜底 + ( + path.file_stem()?.to_string_lossy().to_string(), + String::new(), + None, + ) + }); + Some(SkillInfo { + name, + description, + argument_hint, + source: source.to_string(), + path: path.to_string_lossy().to_string(), + }) +} + +/// 递归收集目录下所有 SKILL.md(用于 plugins/marketplaces 多层嵌套) +fn collect_skill_files(dir: &Path, out: &mut Vec) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let p = entry.path(); + if p.is_dir() { + collect_skill_files(&p, out); + } else if p.file_name().and_then(|n| n.to_str()) == Some("SKILL.md") { + out.push(p); + } + } + } +} + +/// 扫描三类来源,按 name 去重(skills 优先 > commands > plugins) +fn scan_skills() -> Vec { + let home = match claude_home() { + Some(h) => h, + None => return Vec::new(), + }; + let mut skills = Vec::new(); + let mut seen: HashSet = HashSet::new(); + + // 1. ~/.claude/skills/*/SKILL.md + if let Ok(entries) = fs::read_dir(home.join("skills")) { + for entry in entries.flatten() { + if let Some(info) = parse_skill_file(&entry.path().join("SKILL.md"), "skill") { + if seen.insert(info.name.clone()) { + skills.push(info); + } + } + } + } + + // 2. ~/.claude/commands/*.md + if let Ok(entries) = fs::read_dir(home.join("commands")) { + for entry in entries.flatten() { + let p = entry.path(); + if p.extension().and_then(|e| e.to_str()) == Some("md") { + if let Some(info) = parse_skill_file(&p, "command") { + if seen.insert(info.name.clone()) { + skills.push(info); + } + } + } + } + } + + // 3. ~/.claude/plugins/marketplaces/**/skills/*/SKILL.md(递归;cache 不在此路径下) + let mut files = Vec::new(); + collect_skill_files(&home.join("plugins").join("marketplaces"), &mut files); + for f in files { + if let Some(info) = parse_skill_file(&f, "plugin") { + if seen.insert(info.name.clone()) { + skills.push(info); + } + } + } + + skills +} + +/// 按 name 读取技能全文(注入 system prompt) +fn read_skill_content(name: &str) -> Option { + scan_skills() + .into_iter() + .find(|s| s.name == name) + .and_then(|s| fs::read_to_string(&s.path).ok()) +} + +// ============================================================ +// 工具执行 — 根据 tool_name 路由到真实 repo 方法 +// ============================================================ + +/// 验证文件路径:禁止访问系统敏感目录 +fn validate_path(path: &str) -> anyhow::Result<()> { + // 规范化为反斜杠:LLM 可能传正斜杠绕过黑名单(Windows tokio::fs 两种分隔符都吃) + let normalized = path.replace('/', "\\"); + let lower = normalized.to_lowercase(); + if lower.contains("..") { + anyhow::bail!("禁止路径遍历 (..)"); + } + if lower.contains("\\.ssh") + || lower.contains("\\.aws") + || lower.contains("\\.gnupg") + || lower.contains("\\appdata\\") + || lower.contains("\\programdata\\") + || lower.contains("\\windows\\") + || lower.contains("\\system32\\") + { + anyhow::bail!("禁止访问敏感系统目录"); + } + Ok(()) +} + +/// 在 spawn 任务中执行工具调用,通过 db Arc 重建 repo 并操作数据库 +async fn execute_tool_on_repo( + tool_name: &str, + args: serde_json::Value, + db: &Arc, +) -> anyhow::Result { + match tool_name { + // ── 只读 ── + "list_projects" => { + let repo = df_storage::crud::ProjectRepo::new(db); + let projects = repo.list_all().await?; + Ok(serde_json::to_value(projects)?) + } + "list_tasks" => { + let repo = df_storage::crud::TaskRepo::new(db); + let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) { + repo.query("project_id", pid).await? + } else { + repo.list_all().await? + }; + Ok(serde_json::to_value(tasks)?) + } + "list_ideas" => { + let repo = df_storage::crud::IdeaRepo::new(db); + let ideas = repo.list_all().await?; + Ok(serde_json::to_value(ideas)?) + } + + // ── 创建 ── + "update_project" => { + let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; + let field = args["field"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 field"))?; + let value = args["value"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 value"))?; + // 白名单校验:只允许更新的字段 + match field { + "name" | "status" | "description" => {} + _ => anyhow::bail!("不允许更新字段 '{}'", field), + } + let repo = df_storage::crud::ProjectRepo::new(db); + repo.update_field(id, field, value).await?; + Ok(serde_json::json!({ "id": id, "field": field, "updated": true })) + } + "create_project" => { + let name = args["name"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 name 参数"))?; + let description = args["description"].as_str().unwrap_or(""); + let repo = df_storage::crud::ProjectRepo::new(db); + let record = ProjectRecord { + id: new_id(), + name: name.to_string(), + description: description.to_string(), + status: "planning".to_string(), + idea_id: None, + created_at: now_millis(), + updated_at: now_millis(), + }; + let id = record.id.clone(); + repo.insert(record).await?; + Ok(serde_json::json!({ "id": id, "name": name, "status": "planning" })) + } + "create_task" => { + let project_id = args["project_id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 project_id"))?; + let title = args["title"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 title"))?; + let repo = df_storage::crud::TaskRepo::new(db); + let record = TaskRecord { + id: new_id(), + project_id: project_id.to_string(), + title: title.to_string(), + description: args["description"].as_str().unwrap_or("").to_string(), + status: "todo".to_string(), + priority: args["priority"].as_i64().unwrap_or(2) as i32, + branch_name: None, + assignee: None, + workflow_def_id: None, + base_branch: None, + created_at: now_millis(), + updated_at: now_millis(), + }; + let id = record.id.clone(); + repo.insert(record).await?; + Ok(serde_json::json!({ "id": id, "title": title, "status": "todo" })) + } + "create_idea" => { + let title = args["title"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 title"))?; + let repo = df_storage::crud::IdeaRepo::new(db); + let record = IdeaRecord { + id: new_id(), + title: title.to_string(), + description: args["description"].as_str().unwrap_or("").to_string(), + status: "draft".to_string(), + priority: args["priority"].as_i64().unwrap_or(1) as i32, + score: None, + tags: args["tags"].as_str().map(|s| s.to_string()), + source: args["source"].as_str().map(|s| s.to_string()), + promoted_to: None, + ai_analysis: None, + scores: None, + created_at: now_millis(), + updated_at: now_millis(), + }; + let id = record.id.clone(); + repo.insert(record).await?; + Ok(serde_json::json!({ "id": id, "title": title, "status": "draft" })) + } + + // ── 高风险 ── + "delete_project" => { + let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?; + let repo = df_storage::crud::ProjectRepo::new(db); + let deleted = repo.delete(id).await?; + Ok(serde_json::json!({ "deleted": deleted, "id": id })) + } + "run_workflow" => { + // run_workflow 需要完整的 DAG 执行,这里返回提示由前端触发 + Ok(serde_json::json!({ "note": "请通过工作流页面运行工作流", "tool": "run_workflow" })) + } + + // ── 文件系统 ── + "read_file" => { + let path = args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?; + validate_path(path)?; + let metadata = tokio::fs::metadata(path).await + .map_err(|e| anyhow::anyhow!("无法访问文件 {}: {}", path, e))?; + if metadata.len() > 1_048_576 { + anyhow::bail!("文件超过 1MB 限制 ({} 字节)", metadata.len()); + } + let content = tokio::fs::read_to_string(path).await + .map_err(|e| anyhow::anyhow!("读取文件失败: {}", e))?; + // 可选 offset/limit 截取 + let result = if let Some(offset) = args["offset"].as_u64() { + let lines: Vec<&str> = content.lines().collect(); + let skip = offset as usize; + let limit = args["limit"].as_u64().unwrap_or(200) as usize; + let sliced: Vec<&str> = lines.into_iter().skip(skip).take(limit).collect(); + sliced.join("\n") + } else { + content.clone() + }; + let line_count = content.lines().count(); + Ok(serde_json::json!({ + "path": path, + "content": result, + "size": metadata.len(), + "lines": line_count, + })) + } + "list_directory" => { + let path = args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?; + validate_path(path)?; + let recursive = args["recursive"].as_bool().unwrap_or(false); + let mut entries = Vec::new(); + list_dir_recursive(path, recursive, 0, 3, &mut entries).await?; + Ok(serde_json::json!({ + "path": path, + "entries": entries, + })) + } + "write_file" => { + let path = args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?; + validate_path(path)?; + let content = args["content"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 content 参数"))?; + // 确保父目录存在 + if let Some(parent) = std::path::Path::new(path).parent() { + tokio::fs::create_dir_all(parent).await + .map_err(|e| anyhow::anyhow!("创建目录失败: {}", e))?; + } + tokio::fs::write(path, content).await + .map_err(|e| anyhow::anyhow!("写入文件失败: {}", e))?; + Ok(serde_json::json!({ + "path": path, + "bytes_written": content.len(), + })) + } + + _ => Err(anyhow::anyhow!("未知工具: {}", tool_name)), + } +} + +/// 从消息历史中提取对话标题(取第一条用户消息前 30 字) +fn extract_title(messages: &[ChatMessage]) -> Option { + messages.iter() + .find(|m| matches!(m.role, MessageRole::User)) + .map(|m| { + let mut chars = m.content.chars(); + let t: String = chars.by_ref().take(30).collect(); + if chars.next().is_some() { format!("{}...", t) } else { t } + }) +} + +/// 在消息历史中查找并替换指定 tool_call_id 的 tool_result 消息 +fn replace_tool_result(messages: &mut Vec, tool_call_id: &str, new_content: &str) { + if let Some(msg) = messages.iter_mut().find(|m| { + matches!(m.role, MessageRole::Tool) && m.tool_call_id.as_deref() == Some(tool_call_id) + }) { + msg.content = new_content.to_string(); + } +} + +/// 递归列出目录内容(最多 max_depth 层) +fn list_dir_recursive<'a>( + path: &'a str, + recursive: bool, + depth: usize, + max_depth: usize, + result: &'a mut Vec, +) -> std::pin::Pin> + Send + 'a>> { + Box::pin(async move { + let mut dir = tokio::fs::read_dir(path).await + .map_err(|e| anyhow::anyhow!("无法读取目录 {}: {}", path, e))?; + while let Some(entry) = dir.next_entry().await? { + let name = entry.file_name().to_string_lossy().to_string(); + let metadata = entry.metadata().await?; + let is_dir = metadata.is_dir(); + let entry_json = serde_json::json!({ + "name": name, + "type": if is_dir { "directory" } else { "file" }, + "size": metadata.len(), + "depth": depth, + }); + result.push(entry_json); + if recursive && is_dir && depth < max_depth { + let child_path = std::path::Path::new(path).join(&name).to_string_lossy().into_owned(); + list_dir_recursive(&child_path, true, depth + 1, max_depth, result).await?; + } + } + Ok(()) + }) +} diff --git a/src-tauri/src/commands/idea.rs b/src-tauri/src/commands/idea.rs new file mode 100644 index 0000000..f1b44e6 --- /dev/null +++ b/src-tauri/src/commands/idea.rs @@ -0,0 +1,85 @@ +//! 想法相关命令 + +use serde::Deserialize; +use tauri::State; + +use df_core::types::new_id; +use df_storage::models::IdeaRecord; + +use crate::state::AppState; + +use super::now_millis; + +/// 创建想法入参 +#[derive(Debug, Deserialize)] +pub struct CreateIdeaInput { + pub title: String, + #[serde(default)] + pub description: String, + #[serde(default = "default_priority")] + pub priority: i32, + /// 标签 JSON 数组字符串 + pub tags: Option, + pub source: Option, +} + +fn default_priority() -> i32 { + 1 +} + +/// 列出全部想法 +#[tauri::command] +pub async fn list_ideas(state: State<'_, AppState>) -> Result, String> { + state.ideas.list_all().await.map_err(|e| e.to_string()) +} + +/// 创建想法,返回完整记录 +#[tauri::command] +pub async fn create_idea( + state: State<'_, AppState>, + input: CreateIdeaInput, +) -> Result { + let now = now_millis(); + let record = IdeaRecord { + id: new_id(), + title: input.title, + description: input.description, + status: "draft".to_string(), + priority: input.priority, + score: None, + tags: input.tags, + source: input.source, + promoted_to: None, + ai_analysis: None, + scores: None, + created_at: now.clone(), + updated_at: now, + }; + state + .ideas + .insert(record.clone()) + .await + .map_err(|e| e.to_string())?; + Ok(record) +} + +/// 更新想法单个字段(字段名走 df-storage 白名单校验) +#[tauri::command] +pub async fn update_idea( + state: State<'_, AppState>, + id: String, + field: String, + value: String, +) -> Result { + state + .ideas + .update_field(&id, &field, &value) + .await + .map_err(|e| e.to_string()) +} + +/// 删除想法 +#[tauri::command] +pub async fn delete_idea(state: State<'_, AppState>, id: String) -> Result { + state.ideas.delete(&id).await.map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..e8aca7b --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,18 @@ +//! Tauri IPC 命令层 — 按业务域拆分模块 +//! +//! 所有 command 统一返回 `Result`,错误经 `to_string()` 传给前端。 + +pub mod ai; +pub mod idea; +pub mod project; +pub mod task; +pub mod workflow; + +/// 当前时间戳(毫秒字符串)— 与 df-storage 内部的时间格式保持一致 +pub(crate) fn now_millis() -> String { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + .to_string() +} diff --git a/src-tauri/src/commands/project.rs b/src-tauri/src/commands/project.rs new file mode 100644 index 0000000..a18c23e --- /dev/null +++ b/src-tauri/src/commands/project.rs @@ -0,0 +1,84 @@ +//! 项目相关命令 + +use serde::Deserialize; +use tauri::State; + +use df_core::types::new_id; +use df_storage::models::ProjectRecord; + +use crate::state::AppState; + +use super::now_millis; + +/// 创建项目入参 +#[derive(Debug, Deserialize)] +pub struct CreateProjectInput { + pub name: String, + #[serde(default)] + pub description: String, + pub idea_id: Option, +} + +/// 列出全部项目 +#[tauri::command] +pub async fn list_projects(state: State<'_, AppState>) -> Result, String> { + state.projects.list_all().await.map_err(|e| e.to_string()) +} + +/// 创建项目,返回完整记录 +#[tauri::command] +pub async fn create_project( + state: State<'_, AppState>, + input: CreateProjectInput, +) -> Result { + let now = now_millis(); + let record = ProjectRecord { + id: new_id(), + name: input.name, + description: input.description, + status: "planning".to_string(), + idea_id: input.idea_id, + created_at: now.clone(), + updated_at: now, + }; + state + .projects + .insert(record.clone()) + .await + .map_err(|e| e.to_string())?; + Ok(record) +} + +/// 按 ID 查询项目 +#[tauri::command] +pub async fn get_project( + state: State<'_, AppState>, + id: String, +) -> Result, String> { + state + .projects + .get_by_id(&id) + .await + .map_err(|e| e.to_string()) +} + +/// 更新项目单个字段(字段名走 df-storage 白名单校验) +#[tauri::command] +pub async fn update_project( + state: State<'_, AppState>, + id: String, + field: String, + value: String, +) -> Result { + state + .projects + .update_field(&id, &field, &value) + .await + .map_err(|e| e.to_string()) +} + +/// 删除项目 +#[tauri::command] +pub async fn delete_project(state: State<'_, AppState>, id: String) -> Result { + state.projects.delete(&id).await.map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/task.rs b/src-tauri/src/commands/task.rs new file mode 100644 index 0000000..e7c6879 --- /dev/null +++ b/src-tauri/src/commands/task.rs @@ -0,0 +1,91 @@ +//! 任务相关命令 + +use serde::Deserialize; +use tauri::State; + +use df_core::types::new_id; +use df_storage::models::TaskRecord; + +use crate::state::AppState; + +use super::now_millis; + +/// 创建任务入参 +#[derive(Debug, Deserialize)] +pub struct CreateTaskInput { + pub project_id: String, + pub title: String, + #[serde(default)] + pub description: String, + #[serde(default = "default_priority")] + pub priority: i32, + pub branch_name: Option, + pub assignee: Option, +} + +fn default_priority() -> i32 { + 1 +} + +/// 列出任务,可按 project_id 过滤 +#[tauri::command] +pub async fn list_tasks( + state: State<'_, AppState>, + project_id: Option, +) -> Result, String> { + let result = match project_id { + Some(pid) => state.tasks.query("project_id", &pid).await, + None => state.tasks.list_all().await, + }; + result.map_err(|e| e.to_string()) +} + +/// 创建任务,返回完整记录 +#[tauri::command] +pub async fn create_task( + state: State<'_, AppState>, + input: CreateTaskInput, +) -> Result { + let now = now_millis(); + let record = TaskRecord { + id: new_id(), + project_id: input.project_id, + title: input.title, + description: input.description, + status: "todo".to_string(), + priority: input.priority, + branch_name: input.branch_name, + assignee: input.assignee, + workflow_def_id: None, + base_branch: None, + created_at: now.clone(), + updated_at: now, + }; + state + .tasks + .insert(record.clone()) + .await + .map_err(|e| e.to_string())?; + Ok(record) +} + +/// 更新任务单个字段(字段名走 df-storage 白名单校验) +#[tauri::command] +pub async fn update_task( + state: State<'_, AppState>, + id: String, + field: String, + value: String, +) -> Result { + state + .tasks + .update_field(&id, &field, &value) + .await + .map_err(|e| e.to_string()) +} + +/// 删除任务 +#[tauri::command] +pub async fn delete_task(state: State<'_, AppState>, id: String) -> Result { + state.tasks.delete(&id).await.map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/workflow.rs b/src-tauri/src/commands/workflow.rs new file mode 100644 index 0000000..0874bcf --- /dev/null +++ b/src-tauri/src/commands/workflow.rs @@ -0,0 +1,187 @@ +//! 工作流相关命令 — 触发执行、查询执行记录 +//! +//! 事件转发:run_workflow 在执行前订阅事件总线, +//! 将 WorkflowEvent 包装为 `{ execution_id, event }` 通过 `workflow-event` 事件发给前端。 + +use serde::Serialize; +use tauri::{AppHandle, Emitter, State}; +use tokio::sync::broadcast::error::RecvError; + +use df_core::events::{WorkflowEvent, HumanApprovalResponse}; +use df_core::types::new_id; +use df_storage::crud::WorkflowRepo; +use df_storage::models::WorkflowRecord; +use df_workflow::dag_def::DagDef; +use df_workflow::executor::DagExecutor; + +use crate::state::AppState; + +use super::now_millis; + +/// 转发到前端的事件载荷 +#[derive(Debug, Clone, Serialize)] +struct WorkflowEventPayload { + /// 工作流执行记录 ID + execution_id: String, + /// 原始工作流事件(serde tag = "type") + event: WorkflowEvent, +} + +/// 触发工作流执行(核心命令) +/// +/// 流程:build_dag 校验 → 写入执行记录(status=running) → 后台异步执行 → +/// 事件经 EventBus 转发到前端 → 完成后更新执行记录状态。 +/// 立即返回执行记录 ID,前端凭此关联后续事件。 +#[tauri::command] +pub async fn run_workflow( + app: AppHandle, + state: State<'_, AppState>, + name: String, + dag: DagDef, + config: serde_json::Value, +) -> Result { + // 1. 先构建运行时 DAG,校验失败直接返回,不落库 + let runtime_dag = state.registry.build_dag(&dag).map_err(|e| e.to_string())?; + + // 2. 写入执行记录(status=running) + let execution_id = new_id(); + let dag_json = serde_json::to_string(&dag).map_err(|e| e.to_string())?; + let record = WorkflowRecord { + id: execution_id.clone(), + name, + dag_json, + status: "running".to_string(), + triggered_by: Some("manual".to_string()), + project_id: None, + task_id: None, + created_at: now_millis(), + completed_at: None, + }; + state + .workflows + .insert(record) + .await + .map_err(|e| e.to_string())?; + + // 3. 订阅事件总线,把事件转发给前端(收到完成/失败事件后退出) + let mut rx = state.event_bus.subscribe(); + let forward_app = app.clone(); + let forward_exec_id = execution_id.clone(); + tauri::async_runtime::spawn(async move { + loop { + match rx.recv().await { + Ok(event) => { + let finished = matches!( + event, + WorkflowEvent::WorkflowCompleted { .. } + | WorkflowEvent::WorkflowFailed { .. } + ); + let payload = WorkflowEventPayload { + execution_id: forward_exec_id.clone(), + event, + }; + if let Err(e) = forward_app.emit("workflow-event", &payload) { + tracing::warn!("工作流事件转发失败: {}", e); + } + if finished { + break; + } + } + // 消费过慢丢失部分事件时继续接收 + Err(RecvError::Lagged(n)) => { + tracing::warn!("工作流事件消费滞后,丢失 {} 条", n); + } + Err(RecvError::Closed) => break, + } + } + }); + + // 4. 后台异步执行 DAG,完成后更新执行记录状态 + let event_bus = state.event_bus.clone(); + let db = state.db.clone(); + let exec_id = execution_id.clone(); + tauri::async_runtime::spawn(async move { + let mut executor = DagExecutor::new(event_bus.clone()); + let result = executor.run(&runtime_dag, config).await; + + let (status, error) = match &result { + Ok(_) => ("completed", None), + Err(e) => ("failed", Some(format!("{:#}", e))), + }; + + // 更新执行记录(Repo 内部仅持有 Arc 连接,重建开销可忽略) + let workflows = WorkflowRepo::new(&db); + if let Err(e) = workflows.update_field(&exec_id, "status", status).await { + tracing::error!("更新工作流状态失败: {}", e); + } + if let Err(e) = workflows + .update_field(&exec_id, "completed_at", &now_millis()) + .await + { + tracing::error!("更新工作流完成时间失败: {}", e); + } + + // 执行失败时补发 WorkflowFailed(执行器内部只发 NodeFailed) + if let Some(error) = error { + event_bus + .send(WorkflowEvent::WorkflowFailed { + error, + failed_node: String::new(), + }) + .await; + } + }); + + Ok(execution_id) +} + +/// 列出全部工作流执行记录 +#[tauri::command] +pub async fn list_workflow_executions( + state: State<'_, AppState>, +) -> Result, String> { + state.workflows.list_all().await.map_err(|e| e.to_string()) +} + +/// 按 ID 查询工作流执行记录 +#[tauri::command] +pub async fn get_workflow_execution( + state: State<'_, AppState>, + id: String, +) -> Result, String> { + state + .workflows + .get_by_id(&id) + .await + .map_err(|e| e.to_string()) +} + +/// 发送人工审批响应 +#[tauri::command] +pub async fn approve_human_approval( + app: AppHandle, + state: State<'_, AppState>, + execution_id: String, + node_id: String, + decision: String, + comment: Option, +) -> Result<(), String> { + // 发送审批响应到事件总线 + let response = HumanApprovalResponse { + execution_id: execution_id.clone(), + node_id: node_id.clone(), + decision, + comment, + }; + + let event = WorkflowEvent::HumanApprovalResponse { + execution_id: response.execution_id, + node_id: response.node_id, + decision: response.decision, + comment: response.comment, + }; + + // 直接发送到全局事件总线 + state.event_bus.send(event).await; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..29d36e4 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,70 @@ +//! DevFlow Tauri 入口 — 初始化 AppState 并注册全部 IPC 命令 + +mod commands; +mod state; + +use tauri::Manager; + +use state::AppState; + +#[tauri::command] +fn greet(name: &str) -> String { + format!("Hello, {}! Welcome to DevFlow.", name) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .setup(|app| { + // 数据库放在系统应用数据目录下:/devflow.db + let data_dir = app.path().app_data_dir()?; + std::fs::create_dir_all(&data_dir)?; + let db_path = data_dir.join("devflow.db"); + + // 初始化全局状态(打开数据库 + 执行迁移) + let app_state = tauri::async_runtime::block_on(AppState::init(&db_path))?; + app.manage(app_state); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + greet, + // 项目 + commands::project::list_projects, + commands::project::create_project, + commands::project::get_project, + commands::project::update_project, + commands::project::delete_project, + // 任务 + commands::task::list_tasks, + commands::task::create_task, + commands::task::update_task, + commands::task::delete_task, + // 想法 + commands::idea::list_ideas, + commands::idea::create_idea, + commands::idea::update_idea, + commands::idea::delete_idea, + // 工作流 + commands::workflow::run_workflow, + commands::workflow::list_workflow_executions, + commands::workflow::get_workflow_execution, + commands::workflow::approve_human_approval, + // AI 聊天 + commands::ai::ai_chat_send, + commands::ai::ai_chat_stop, + commands::ai::ai_approve, + commands::ai::ai_chat_clear, + commands::ai::ai_list_providers, + commands::ai::ai_save_provider, + commands::ai::ai_set_provider, + // AI 对话管理 + commands::ai::ai_conversation_create, + commands::ai::ai_conversation_list, + commands::ai::ai_conversation_switch, + commands::ai::ai_conversation_delete, + commands::ai::ai_conversation_rename, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..e75f473 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + devflow_lib::run() +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs new file mode 100644 index 0000000..7b7606c --- /dev/null +++ b/src-tauri/src/state.rs @@ -0,0 +1,282 @@ +//! 应用全局状态 — 数据库、Repo、事件总线、节点注册表、AI 会话 + +use std::path::Path; +use std::sync::Arc; + +use anyhow::Result; +use tokio::sync::Mutex; + +use df_ai::ai_tools::{AiToolRegistry, RiskLevel}; +use df_storage::crud::{ + AiConversationRepo, AiProviderRepo, AiToolExecutionRepo, IdeaRepo, NodeExecutionRepo, + ProjectRepo, ReleaseRepo, TaskRepo, WorkflowRepo, +}; +use df_storage::db::Database; +use df_workflow::eventbus::EventBus; +use df_workflow::registry::NodeRegistry; + +use crate::commands::ai::AiSession; + +/// 应用全局状态 — 通过 `app.manage()` 注入,command 中以 `State<'_, AppState>` 取用 +pub struct AppState { + /// 数据库句柄(Arc 包装,便于在异步任务中重建 Repo) + pub db: Arc, + /// 想法表 Repo + pub ideas: IdeaRepo, + /// 项目表 Repo + pub projects: ProjectRepo, + /// 任务表 Repo + pub tasks: TaskRepo, + /// 发布表 Repo + pub releases: ReleaseRepo, + /// 工作流执行表 Repo + pub workflows: WorkflowRepo, + /// 节点执行表 Repo + pub node_executions: NodeExecutionRepo, + /// 工作流事件总线(tokio broadcast) + pub event_bus: EventBus, + /// 节点注册表(已注册内置节点) + pub registry: Arc, + // ── AI ── + /// AI 提供商配置 Repo + pub ai_providers: AiProviderRepo, + /// AI 对话历史 Repo + pub ai_conversations: AiConversationRepo, + /// AI 工具执行审计 Repo + pub ai_tool_executions: AiToolExecutionRepo, + /// AI 工具注册表 + pub ai_tools: Arc, + /// AI 会话状态 + pub ai_session: Arc>, +} + +impl AppState { + /// 初始化应用状态:打开(或创建)数据库并执行迁移,构建各 Repo 与节点注册表 + pub async fn init(db_path: &Path) -> Result { + let db = Arc::new(Database::open(db_path).await?); + let ai_tools = Arc::new(build_ai_tool_registry()); + Ok(Self { + ideas: IdeaRepo::new(&db), + projects: ProjectRepo::new(&db), + tasks: TaskRepo::new(&db), + releases: ReleaseRepo::new(&db), + workflows: WorkflowRepo::new(&db), + node_executions: NodeExecutionRepo::new(&db), + ai_providers: AiProviderRepo::new(&db), + ai_conversations: AiConversationRepo::new(&db), + ai_tool_executions: AiToolExecutionRepo::new(&db), + ai_session: Arc::new(Mutex::new(AiSession::new())), + db, + event_bus: EventBus::new(), + registry: Arc::new(build_registry()), + ai_tools, + }) + } +} + +/// 构建节点注册表 — 注册内置节点 +/// +/// 注意:不使用 `NodeRegistry::default()`,其 script 工厂为占位实现(会 panic), +/// 这里注册 df-nodes 提供的真实 ScriptNode 和 HumanNode。 +fn build_registry() -> NodeRegistry { + let mut registry = NodeRegistry::new(); + registry.register("script", |_config| { + Box::new(df_nodes::script_node::ScriptNode) + }); + registry.register("human", |_config| { + Box::new(df_nodes::human_node::HumanNode) + }); + registry.register("ai", |_config| { + Box::new(df_nodes::ai_node::AiNode) + }); + registry +} + +// ============================================================ +// AI 工具注册 +// ============================================================ + +use serde_json::json; + +fn object_schema(fields: Vec<(&str, &str, bool)>) -> serde_json::Value { + let mut props = serde_json::Map::new(); + let mut required = Vec::new(); + + for (name, typ, req) in &fields { + props.insert(name.to_string(), json!({ "type": typ })); + if *req { + required.push(name.to_string()); + } + } + + json!({ + "type": "object", + "properties": props, + "required": required, + }) +} + +/// 构建 AI 工具注册表 — 注册所有 CRUD 操作为可调用工具 +fn build_ai_tool_registry() -> AiToolRegistry { + let mut registry = AiToolRegistry::new(); + + // ── 只读工具 (Low risk) ── + + registry.register( + "list_projects", + "列出所有项目,返回项目列表(ID、名称、状态、描述)", + object_schema(vec![]), + RiskLevel::Low, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry.register( + "list_tasks", + "列出任务,可按 project_id 筛选", + object_schema(vec![("project_id", "string", false)]), + RiskLevel::Low, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry.register( + "list_ideas", + "列出所有想法", + object_schema(vec![]), + RiskLevel::Low, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + // ── 创建工具 (Medium risk) ── + + registry.register( + "update_project", + "更新项目的指定字段(name/status/description),需要提供项目 ID、字段名和新值", + object_schema(vec![ + ("id", "string", true), + ("field", "string", true), + ("value", "string", true), + ]), + RiskLevel::Medium, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry.register( + "create_project", + "创建新项目", + object_schema(vec![ + ("name", "string", true), + ("description", "string", false), + ]), + RiskLevel::Medium, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry.register( + "create_task", + "在指定项目下创建新任务", + object_schema(vec![ + ("project_id", "string", true), + ("title", "string", true), + ("description", "string", false), + ("priority", "integer", false), + ]), + RiskLevel::Medium, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry.register( + "create_idea", + "捕获一个新想法", + object_schema(vec![ + ("title", "string", true), + ("description", "string", false), + ("tags", "string", false), + ("source", "string", false), + ]), + RiskLevel::Medium, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + // ── 高风险工具 (High risk) ── + + registry.register( + "delete_project", + "删除项目及其所有关联数据", + object_schema(vec![("id", "string", true)]), + RiskLevel::High, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry.register( + "run_workflow", + "运行指定的工作流 DAG", + object_schema(vec![ + ("name", "string", true), + ("dag", "object", true), + ]), + RiskLevel::High, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + // ── 文件系统工具 ── + + registry.register( + "read_file", + "读取文件内容,返回文本内容。支持 offset 和 limit 参数分页读取大文件", + object_schema(vec![ + ("path", "string", true), + ("offset", "integer", false), + ("limit", "integer", false), + ]), + RiskLevel::Low, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry.register( + "list_directory", + "列出目录内容,返回文件和子目录列表(名称、类型、大小)", + object_schema(vec![ + ("path", "string", true), + ("recursive", "boolean", false), + ]), + RiskLevel::Low, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry.register( + "write_file", + "写入或创建文件,自动创建不存在的父目录", + object_schema(vec![ + ("path", "string", true), + ("content", "string", true), + ]), + RiskLevel::Medium, + Box::new(|_args| { + Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) }) + }), + ); + + registry +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..faf5902 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,37 @@ +{ + "productName": "devflow", + "version": "0.1.0", + "identifier": "top.1216.devflow", + "build": { + "beforeDevCommand": "bun dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "bun run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "DevFlow", + "width": 1024, + "height": 768, + "resizable": true, + "fullscreen": false, + "devtools": true + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..a8ae511 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,440 @@ + + + + + diff --git a/src/api/ai.ts b/src/api/ai.ts new file mode 100644 index 0000000..16721c3 --- /dev/null +++ b/src/api/ai.ts @@ -0,0 +1,90 @@ +//! AI 聊天 API — IPC invoke 封装 + 事件监听 + +import { invoke } from '@tauri-apps/api/core' +import { listen, type UnlistenFn } from '@tauri-apps/api/event' +import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProviderConfig } from './types' + +export const aiApi = { + /** 发送消息(非阻塞,通过 ai-chat-event 流式返回) */ + sendMessage(message: string, language?: string): Promise { + return invoke('ai_chat_send', { message, language: language || 'zh-CN' }) + }, + + /** 批准/拒绝工具调用 */ + approve(toolCallId: string, approved: boolean): Promise { + return invoke('ai_approve', { toolCallId, approved }) + }, + + /** 清空对话 */ + clearChat(): Promise { + return invoke('ai_chat_clear') + }, + + /** 停止当前生成 */ + stopChat(): Promise { + return invoke('ai_chat_stop') + }, + + /** 列出所有 AI 提供商 */ + listProviders(): Promise { + return invoke('ai_list_providers') + }, + + /** 保存 AI 提供商 */ + saveProvider(input: { + id?: string + name: string + providerType: string + baseUrl: string + apiKey: string + defaultModel: string + }): Promise { + return invoke('ai_save_provider', { + id: input.id, + name: input.name, + providerType: input.providerType, + baseUrl: input.baseUrl, + apiKey: input.apiKey, + defaultModel: input.defaultModel, + }) + }, + + /** 设置活跃提供商 */ + setProvider(providerId: string): Promise { + return invoke('ai_set_provider', { providerId }) + }, + + /** 监听 AI 聊天事件 */ + onEvent(callback: (event: AiChatEvent) => void): Promise { + return listen('ai-chat-event', (e) => { + callback(e.payload) + }) + }, + + // ── 对话管理 ── + + /** 创建新对话 */ + createConversation(): Promise<{ id: string }> { + return invoke('ai_conversation_create') + }, + + /** 列出所有对话(摘要) */ + listConversations(): Promise { + return invoke('ai_conversation_list') + }, + + /** 切换到指定对话 */ + switchConversation(conversationId: string): Promise { + return invoke('ai_conversation_switch', { conversationId }) + }, + + /** 删除对话 */ + deleteConversation(conversationId: string): Promise { + return invoke('ai_conversation_delete', { conversationId }) + }, + + /** 重命名对话标题 */ + renameConversation(conversationId: string, title: string): Promise { + return invoke('ai_conversation_rename', { conversationId, title }) + }, +} diff --git a/src/api/idea.ts b/src/api/idea.ts new file mode 100644 index 0000000..b46f560 --- /dev/null +++ b/src/api/idea.ts @@ -0,0 +1,20 @@ +import { invoke } from '@tauri-apps/api/core' +import type { IdeaRecord, CreateIdeaInput } from './types' + +export const ideaApi = { + list(): Promise { + return invoke('list_ideas') + }, + + create(input: CreateIdeaInput): Promise { + return invoke('create_idea', { input }) + }, + + update(id: string, field: string, value: string): Promise { + return invoke('update_idea', { id, field, value }) + }, + + delete(id: string): Promise { + return invoke('delete_idea', { id }) + }, +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..7d2024e --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,6 @@ +export * from './types' +export { projectApi } from './project' +export { taskApi } from './task' +export { ideaApi } from './idea' +export { workflowApi } from './workflow' +export { aiApi } from './ai' diff --git a/src/api/project.ts b/src/api/project.ts new file mode 100644 index 0000000..e2bfd50 --- /dev/null +++ b/src/api/project.ts @@ -0,0 +1,24 @@ +import { invoke } from '@tauri-apps/api/core' +import type { ProjectRecord, CreateProjectInput } from './types' + +export const projectApi = { + list(): Promise { + return invoke('list_projects') + }, + + create(input: CreateProjectInput): Promise { + return invoke('create_project', { input }) + }, + + get(id: string): Promise { + return invoke('get_project', { id }) + }, + + update(id: string, field: string, value: string): Promise { + return invoke('update_project', { id, field, value }) + }, + + delete(id: string): Promise { + return invoke('delete_project', { id }) + }, +} diff --git a/src/api/task.ts b/src/api/task.ts new file mode 100644 index 0000000..6974a03 --- /dev/null +++ b/src/api/task.ts @@ -0,0 +1,20 @@ +import { invoke } from '@tauri-apps/api/core' +import type { TaskRecord, CreateTaskInput } from './types' + +export const taskApi = { + list(projectId?: string): Promise { + return invoke('list_tasks', { projectId: projectId ?? null }) + }, + + create(input: CreateTaskInput): Promise { + return invoke('create_task', { input }) + }, + + update(id: string, field: string, value: string): Promise { + return invoke('update_task', { id, field, value }) + }, + + delete(id: string): Promise { + return invoke('delete_task', { id }) + }, +} diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..f6633d1 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,204 @@ +//! TypeScript 类型定义 — 与 Rust Record 结构体严格对齐 + +// ============================================================ +// 想法 +// ============================================================ + +export interface IdeaRecord { + id: string + title: string + description: string + status: string // draft | pending_review | approved | promoted | rejected + priority: number + score: number | null + tags: string | null // JSON 数组字符串 + source: string | null + promoted_to: string | null + ai_analysis: string | null + scores: string | null + created_at: string + updated_at: string +} + +export interface CreateIdeaInput { + title: string + description?: string + priority?: number + tags?: string + source?: string +} + +// ============================================================ +// 项目 +// ============================================================ + +export interface ProjectRecord { + id: string + name: string + description: string + status: string // planning | in_progress | paused | completed | cancelled + idea_id: string | null + created_at: string + updated_at: string +} + +export interface CreateProjectInput { + name: string + description?: string + idea_id?: string +} + +// ============================================================ +// 任务 +// ============================================================ + +export interface TaskRecord { + id: string + project_id: string + title: string + description: string + status: string // todo | in_progress | review_ready | merged | abandoned + priority: number // 0=critical, 1=high, 2=medium, 3=low + branch_name: string | null + assignee: string | null + workflow_def_id: string | null + base_branch: string | null + created_at: string + updated_at: string +} + +export interface CreateTaskInput { + project_id: string + title: string + description?: string + priority?: number + branch_name?: string + assignee?: string +} + +// ============================================================ +// 工作流 +// ============================================================ + +export interface WorkflowRecord { + id: string + name: string + dag_json: string + status: string // running | completed | failed | cancelled + triggered_by: string | null + project_id: string | null + task_id: string | null + created_at: string + completed_at: string | null +} + +/** 工作流事件载荷(后端 emit 的结构) */ +export interface WorkflowEventPayload { + execution_id: string + event: { + type: string + [key: string]: unknown + } +} + +// ============================================================ +// 通用 +// ============================================================ + +export type UpdateField = { + id: string + field: string + value: string +} + +// ============================================================ +// AI 聊天 +// ============================================================ + +export interface AiProviderConfig { + id: string + name: string + provider_type: string + api_key: string + base_url: string + default_model: string + models: string | null + is_default: boolean + config: string | null + created_at: string + updated_at: string +} + +/** AI 聊天事件(后端 emit 的结构,conversation_id 用于多对话流式路由) */ +export type AiChatEvent = ({ + type: 'AiTextDelta'; delta: string +} | { + type: 'AiToolCallStarted'; id: string; name: string; args: unknown +} | { + type: 'AiToolCallCompleted'; id: string; result: unknown +} | { + type: 'AiApprovalRequired'; id: string; name: string; args: unknown; reason: string +} | { + type: 'AiApprovalResult'; id: string; approved: boolean +} | { + type: 'AiCompleted'; total_tokens: number +} | { + type: 'AiError'; error: string +} | { + type: 'AiAgentRound'; round: number +}) & { + conversation_id?: string +} + +/** AI 消息(前端渲染用,isError 标记错误消息用于差异化样式) */ +export interface AiMessage { + id: string + role: 'user' | 'assistant' | 'tool' + content: string + isError?: boolean + toolCalls?: AiToolCallInfo[] + timestamp: number +} + +/** 工具调用信息 */ +export interface AiToolCallInfo { + id: string + name: string + args: unknown + status: 'running' | 'pending_approval' | 'completed' | 'rejected' + result?: unknown +} + +/** 对话列表摘要 */ +export interface AiConversationSummary { + id: string + title: string | null + provider_id: string | null + model: string | null + created_at: string + updated_at: string +} + +/** 人工审批请求 */ +export interface HumanApprovalRequest { + execution_id: string + node_id: string + title: string + description: string + options: string[] +} + +/** 人工审批响应 */ +export interface HumanApprovalResponse { + execution_id: string + node_id: string + decision: string + comment?: string +} + +/** 切换对话返回(含 messages JSON) */ +export interface AiConversationDetail { + id: string + title: string | null + messages: string // JSON string of ChatMessage[] +} diff --git a/src/api/workflow.ts b/src/api/workflow.ts new file mode 100644 index 0000000..ccb2f80 --- /dev/null +++ b/src/api/workflow.ts @@ -0,0 +1,29 @@ +import { invoke } from '@tauri-apps/api/core' +import { listen } from '@tauri-apps/api/event' +import type { WorkflowRecord, WorkflowEventPayload } from './types' + +export const workflowApi = { + /** 触发工作流执行,返回 execution_id */ + run(name: string, dag: unknown, config?: Record): Promise { + return invoke('run_workflow', { + name, + dag, + config: config ?? {}, + }) + }, + + listExecutions(): Promise { + return invoke('list_workflow_executions') + }, + + getExecution(id: string): Promise { + return invoke('get_workflow_execution', { id }) + }, + + /** 监听工作流实时事件,返回 unlisten 函数 */ + onEvent(callback: (payload: WorkflowEventPayload) => void): Promise<() => void> { + return listen('workflow-event', (event) => { + callback(event.payload) + }) + }, +} diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue new file mode 100644 index 0000000..0243732 --- /dev/null +++ b/src/components/AiChat.vue @@ -0,0 +1,1348 @@ + + + + + diff --git a/src/i18n/en.ts b/src/i18n/en.ts new file mode 100644 index 0000000..e41ddd8 --- /dev/null +++ b/src/i18n/en.ts @@ -0,0 +1,78 @@ +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', + refresh: 'Refresh', + captureIdea: 'Capture Idea', + stats: { + ideas: 'Ideas', + projects: 'Projects', + activeTasks: 'Active Tasks', + drafts: 'Drafts', + annotations: 'Annotations', + }, + activeProjects: 'Active Projects', + viewAll: 'View all', + ideaPool: 'Idea Pool', + recentDecisions: 'Recent Decisions', + openAnnotations: 'Open Annotations', + aiBatchProcess: 'AI Batch Process', + tasks: '{count} tasks', + taskUnit: '{n} tasks', + trending: 'Trending', + approved: 'Approved', + pending: 'Pending', + reviewing: 'Reviewing', + coding: 'Coding', + testing: 'Testing', + release: 'Release', + empty: { + noProjects: 'No projects yet', + noIdeas: 'No ideas yet, click above to capture', + noData: 'No data', + }, + stage: { + planning: 'Planning', + coding: 'Coding', + paused: 'Paused', + done: 'Done', + cancelled: 'Cancelled', + }, + ideaStatus: { + draft: 'Draft', + pending_review: 'Pending', + approved: 'Approved', + promoted: 'Promoted', + 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/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..b8490b5 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,17 @@ +import { createI18n } from 'vue-i18n' +import zhCN from './zh-CN' +import en from './en' + +// 界面语言:跟随 localStorage 中用户选择,默认简体中文 +const i18n = createI18n({ + legacy: false, + globalInjection: true, // 模板中可直接用 $t / $tc + locale: localStorage.getItem('df-language') || 'zh-CN', + fallbackLocale: 'en', + messages: { + 'zh-CN': zhCN, + en, + }, +}) + +export default i18n diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts new file mode 100644 index 0000000..1df7114 --- /dev/null +++ b/src/i18n/zh-CN.ts @@ -0,0 +1,78 @@ +export default { + nav: { + overview: '总览', + ideas: '灵感', + projects: '项目', + tasks: '任务', + knowledge: '知识库', + decisions: '决策', + settings: '设置', + workspace: '工作区', + traceability: '追溯', + systemReady: '系统就绪', + aiPanel: 'AI 面板', + }, + dashboard: { + title: '总览', + subtitle: '所有系统运行正常', + refresh: '刷新', + captureIdea: '捕捉灵感', + stats: { + ideas: '灵感', + projects: '项目', + activeTasks: '活跃任务', + drafts: '草稿', + annotations: '待处理标注', + }, + activeProjects: '活跃项目', + viewAll: '查看全部', + ideaPool: '灵感', + recentDecisions: '最近决策', + openAnnotations: '待处理标注', + aiBatchProcess: 'AI 批量处理', + tasks: '{count} 个任务', + taskUnit: '{n} 个任务', + trending: '热门', + approved: '已立项', + pending: '待定', + reviewing: '评估中', + coding: '编码中', + testing: '测试中', + release: '发布中', + empty: { + noProjects: '暂无项目,去创建一个', + noIdeas: '暂无灵感,点击上方按钮捕捉', + noData: '暂无数据', + }, + stage: { + planning: '规划中', + coding: '编码中', + paused: '已暂停', + done: '已完成', + cancelled: '已取消', + }, + ideaStatus: { + draft: '草稿', + pending_review: '待评估', + approved: '已立项', + promoted: '已转立项', + rejected: '已驳回', + }, + }, + ai: { + assistant: 'AI 助手', + toolName: { + readFile: '读取文件', + listDirectory: '查看目录', + writeFile: '写入文件', + }, + }, + common: { + justNow: '刚刚', + minutesAgo: '{n} 分钟前', + hoursAgo: '{n} 小时前', + dayAgo: '{n} 天前', + yesterday: '昨天', + ago: '{time}前', + }, +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b4ebbef --- /dev/null +++ b/src/main.ts @@ -0,0 +1,11 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import router from "./router"; +import i18n from "./i18n"; +import "./styles/global.css"; + +createApp(App).use(router).use(i18n).mount("#app"); + +// 启动计时:从 index.html 解析到 Vue 挂载完成 +const t0 = (window as any).__APP_T0 ?? 0; +console.log(`[启动] Vue mount 完成: ${(performance.now() - t0).toFixed(0)}ms`); diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..45ae77d --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,69 @@ +import { createRouter, createWebHashHistory } from 'vue-router' + +const routes = [ + { + path: '/', + redirect: '/ai-home', + }, + { + path: '/ai-home', + name: 'AiHome', + component: () => import('../views/AiHome.vue'), + meta: { title: 'AI Chat' }, + }, + { + path: '/dashboard', + name: 'Dashboard', + component: () => import('../views/Dashboard.vue'), + meta: { title: '总览', icon: 'icon-dashboard' }, + }, + { + path: '/ideas', + name: 'Ideas', + component: () => import('../views/Ideas.vue'), + meta: { title: '灵感', icon: 'icon-lightbulb' }, + }, + { + path: '/projects', + name: 'Projects', + component: () => import('../views/Projects.vue'), + meta: { title: '项目', icon: 'icon-apps' }, + }, + { + path: '/projects/:id', + name: 'ProjectDetail', + component: () => import('../views/ProjectDetail.vue'), + meta: { title: '项目详情', icon: 'icon-apps' }, + }, + { + path: '/tasks', + name: 'Tasks', + component: () => import('../views/Tasks.vue'), + meta: { title: '任务', icon: 'icon-thunder' }, + }, + { + path: '/knowledge', + name: 'Knowledge', + component: () => import('../views/Knowledge.vue'), + meta: { title: '知识库', icon: 'icon-book' }, + }, + { + path: '/settings', + name: 'Settings', + component: () => import('../views/Settings.vue'), + meta: { title: '设置', icon: 'icon-settings' }, + }, + { + path: '/ai-detached', + name: 'AiDetached', + component: () => import('../views/AiDetached.vue'), + meta: { title: 'AI Chat', icon: 'icon-ai' }, + }, +] + +const router = createRouter({ + history: createWebHashHistory(), + routes, +}) + +export default router diff --git a/src/stores/ai.ts b/src/stores/ai.ts new file mode 100644 index 0000000..90957a1 --- /dev/null +++ b/src/stores/ai.ts @@ -0,0 +1,509 @@ +//! AI 聊天 Store — 管理对话状态、流式渲染、工具审批、对话管理 + +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' + +let _unlistenAiEvent: (() => void) | null = null +let _unlistenConvChanged: (() => void) | null = null +let _msgCounter = 0 + +const state = reactive({ + messages: [] as AiMessage[], + streaming: false, + currentText: '', + pendingApprovals: [] as AiToolCallInfo[], + // 正在生成的对话 id:生成中切走时用于路由,后台事件不污染当前视图 + generatingConvId: null as string | null, + providers: [] as AiProviderConfig[], + activeProvider: null as string | null, + panelOpen: true, + // 面板模式:maximized=全宽占满main | sidebar=固定宽侧栏(可拖拽) + maximized: true, + // 对话管理 + conversations: [] as AiConversationSummary[], + activeConversationId: null as string | null, + sidebarOpen: false, + // 窗口分离 + detached: false, + // 吸附跟随中 + docked: false, +}) + +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, + } +} diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..860306e --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,8 @@ +export { useProjectStore } from './project' + +export { useKnowledgeStore } from './knowledge' +export type { KnowledgeItem } from './knowledge' + + +export { useSettingsStore } from './settings' +export type { AIProvider, Connection, GeneralSettings } from './settings' diff --git a/src/stores/knowledge.ts b/src/stores/knowledge.ts new file mode 100644 index 0000000..f24ad67 --- /dev/null +++ b/src/stores/knowledge.ts @@ -0,0 +1,79 @@ +import { reactive, computed } from 'vue' + +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: '🚀' }, + ], +}) + +export function useKnowledgeStore() { + const items = computed(() => state.items) + + 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, + } +} diff --git a/src/stores/project.ts b/src/stores/project.ts new file mode 100644 index 0000000..8378867 --- /dev/null +++ b/src/stores/project.ts @@ -0,0 +1,205 @@ +import { reactive, computed } from 'vue' +import { invoke } from '@tauri-apps/api/core' +import { projectApi, taskApi, ideaApi, workflowApi } from '../api' +import type { ProjectRecord, TaskRecord, IdeaRecord, WorkflowRecord, WorkflowEventPayload } from '../api/types' + +// ── 全局响应式状态(单例) ── + +const state = reactive({ + projects: [] as ProjectRecord[], + tasks: [] as TaskRecord[], + ideas: [] as IdeaRecord[], + workflowExecutions: [] as WorkflowRecord[], + liveEvents: [] as WorkflowEventPayload[], + pendingApproval: null as { + execution_id: string + node_id: string + title: string + description: string + options: string[] + } | null, + loading: false, + error: null as string | null, +}) + +let _eventUnlisten: (() => void) | null = null + +export function useProjectStore() { + // ── 项目 CRUD ── + async function loadProjects() { + state.loading = true + state.error = null + try { + state.projects = await projectApi.list() + } catch (e: any) { + state.error = e?.toString() ?? '加载项目失败' + } finally { + state.loading = false + } + } + + async function createProject(name: string, description = '', ideaId?: string) { + const record = await projectApi.create({ name, description, idea_id: ideaId }) + state.projects.push(record) + return record + } + + async function updateProject(id: string, field: string, value: string) { + await projectApi.update(id, field, value) + const idx = state.projects.findIndex(p => p.id === id) + if (idx >= 0) { + (state.projects[idx] as any)[field] = value + } + } + + async function deleteProject(id: string) { + await projectApi.delete(id) + state.projects = state.projects.filter(p => p.id !== id) + } + + // ── 任务 CRUD ── + async function loadTasks(projectId?: string) { + try { + state.tasks = await taskApi.list(projectId) + } catch (e: any) { + state.error = e?.toString() ?? '加载任务失败' + } + } + + 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 + } + + async function updateTask(id: string, field: string, value: string) { + await taskApi.update(id, field, value) + const idx = state.tasks.findIndex(t => t.id === id) + if (idx >= 0) { + (state.tasks[idx] as any)[field] = value + } + } + + async function deleteTask(id: string) { + await taskApi.delete(id) + state.tasks = state.tasks.filter(t => t.id !== id) + } + + // ── 想法 CRUD ── + async function loadIdeas() { + try { + state.ideas = await ideaApi.list() + } catch (e: any) { + state.error = e?.toString() ?? '加载想法失败' + } + } + + 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 + } + + async function updateIdea(id: string, field: string, value: string) { + await ideaApi.update(id, field, value) + const idx = state.ideas.findIndex(i => i.id === id) + if (idx >= 0) { + (state.ideas[idx] as any)[field] = value + } + } + + async function deleteIdea(id: string) { + await ideaApi.delete(id) + state.ideas = state.ideas.filter(i => i.id !== id) + } + + // ── 工作流 ── + async function runWorkflow(name: string, dag: unknown, config?: Record) { + return await workflowApi.run(name, dag, config) + } + + async function loadWorkflowExecutions() { + try { + state.workflowExecutions = await workflowApi.listExecutions() + } catch (e: any) { + state.error = e?.toString() ?? '加载工作流记录失败' + } + } + + 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 + } + }) + return _eventUnlisten + } + + function clearLiveEvents() { + state.liveEvents = [] + } + + async function approveHumanApproval(decision: string, comment?: string) { + if (!state.pendingApproval) return + + try { + // 调用 IPC 命令发送审批响应 + await invoke('approve_human_approval', { + execution_id: state.pendingApproval.execution_id, + node_id: state.pendingApproval.node_id, + decision, + comment + }) + + // 清除待审批状态 + state.pendingApproval = null + } catch (error) { + console.error('审批失败:', error) + } + } + + function stopEventListener() { + if (_eventUnlisten) { + try { + _eventUnlisten() + } catch (e) { + console.error('停止事件监听失败:', e) + } + _eventUnlisten = null + } + } + + // ── 统计 ── + const stats = computed(() => ({ + ideas: state.ideas.length, + projects: state.projects.length, + activeTasks: state.tasks.filter(t => t.status === 'in_progress').length, + drafts: state.ideas.filter(i => i.status === 'draft').length, + })) + + 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, + // project actions + loadProjects, createProject, updateProject, deleteProject, + // task actions + loadTasks, createTask, updateTask, deleteTask, + // idea actions + loadIdeas, createIdea, updateIdea, deleteIdea, + // workflow actions + runWorkflow, loadWorkflowExecutions, startEventListener, stopEventListener, clearLiveEvents, + approveHumanApproval, pendingApproval: computed(() => state.pendingApproval), + // computed + stats, + }) +} diff --git a/src/stores/settings.ts b/src/stores/settings.ts new file mode 100644 index 0000000..8e8c177 --- /dev/null +++ b/src/stores/settings.ts @@ -0,0 +1,98 @@ +import { reactive, computed } from 'vue' + +export interface AIProvider { + id: number + name: string + apiKey: string + baseUrl: string + models: string[] + isDefault: boolean +} + +export interface Connection { + id: number + name: string + type: 'mysql' | 'ssh' | 'redis' | 'mongo' + typeLabel: string + icon: string + host: string + port: number + user: string + connected: boolean +} + +export interface GeneralSettings { + theme: 'dark' | 'light' | 'system' + language: string + dataDir: string + autoExecute: boolean + logLevel: 'error' | 'warn' | 'info' | 'debug' +} + +const state = reactive({ + aiProviders: [ + { + id: 1, + name: 'Anthropic Claude', + apiKey: 'sk-ant-••••••••••••••••7xKm', + baseUrl: 'https://api.anthropic.com', + models: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514'], + isDefault: true, + }, + { + id: 2, + name: 'Zhipu GLM', + apiKey: '••••••••••••••••a3f2', + baseUrl: 'https://open.bigmodel.cn/api/paas', + models: ['glm-4-plus', 'glm-4-flash'], + isDefault: false, + }, + { + id: 3, + name: 'DeepSeek', + apiKey: '••••••••••••••••9b8c', + baseUrl: 'https://api.deepseek.com', + models: ['deepseek-chat', 'deepseek-coder'], + isDefault: false, + }, + ] as AIProvider[], + + connections: [ + { id: 1, name: 'flux_dev', type: 'mysql' as const, typeLabel: 'MySQL', icon: '🗄️', host: '39.99.243.191', port: 3306, user: 'root', connected: true }, + { id: 2, name: 'flux_dev', type: 'ssh' as const, typeLabel: 'SSH', icon: '🔐', host: '39.99.243.191', port: 22, user: 'root', connected: true }, + { id: 3, name: 'flux_redis', type: 'redis' as const, typeLabel: 'Redis', icon: '⚡', host: '127.0.0.1', port: 6379, user: '', connected: true }, + { id: 4, name: 'suke_dev', type: 'mongo' as const, typeLabel: 'MongoDB', icon: '🍃', host: '127.0.0.1', port: 27017, user: 'admin', connected: false }, + { id: 5, name: 'rds_read', type: 'mysql' as const, typeLabel: 'MySQL', icon: '🗄️', host: 'rm-8vbrd2wil14hclop3vm.mysql.zhangbei.rds.aliyuncs.com', port: 3306, user: 'u_read', connected: true }, + ] as Connection[], + + general: { + theme: 'dark' as const, + language: 'zh-CN', + dataDir: 'E:/wk-lab/devflow/data', + autoExecute: false, + logLevel: 'info' as const, + } as GeneralSettings, +}) + +export function useSettingsStore() { + const aiProviders = computed(() => state.aiProviders) + const connections = computed(() => state.connections) + const general = computed(() => state.general) + + const defaultProvider = computed(() => + state.aiProviders.find(p => p.isDefault) + ) + + const connectionStats = computed(() => ({ + total: state.connections.length, + connected: state.connections.filter(c => c.connected).length, + })) + + return { + aiProviders, + connections, + general, + defaultProvider, + connectionStats, + } +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..bb133a9 --- /dev/null +++ b/src/style.css @@ -0,0 +1 @@ +/* 已迁移到 styles/global.css,此文件保留为空 */ diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..733298d --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,219 @@ +/* ═══════════════════════════════════════════════════════════ + DevFlow Design System — Claude 设计系统规范 + 扁平化 · 0.5px 边框 · 8/12px 圆角 · 400/500 字重 + 紫/青绿/琥珀/珊瑚 四色体系 · 深色优先 + ═══════════════════════════════════════════════════════════ */ + +:root { + /* — Surface — */ + --df-bg: #0c0e1a; + --df-bg-raised: #12152a; + --df-bg-card: #171b33; + --df-bg-card-hover: #1c2140; + --df-sidebar-bg: #090b15; + --df-sidebar-hover: rgba(108,99,255,0.06); + --df-sidebar-active: rgba(108,99,255,0.12); + + /* — Text — */ + --df-text: #eef0f6; + --df-text-secondary: #8b90a8; + --df-text-dim: #4a4f6a; + + /* — Accent: 紫色系(系统功能、互动元素)— */ + --df-accent: #7b6ff0; + --df-accent-hover: #6a5de0; + --df-accent-soft: rgba(123,111,240,0.15); + --df-accent-bg: rgba(123,111,240,0.08); + + /* — Semantic — */ + /* 青绿:成功/流程 */ + --df-success: #3ddba0; + --df-success-bg: rgba(61,219,160,0.12); + /* 琥珀:警告/热度 */ + --df-warning: #f0c75e; + --df-warning-bg: rgba(240,199,94,0.12); + /* 珊瑚:危险/对比 */ + --df-danger: #f06565; + --df-danger-bg: rgba(240,101,101,0.12); + /* 蓝色:信息 */ + --df-info: #5eaff0; + --df-info-bg: rgba(94,175,240,0.12); + + /* — Border: 统一 0.5px 极细边框 — */ + --df-border: rgba(255,255,255,0.06); + --df-border-strong: rgba(255,255,255,0.10); + + /* — Radius: 2/4/6/8/10 渐进(全站统一走 token)— */ + --df-radius-xs: 2px; + --df-radius-sm: 4px; + --df-radius: 6px; + --df-radius-md: 8px; + --df-radius-lg: 10px; + + /* — Spacing: 全站间距基准(参考总览紧凑度,回退改值即可)— */ + --df-gap-page: 14px; /* 页面区块间距:header→主区、区块垂直分隔 */ + --df-gap-grid: 12px; /* 卡片/网格 gap */ + --df-gap-head: 10px; /* 面板标题→内容 */ + --df-pad-panel: 14px 16px; /* 面板/卡片内边距 */ + + /* — Typography: 仅 400/500 字重 — */ + --df-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --df-font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace; + + /* — Transition — */ + --df-ease: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ═══ 亮色主题 ═══ */ +[data-theme="light"] { + --df-bg: #f5f6fa; + --df-bg-raised: #ffffff; + --df-bg-card: #ffffff; + --df-bg-card-hover: #f0f1f5; + --df-sidebar-bg: #ffffff; + --df-sidebar-hover: rgba(108,99,255,0.06); + --df-sidebar-active: rgba(108,99,255,0.10); + + --df-text: #1a1d2e; + --df-text-secondary: #5a5f7a; + --df-text-dim: #9a9fba; + + --df-accent: #6c5ce7; + --df-accent-hover: #5b4bd6; + --df-accent-soft: rgba(108,92,231,0.15); + --df-accent-bg: rgba(108,92,231,0.06); + + --df-success: #00b894; + --df-success-bg: rgba(0,184,148,0.10); + --df-warning: #e17055; + --df-warning-bg: rgba(225,112,85,0.10); + --df-danger: #d63031; + --df-danger-bg: rgba(214,48,49,0.10); + --df-info: #0984e3; + --df-info-bg: rgba(9,132,227,0.10); + + --df-border: rgba(0,0,0,0.08); + --df-border-strong: rgba(0,0,0,0.14); +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +html, body, #app { + height: 100%; + width: 100%; + overflow: hidden; + font-family: var(--df-font-sans); + font-size: 14px; + font-weight: 400; + background: var(--df-bg); + color: var(--df-text); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + line-height: 1.5; +} + +/* — Scrollbar — */ +::-webkit-scrollbar { width: 5px; height: 5px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: var(--df-radius-xs); } +::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.14); } + +/* — Selection — */ +::selection { background: var(--df-accent-soft); color: var(--df-accent); } + +/* — Animations — */ +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes pulseGlow { + 0%, 100% { box-shadow: 0 0 0 0 var(--df-accent-soft); } + 50% { box-shadow: 0 0 12px 2px var(--df-accent-soft); } +} + +/* ═══════════════════════════════════════════════════════════ + 公共组件 — 各功能页复用,避免 scoped 重复定义 + class 名与各页 template 保持一致,零侵入 + ═══════════════════════════════════════════════════════════ */ + +/* — Page Header(功能内页统一:24px 标题 + 紧凑下间距)— */ +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--df-gap-page); +} +.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); } +.header-actions { display: flex; gap: 10px; } + +/* — Button — */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 16px; + line-height: 1; + border: none; + border-radius: var(--df-radius-sm); + font-family: var(--df-font-sans); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s var(--df-ease); + white-space: nowrap; +} +.btn-primary { background: var(--df-accent); color: #fff; } +.btn-primary:hover { background: var(--df-accent-hover); } +.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } +.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); } +.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); } +.btn-accent { background: var(--df-success-bg); color: var(--df-success); border: 0.5px solid rgba(61,219,160,0.3); } +.btn-accent:hover { background: rgba(61,219,160,0.22); } +.btn-sm { padding: 4px 12px; font-size: 12px; } +.btn-link { background: none; border: none; color: var(--df-accent); font-size: 12px; cursor: pointer; padding: 2px 6px; } +.btn-link:hover { text-decoration: underline; } +.btn-link-danger { color: var(--df-danger); } + +/* — Modal — */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} +.modal-box { + background: var(--df-bg-card); + border: 0.5px solid var(--df-border); + border-radius: var(--df-radius-lg); + padding: 24px; + min-width: 360px; + max-width: 90vw; +} +.modal-title { font-size: 18px; font-weight: 500; color: var(--df-text); margin-bottom: 16px; } +.modal-field { margin-bottom: 14px; } +.modal-field label { display: block; font-size: 12px; color: var(--df-text-dim); margin-bottom: 4px; } +.modal-field input, +.modal-field textarea, +.modal-field select { + width: 100%; + padding: 8px 10px; + border-radius: var(--df-radius-sm); + border: 0.5px solid var(--df-border); + background: var(--df-bg); + color: var(--df-text); + font-size: 13px; + font-family: inherit; + box-sizing: border-box; +} +.modal-field input:focus, +.modal-field textarea:focus, +.modal-field select:focus { outline: none; border-color: var(--df-accent); } +.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 4px; } + +/* — Empty State — */ +.empty-state { text-align: center; padding: 60px 20px; font-size: 14px; color: var(--df-text-dim); } +.empty-hint { font-size: 13px; color: var(--df-text-dim); padding: 12px 0; } diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..901b57b --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,52 @@ +/** + * 统一时间戳解析与格式化 — 根治前端时间显示 NaN 问题 + * + * 后端 (df-storage) 统一以「毫秒数字字符串」存储 created_at / updated_at + * (见 src-tauri now_millis:`SystemTime::now().as_millis().to_string()`)。 + * + * 陷阱:`new Date("1718000000000")` 返回 Invalid Date —— JS 规范只对 number 类型 + * 按时间戳解析,纯数字字符串走日期字符串解析路径会失败 → getTime() = NaN + * → `"{n} 天前".format(NaN)` = "NaN 天前"。 + * + * 正确做法:先转 number 再 new Date。本模块统一处理毫秒字符串 / 秒级 / ISO / number / 空。 + */ + +/** 解析任意时间值为毫秒时间戳;无效或空返回 null */ +export function parseTs(v: string | number | null | undefined): number | null { + if (v == null) return null + if (typeof v === 'number') { + if (!Number.isFinite(v)) return null + return v > 1e12 ? v : v * 1000 // 秒级 → 毫秒 + } + const s = String(v).trim() + if (!s) return null + // 纯数字字符串:> 1e12 视为毫秒,否则秒 + if (/^\d+$/.test(s)) { + const n = Number(s) + return n > 1e12 ? n : n * 1000 + } + // ISO / 其他日期字符串 + const ms = new Date(s).getTime() + return isNaN(ms) ? null : ms +} + +/** 绝对日期 'YYYY/MM/DD HH:mm';无效返回 '—' */ +export function formatDate(v: string | number | null | undefined): string { + const ms = parseTs(v) + if (ms == null) return '—' + const d = new Date(ms) + return d.toLocaleDateString('zh-CN') + ' ' + d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) +} + +/** 中文相对时间 '刚刚 / X 分钟前 / X 小时前 / X 天前';无效返回 '—' */ +export function formatRelativeZh(v: string | number | null | undefined): string { + const ms = parseTs(v) + if (ms == null) return '—' + const diffMin = Math.floor((Date.now() - ms) / 60000) + if (diffMin < 1) return '刚刚' + if (diffMin < 60) return `${diffMin} 分钟前` + const diffH = Math.floor(diffMin / 60) + if (diffH < 24) return `${diffH} 小时前` + const diffD = Math.floor(diffH / 24) + return `${diffD} 天前` +} diff --git a/src/views/AiDetached.vue b/src/views/AiDetached.vue new file mode 100644 index 0000000..dd4fed4 --- /dev/null +++ b/src/views/AiDetached.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/views/AiHome.vue b/src/views/AiHome.vue new file mode 100644 index 0000000..76dd55b --- /dev/null +++ b/src/views/AiHome.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue new file mode 100644 index 0000000..75758a3 --- /dev/null +++ b/src/views/Dashboard.vue @@ -0,0 +1,609 @@ + + + + + diff --git a/src/views/Ideas.vue b/src/views/Ideas.vue new file mode 100644 index 0000000..c1687e5 --- /dev/null +++ b/src/views/Ideas.vue @@ -0,0 +1,930 @@ + + + + + diff --git a/src/views/Knowledge.vue b/src/views/Knowledge.vue new file mode 100644 index 0000000..c73c3c7 --- /dev/null +++ b/src/views/Knowledge.vue @@ -0,0 +1,338 @@ + + + + + \ No newline at end of file diff --git a/src/views/ProjectDetail.vue b/src/views/ProjectDetail.vue new file mode 100644 index 0000000..c05525c --- /dev/null +++ b/src/views/ProjectDetail.vue @@ -0,0 +1,667 @@ + + + + + + + diff --git a/src/views/Projects.vue b/src/views/Projects.vue new file mode 100644 index 0000000..7f8ef8c --- /dev/null +++ b/src/views/Projects.vue @@ -0,0 +1,289 @@ + + + + + diff --git a/src/views/Settings.vue b/src/views/Settings.vue new file mode 100644 index 0000000..867d7ce --- /dev/null +++ b/src/views/Settings.vue @@ -0,0 +1,696 @@ + + + + + diff --git a/src/views/Tasks.vue b/src/views/Tasks.vue new file mode 100644 index 0000000..3819aa5 --- /dev/null +++ b/src/views/Tasks.vue @@ -0,0 +1,388 @@ + + + + + diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..fc81239 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ea92286 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8e5b203 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..6513dd2 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +const host = process.env.TAURI_DEV_HOST; + +export default defineConfig(async () => ({ + plugins: [vue()], + clearScreen: false, + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + ignored: ["**/src-tauri/**"], + }, + }, +}));