新增: 初始化 DevFlow 项目仓库

Tauri 2 + Vue 3 + Vite 6 桌面应用,Rust workspace 含 13 个 crate
(df-ai / df-storage / df-workflow / df-core / df-execute 等)。
核心能力:AI 聊天 agentic 循环(工具调用+人工审批)、工作流引擎、
任务/想法/项目/阶段管理、可追溯性,及配套前端组件。
This commit is contained in:
2026-06-12 01:31:05 +08:00
commit 98393b4908
178 changed files with 27859 additions and 0 deletions

49
.gitignore vendored Normal file
View File

@@ -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

542
ARCHITECTURE.md Normal file
View File

@@ -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<String>,// 考虑过的方案
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 输出全部流式推送到前端

5797
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
Cargo.toml Normal file
View File

@@ -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"

397
PROGRESS.md Normal file
View File

@@ -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 真实可用(接 Shellhuman_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 但**无 actionView 未引用** |
| 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 317 commands |
| 3 | **Store 未接入 View** — 两套独立硬编码数据 | 数据流断裂 | P0 | ✅ Sprint 4 |
| 4 | **AI Provider 无实现** — trait 定义完整但无 HTTP client | AI 功能全部不可用 | P1 | ✅ Sprint 5 |
| 5 | **同层节点未并行** — DagExecutor 有 TODO 注释 | 工作流执行效率 | P1 | ✅ Sprint 3join_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 45 页面接 Store + 创建/运行交互 |
| 验证3 节点简单工作流 | ⬜ 待验证 | 后端就绪,需端到端跑通 |
| df-storage CRUD 层 | ✅ 完成 | Sprint 26 Repo + impl_repo! 宏Sprint 3 增 BranchRepo |
| 对抗式评估系统 | ✅ 完成 | Sprint 6AdversarialEngine + 正反方辩论 + AI 分析师 |
| Migrations V2关联字段 + branches 表) | ✅ 完成 | Sprint 3 |
| Executor 同层并行 + 状态转换校验 | ✅ 完成 | Sprint 34 单测全绿 |
| Tauri IPC 命令 | ✅ 完成 | Sprint 317 commands + 事件转发 |
| Store 接入 View | ✅ 完成 | Sprint 4API 层 + 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-aiconfig 驱动 providerbase_url/api_key/model/prompt→ OpenAICompatProvider.complete → 输出 text/model/usage注册 "ai" 节点;未端到端实测 |
| **AI Chat LLM 优化** | 🚧 P0 完成 | 任务 #43P0 可靠性(超时/断连/停止生成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 IPCAppState + 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 改返回 Result4 个单测全绿(含并行耗时断言 <180ms、失败中止下游层
- **Tauri IPC**src-tauriAppStatedb + 6 Repo + EventBus + NodeRegistry+ 17 个 commandproject/task/idea CRUD + run_workflow/list/get executionsrun_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 clientSSE 流式解析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 调用可靠性 P0df-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<AtomicBool>` + 新 `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 轮 reviewai.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 正常输出时停止立即生效(<1sidle无输出时最多等 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.mdskills + 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 空壳,单独立项
<!--
### [Sprint N] YYYY-MM-DD — 简述
**工作内容**
- 具体做了什么
**代码变更**
- 修改了哪些文件/Crate
**遗留问题**
- 1. ...
**下一步**
- 1. ...
-->
---
## 七、开发约定
### 构建命令
```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` |

301
bun.lock Normal file
View File

@@ -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=="],
}
}

18
crates/df-ai/Cargo.toml Normal file
View File

@@ -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"

View File

@@ -0,0 +1,166 @@
//! AI 工具注册 — 将现有 CRUD 操作注册为 LLM 可调用的 Tool
//!
//! 风险分级:
//! - Low: 只读操作list/getAI 自动执行
//! - 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<dyn Fn(Value) -> Pin<Box<dyn Future<Output = anyhow::Result<Value>> + Send>> + Send + Sync>;
// ============================================================
// 工具注册表
// ============================================================
/// AI 工具注册表
pub struct AiToolRegistry {
tools: HashMap<String, AiTool>,
}
impl AiToolRegistry {
/// 创建空注册表
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
/// 注册一个工具
pub fn register(
&mut self,
name: impl Into<String>,
description: impl Into<String>,
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<ToolDefinition> {
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<String> {
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,
})
}

View File

@@ -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<serde_json::Value>,
max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<AnthropicToolDef>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<serde_json::Value>,
}
/// Anthropic 工具定义input_schema 对应 OpenAI 的 parameters
#[derive(Debug, Serialize)]
struct AnthropicToolDef {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
input_schema: serde_json::Value,
}
/// Anthropic 同步响应
#[derive(Debug, Deserialize)]
struct AnthropicResponse {
#[allow(dead_code)]
id: String,
model: String,
content: Vec<AnthropicContentBlock>,
#[allow(dead_code)]
stop_reason: Option<String>,
usage: AnthropicUsage,
}
/// 响应 content 块text 或 tool_use
#[derive(Debug, Deserialize)]
struct AnthropicContentBlock {
#[serde(rename = "type")]
block_type: String,
#[serde(default)]
text: Option<String>,
/// tool_use 块字段
id: Option<String>,
name: Option<String>,
input: Option<serde_json::Value>,
}
#[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<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> 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_resultrole=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<String> = {
let sys: Vec<String> = 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<serde_json::Value> = Vec::new();
let mut pending_tool_results: Vec<serde_json::Value> = 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<serde_json::Value> = 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<serde_json::Value>,
pending: &mut Vec<serde_json::Value>,
) {
if pending.is_empty() {
return;
}
let blocks: Vec<serde_json::Value> = 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<CompletionResponse> {
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<ToolCall> = 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<StreamResult> {
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,
}
}
}

View File

@@ -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<ChatMessage>,
/// 配置
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<ChatMessage> {
&self.messages
}
/// 清空上下文
pub fn clear(&mut self) {
self.messages.clear();
}
}

View File

@@ -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<String> {
tracing::info!("AgentCoordinator: 协调任务开始");
// TODO: 实现多 Agent 协作
Ok("TODO: Agent 协作结果".to_string())
}
}
impl Default for AgentCoordinator {
fn default() -> Self {
Self::new()
}
}

10
crates/df-ai/src/lib.rs Normal file
View File

@@ -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;

View File

@@ -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<OpenAiMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,
stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<serde_json::Value>,
}
/// OpenAI 消息格式
#[derive(Debug, Serialize, Deserialize)]
struct OpenAiMessage {
role: String,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_calls: Option<Vec<serde_json::Value>>,
}
/// OpenAI 同步响应
#[derive(Debug, Deserialize)]
struct OpenAiResponse {
choices: Vec<OpenAiChoice>,
model: String,
usage: Option<OpenAiUsage>,
}
#[derive(Debug, Deserialize)]
struct OpenAiChoice {
message: OpenAiMessageResp,
finish_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OpenAiMessageResp {
content: Option<String>,
tool_calls: Option<Vec<OpenAiToolCallResp>>,
}
#[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<OpenAiStreamChoice>,
}
#[derive(Debug, Deserialize)]
struct OpenAiStreamChoice {
delta: OpenAiStreamDelta,
finish_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OpenAiStreamDelta {
content: Option<String>,
tool_calls: Option<Vec<OpenAiStreamToolCall>>,
}
#[derive(Debug, Deserialize)]
struct OpenAiStreamToolCall {
index: u32,
id: Option<String>,
function: Option<OpenAiStreamFunction>,
}
#[derive(Debug, Deserialize)]
struct OpenAiStreamFunction {
name: Option<String>,
arguments: Option<String>,
}
// ============================================================
// 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<String>, api_key: impl Into<String>, default_model: impl Into<String>) -> 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/completionsOpenAI 约定)
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<OpenAiMessage> = 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<OpenAiToolCallResp>) -> Vec<ToolCall> {
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<CompletionResponse> {
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<StreamResult> {
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::<OpenAiStreamChunk>(&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,
}
}
}

View File

@@ -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<ChatMessage>,
/// 温度0.0 ~ 2.0
pub temperature: Option<f32>,
/// 最大生成 token 数
pub max_tokens: Option<u32>,
/// 是否流式输出
pub stream: bool,
/// 可调用的工具定义
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ToolDefinition>>,
/// 工具调用策略: "auto" | "none" | {"type":"function","name":"xxx"}
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
}
/// 聊天消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: MessageRole,
pub content: String,
/// 工具调用 IDrole=Tool 时必填)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
/// AI 发起的工具调用列表role=Assistant 时可能有)
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self {
Self { role: MessageRole::System, content: content.into(), tool_call_id: None, tool_calls: None }
}
pub fn user(content: impl Into<String>) -> Self {
Self { role: MessageRole::User, content: content.into(), tool_call_id: None, tool_calls: None }
}
pub fn assistant(content: impl Into<String>) -> Self {
Self { role: MessageRole::Assistant, content: content.into(), tool_call_id: None, tool_calls: None }
}
pub fn assistant_with_tools(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> 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<String>, content: impl Into<String>) -> 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<String>, description: impl Into<String>, 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<String>, name: impl Into<String>, arguments: impl Into<String>) -> 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<Vec<ToolCall>>,
}
/// 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<Vec<ToolCallDelta>>,
}
/// 工具调用增量(流式中的片段)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallDelta {
/// 索引
pub index: u32,
/// 工具调用 ID仅第一个 chunk 有)
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// 函数名片段
#[serde(skip_serializing_if = "Option::is_none")]
pub function_name: Option<String>,
/// 函数参数片段
#[serde(skip_serializing_if = "Option::is_none")]
pub function_arguments: Option<String>,
}
/// 异步流类型别名
pub type StreamResult = Pin<Box<dyn Stream<Item = anyhow::Result<StreamChunk>> + Send>>;
/// LLM Provider trait
#[async_trait]
pub trait LlmProvider: Send + Sync {
/// 同步调用
async fn complete(&self, request: CompletionRequest) -> anyhow::Result<CompletionResponse>;
/// 流式调用(返回异步流)
async fn stream(
&self,
request: CompletionRequest,
) -> anyhow::Result<StreamResult>;
/// Provider 名称
fn name(&self) -> &str;
/// 支持的特性
fn supported_features(&self) -> ProviderFeatures;
}

View File

@@ -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(),
}
}
}

View File

@@ -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()
}
}

11
crates/df-core/Cargo.toml Normal file
View File

@@ -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 }

View File

@@ -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<T> = std::result::Result<T, Error>;

View File

@@ -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<String>,
}
/// 工作流事件
#[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<String>,
},
/// 人工审批响应
HumanApprovalResponse {
execution_id: String,
node_id: NodeId,
decision: String,
comment: Option<String>,
},
}

View File

@@ -0,0 +1,5 @@
//! df-core: 核心类型定义,所有 crate 的基础依赖
pub mod error;
pub mod events;
pub mod types;

301
crates/df-core/src/types.rs Normal file
View File

@@ -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()
}

View File

@@ -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 }

View File

@@ -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<Knowledge> {
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<Knowledge> {
// TODO: 语义搜索知识库
KnowledgeStore::search(_context, _kind, 5)
}
/// 验证知识的有效性(定期执行)
///
/// 检查知识是否仍然适用(依赖版本是否过时、规则是否仍有意义等)
pub async fn validate_knowledge(&self) -> Vec<String> {
// TODO: 遍历知识库,标记过时的知识
vec![]
}
}

View File

@@ -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<String>,
/// 来源项目
pub source_project: Option<ProjectId>,
/// 来源实体(如某次审查、某个 Bug 修复)
pub source_ref: Option<String>,
/// 被复用次数
pub reuse_count: usize,
/// 效果评分 (0-100由用户或 AI 评估)
pub effectiveness: Option<f32>,
/// 是否已验证有效
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<Knowledge> {
// TODO: SQLite 全文搜索或向量搜索
vec![]
}
/// 获取最常用的知识
pub fn top_used(_limit: usize) -> Vec<Knowledge> {
// TODO: 按 reuse_count 降序
vec![]
}
/// 保存知识条目
pub fn save(_knowledge: &Knowledge) -> anyhow::Result<()> {
// TODO: SQLite INSERT/UPDATE
Ok(())
}
}

View File

@@ -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};

View File

@@ -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<Knowledge> {
// TODO:
// 1. 分析 findings 的共性
// 2. 如果出现次数 >= threshold生成规则
// 3. 去重(与已有规则比较)
None
}
/// 从 Bug 修复过程中提取诊断知识
pub fn extract_diagnosis(
_bug_description: &str,
_root_cause: &str,
_fix_description: &str,
) -> Option<Knowledge> {
// TODO: AI 总结为可复用的诊断知识
None
}
/// 从成功的 Prompt 中提取模板
pub fn extract_prompt_template(
_prompt: &str,
_result_quality: f32,
) -> Option<Knowledge> {
// TODO: 如果 result_quality > 0.8,提取为模板
None
}
}

View File

@@ -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<TemplateVariable>,
/// 适用场景
pub applicable_scenarios: Vec<String>,
/// 效果评分
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<String>,
pub required: bool,
}
impl PromptTemplate {
/// 渲染模板(替换变量)
pub fn render(&self, vars: &std::collections::HashMap<String, String>) -> String {
let mut result = self.template.clone();
for (key, value) in vars {
result = result.replace(&format!("{{{}}}", key), value);
}
result
}
}

View File

@@ -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<String>,
/// 检查方式(正则/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,
}

View File

@@ -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 }

View File

@@ -0,0 +1,46 @@
//! Docker 执行器 — 在容器中运行任务
use serde::{Deserialize, Serialize};
/// Docker 容器执行请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerRequest {
/// 镜像名称
pub image: String,
/// 容器内执行的命令
pub command: Option<String>,
/// 环境变量
pub env: std::collections::HashMap<String, String>,
/// 挂载卷
pub volumes: Vec<VolumeMount>,
/// 是否在执行后自动删除容器
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<i32>,
}
/// 在 Docker 容器中执行命令
///
/// TODO: 实现 Docker API 调用或 CLI 包装
pub async fn execute(_request: DockerRequest) -> anyhow::Result<DockerResult> {
tracing::info!("Docker 执行: TODO");
Ok(DockerResult {
stdout: String::new(),
stderr: String::new(),
exit_code: None,
})
}

View File

@@ -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<String>,
/// 提交消息
pub message: Option<String>,
}
/// 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<GitResult> {
tracing::info!("Git 操作: TODO");
Ok(GitResult {
success: true,
message: "TODO: 未实现".to_string(),
})
}

View File

@@ -0,0 +1,6 @@
//! df-execute: 执行运行时 — Shell、Docker、SSH、Git 操作
pub mod docker;
pub mod git_ops;
pub mod shell;
pub mod ssh;

View File

@@ -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<i32>,
/// 执行耗时(毫秒)
pub duration_ms: u64,
}
/// Shell 命令执行请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShellRequest {
/// 要执行的命令
pub command: String,
/// 工作目录
pub working_dir: Option<String>,
/// 环境变量
pub env: std::collections::HashMap<String, String>,
/// 超时时间None 表示不超时
pub timeout_secs: Option<u64>,
}
/// 执行 Shell 命令
///
/// TODO: 完整实现,支持超时、环境变量、工作目录等
pub async fn execute(request: ShellRequest) -> anyhow::Result<ShellResult> {
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,
})
}

View File

@@ -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<u64>,
}
/// SSH 执行结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SshResult {
pub stdout: String,
pub stderr: String,
pub exit_code: Option<i32>,
}
/// 通过 SSH 执行远程命令
///
/// TODO: 实现SSH连接可用 ssh2 crate 或包装 ssh 命令)
pub async fn execute(_request: SshRequest) -> anyhow::Result<SshResult> {
tracing::info!("SSH 执行: TODO");
Ok(SshResult {
stdout: String::new(),
stderr: String::new(),
exit_code: None,
})
}

View File

@@ -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 }

View File

@@ -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<String>, // 证据支持
pub reasoning: Vec<String>, // 推理过程
pub confidence: f64, // 置信度 0-1
}
/// 反方论点
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CounterArgument {
pub thesis: String, // 反对观点
pub evidence: Vec<String>, // 反对证据
pub reasoning: Vec<String>, // 反驳推理
pub confidence: f64, // 置信度 0-1
}
/// AI 分析师综合分析
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalystAnalysis {
pub summary: String, // 综合总结
pub strengths: Vec<String>, // 主要优势
pub weaknesses: Vec<String>, // 主要劣势
pub risks: Vec<String>, // 潜在风险
pub opportunities: Vec<String>, // 机会点
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<AdversarialEval> {
// 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<Argument> {
// 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<CounterArgument> {
// 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<AnalystAnalysis> {
// 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<String>,
pub action_items: Vec<String>,
}
impl From<AdversarialEval> 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::<Vec<_>>().join(" "),
positive_strength: eval.positive.confidence,
negative_strength: eval.negative.confidence,
net_sentiment,
assessment_level: format!("{:?}", eval.analyst.final_assessment),
key_takeaways,
action_items,
}
}
}

View File

@@ -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<String>,
/// 来源(如 "用户输入"、"AI 生成"、"会议记录"
pub source: Option<String>,
}
/// 想法实体
#[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<IdeaScores>,
/// 标签
pub tags: Vec<String>,
/// 来源
pub source: Option<String>,
/// 关联的想法 ID
pub related_ids: Vec<IdeaId>,
/// 创建时间
pub created_at: chrono::DateTime<chrono::Utc>,
/// 更新时间
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// 想法评分详情
#[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,
})
}
}

View File

@@ -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<String>,
}
/// 评估建议
#[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<AdversarialEval> {
AdversarialEngine::evaluate(idea).await
}
/// 评估一个想法 - 保持向后兼容
pub fn evaluate(idea: &Idea) -> Result<EvalResult> {
// 使用简单评分作为后备
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(),
})
}
}

View File

@@ -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<IdeaId, Vec<Relation>>,
}
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()
}
}

View File

@@ -0,0 +1,8 @@
//! df-ideas: 想法池 — 捕获、评估、评分、关联图、晋升
pub mod adversarial;
pub mod capture;
pub mod evaluator;
pub mod graph;
pub mod promotion;
pub mod scoring;

View File

@@ -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<PromotionResult> {
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<PromotionResult> {
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(),
})
}
}

View File

@@ -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
}
}

View File

@@ -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 }

View File

@@ -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 缺少必填参数: promptconfig 或上游输入均无)"))?;
// ── 可选参数 ──
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可设 anthropicGLM 订阅端点 / 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<dyn LlmProvider> = 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默认或 anthropicGLM订阅/Claude官方" },
"base_url": { "type": "string", "description": "API 地址OpenAI 兼容如 https://api.deepseek.comAnthropic 如 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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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::<Vec<_>>())
.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"
}
}

View File

@@ -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;

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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 }

View File

@@ -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<PluginId, LoadedPlugin>,
}
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<dyn Node>)> {
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);
}
}

View File

@@ -0,0 +1,5 @@
//! df-plugin: 插件系统 — 插件加载、宿主环境、SDK
pub mod host;
pub mod loader;
pub mod sdk;

View File

@@ -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<String>,
/// 入口文件路径
pub entry_path: PathBuf,
}
/// 已加载的插件
pub struct LoadedPlugin {
pub metadata: PluginMetadata,
pub nodes: Vec<Box<dyn Node>>,
}
/// 插件加载器
pub struct PluginLoader {
/// 插件搜索目录
search_dirs: Vec<PathBuf>,
}
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<Vec<PluginMetadata>> {
tracing::info!("扫描插件目录: {:?}", self.search_dirs);
// TODO: 实现插件发现逻辑
Ok(Vec::new())
}
/// 加载指定插件
///
/// TODO: 实现动态库加载dlopen/LoadLibrary或 WASM 加载
pub fn load(&self, _metadata: &PluginMetadata) -> anyhow::Result<LoadedPlugin> {
// TODO: 实现插件加载
tracing::warn!("插件加载尚未实现");
Err(anyhow::anyhow!("插件加载尚未实现"))
}
/// 批量加载所有发现的插件
pub fn load_all(&self) -> anyhow::Result<HashMap<PluginId, LoadedPlugin>> {
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()
}
}

View File

@@ -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<Box<dyn Node>>;
/// 插件初始化(可选)
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<Box<dyn Node>>,
}
impl PluginBuilder {
/// 创建插件构建器
pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
version: version.into(),
nodes: Vec::new(),
}
}
/// 注册节点
pub fn node(mut self, node: Box<dyn Node>) -> 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<Box<dyn Node>> {
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: 实现宏
};
}

View File

@@ -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 }

View File

@@ -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<String>,
/// Git 仓库 URL
pub repo_url: Option<String>,
/// 当前分支
pub current_branch: Option<String>,
/// 环境变量
pub env_vars: std::collections::HashMap<String, String>,
/// 技术栈
pub tech_stack: Vec<String>,
/// AI 上下文(项目相关的 AI 记忆)
pub ai_context: Option<AiContext>,
}
/// AI 上下文信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiContext {
/// 项目摘要
pub summary: String,
/// 架构描述
pub architecture: Option<String>,
/// 关键决策记录
pub decisions: Vec<String>,
/// 最近修改摘要
pub recent_changes: Vec<String>,
}
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,
}
}
}

View File

@@ -0,0 +1,6 @@
//! df-project: 项目管理 — 项目创建、调度、上下文、时间线
pub mod context;
pub mod manager;
pub mod scheduler;
pub mod timeline;

View File

@@ -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<IdeaId>,
/// 优先级
pub priority: Priority,
/// 标签
pub tags: Vec<String>,
/// 创建时间
pub created_at: chrono::DateTime<chrono::Utc>,
/// 更新时间
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// 创建项目的输入
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateProjectInput {
pub name: String,
pub description: String,
pub idea_id: Option<IdeaId>,
#[serde(default)]
pub priority: Priority,
#[serde(default)]
pub tags: Vec<String>,
}
/// 项目管理器
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(())
}
}

View File

@@ -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<TaskId>,
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<SchedulingDecision> {
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,
})
}
}

View File

@@ -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<chrono::DateTime<chrono::Utc>>,
/// 实际完成时间
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
/// 进度百分比 (0-100)
pub progress: u8,
/// 是否已完成
pub completed: bool,
}
/// 项目时间线
pub struct Timeline {
pub project_id: ProjectId,
pub milestones: Vec<Milestone>,
}
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)
}
}

View File

@@ -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 }

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -0,0 +1,7 @@
//! df-stages: 阶段插件 — 开发流程各阶段的节点模板注册
pub mod coding;
pub mod idea;
pub mod release;
pub mod requirement;
pub mod testing;

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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 }

View File

@@ -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<Mutex<Connection>>,
}
impl $name {
pub fn new(db: &Database) -> Self {
Self { conn: db.conn() }
}
pub async fn insert(&self, record: $record) -> Result<String> {
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<Option<$record>> {
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<Vec<$record>> {
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<Vec<$record>> {
// 白名单校验,防止 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<bool> {
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<bool> {
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<IdeaRecord, rusqlite::Error> {
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<ProjectRecord, rusqlite::Error> {
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<TaskRecord, rusqlite::Error> {
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<ReleaseRecord, rusqlite::Error> {
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<WorkflowRecord, rusqlite::Error> {
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<BranchRecord, rusqlite::Error> {
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<NodeExecutionRecord, rusqlite::Error> {
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<AiProviderRecord, rusqlite::Error> {
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<AiConversationRecord, rusqlite::Error> {
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<AiToolExecutionRecord, rusqlite::Error> {
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<bool> {
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()))?
}
}

View File

@@ -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<Mutex<Connection>>,
}
impl Database {
/// 打开(或创建)数据库文件
pub async fn open(path: &Path) -> Result<Self> {
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<Self> {
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<Mutex<Connection>> {
Arc::clone(&self.conn)
}
}

View File

@@ -0,0 +1,6 @@
//! df-storage: 存储层 — SQLite 数据库、模型定义、迁移
pub mod crud;
pub mod db;
pub mod migrations;
pub mod models;

View File

@@ -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);
";

View File

@@ -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<f64>,
pub tags: Option<String>, // JSON 数组
pub source: Option<String>,
pub promoted_to: Option<String>, // 晋升后的 project_id
pub ai_analysis: Option<String>, // AI 分析结果 JSON
pub scores: Option<String>, // 多维评分 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<String>,
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<String>,
pub assignee: Option<String>,
pub workflow_def_id: Option<String>, // 关联的工作流定义 ID
pub base_branch: Option<String>, // 基础分支
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<String>,
pub name: String,
pub base: String,
pub status: String,
pub created_at: String,
pub updated_at: String,
pub merged_at: Option<String>,
}
/// 发布记录
#[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<String>,
pub created_at: String,
pub released_at: Option<String>,
}
// ============================================================
// 工作流相关模型
// ============================================================
/// 工作流执行记录
#[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<String>,
pub project_id: Option<String>, // 关联项目 ID
pub task_id: Option<String>, // 关联任务 ID
pub created_at: String,
pub completed_at: Option<String>,
}
/// 节点执行记录
#[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<String>,
pub output_json: Option<String>,
pub error_message: Option<String>,
pub started_at: Option<String>,
pub completed_at: Option<String>,
}
// ============================================================
// 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<String>, // JSON array of model names
pub is_default: bool,
pub config: Option<String>, // 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<String>,
pub messages: String, // JSON array of ChatMessage
pub provider_id: Option<String>,
pub model: Option<String>,
pub created_at: String,
pub updated_at: String,
}
/// AI 工具执行审计记录
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiToolExecutionRecord {
pub id: String,
pub conversation_id: Option<String>,
pub tool_call_id: String,
pub tool_name: String,
pub arguments: String,
pub result: Option<String>,
pub status: String, // pending/approved/rejected/executing/completed/failed
pub risk_level: String, // low/medium/high
pub requested_at: String,
pub executed_at: Option<String>,
pub decided_by: Option<String>, // human/auto
}

13
crates/df-task/Cargo.toml Normal file
View File

@@ -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 }

View File

@@ -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<TaskId>,
/// 基于的分支(通常是 main
pub base_branch: String,
/// 当前状态
pub status: BranchStatus,
/// 创建时间
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// 分支管理器
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<Branch> {
// TODO: 实现
Vec::new()
}
}

View File

@@ -0,0 +1,6 @@
//! df-task: 任务/分支管理 — 任务 CRUD、分支管理、合并协调、发布计划
pub mod branch;
pub mod merge;
pub mod release;
pub mod task;

View File

@@ -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<TaskId>,
pub status: MergeStatus,
pub conflicts: Vec<Conflict>,
}
/// 冲突信息
#[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<TaskId>,
) -> 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<Conflict> {
// TODO: 实现冲突检测
Vec::new()
}
/// AI 辅助解决冲突
///
/// TODO: 调用 df-ai 的 LLM 分析冲突并提供解决方案
pub fn ai_resolve_conflict(_conflict: &Conflict) -> anyhow::Result<String> {
// TODO: 实现 AI 辅助冲突解决
tracing::warn!("AI 冲突解决尚未实现");
Ok(String::new())
}
}

View File

@@ -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<String>,
}
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<TaskId>,
/// 变更日志
pub changelog: Option<String>,
/// 创建时间
pub created_at: chrono::DateTime<chrono::Utc>,
/// 发布时间
pub released_at: Option<chrono::DateTime<chrono::Utc>>,
}
/// 版本号递增类型
#[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<TaskId>,
) -> 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,
},
}
}
}

View File

@@ -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<String>,
/// 关联分支 ID
pub branch_id: Option<BranchId>,
/// 指派人
pub assignee: Option<String>,
/// 标签
pub tags: Vec<String>,
/// 预估工时(小时)
pub estimate_hours: Option<f64>,
/// 实际工时(小时)
pub actual_hours: Option<f64>,
/// 创建时间
pub created_at: chrono::DateTime<chrono::Utc>,
/// 更新时间
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// 创建任务的输入
#[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<String>,
pub tags: Vec<String>,
pub estimate_hours: Option<f64>,
}
/// 任务管理器
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(())
}
}

View File

@@ -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 }

View File

@@ -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<String>,
/// 状态
pub status: AnnotationStatus,
/// 处理者
pub resolved_by: Option<String>,
/// 处理结果
pub resolution: Option<String>,
pub created_at: i64,
pub resolved_at: Option<i64>,
}
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<Annotation> {
// TODO: 从 SQLite 查询所有 status = Open 的标注
vec![]
}
/// 按类型分组
pub fn group_by_marker(annotations: &[Annotation]) -> std::collections::HashMap<AnnotationMarker, Vec<&Annotation>> {
let mut groups: std::collections::HashMap<AnnotationMarker, Vec<&Annotation>> = 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<Vec<Annotation>> {
// TODO: 调用 df-ai 编排器,构建批量处理 prompt
// 1. 按类型分组
// 2. 每组构建一个 AI 请求
// 3. AI 返回处理结果
// 4. 更新标注状态和 resolution
Ok(vec![])
}
}

View File

@@ -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<String>,
/// 最终决定
pub decision: String,
/// 决策原因
pub reason: String,
/// 决策者
pub decided_by: DecidedBy,
/// 关联实体类型
pub entity_type: Option<String>,
/// 关联实体 ID
pub entity_id: Option<String>,
/// 影响范围
pub impact: Option<String>,
/// 所处阶段
pub stage: Option<String>,
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<String>,
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<Decision> {
// 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<Decision> {
// TODO: 查询并按 created_at 排序
vec![]
}
}

View File

@@ -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;

View File

@@ -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<TraceLink> {
// 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<String> {
// TODO: 找出没有关联测试用例的功能
vec![]
}
}

View File

@@ -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 }

View File

@@ -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<bool> {
// 骨架:简单的 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)
}
}

View File

@@ -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<String>,
}
/// DAG 结构
pub struct Dag {
/// 节点集合
pub nodes: HashMap<NodeId, Box<dyn Node>>,
/// 边集合
pub edges: Vec<Edge>,
}
impl Dag {
/// 创建空 DAG
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
}
}
/// 添加节点
pub fn add_node(&mut self, id: NodeId, node: Box<dyn Node>) {
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<NodeId> {
self.edges
.iter()
.filter(|e| &e.target == node_id)
.map(|e| e.source.clone())
.collect()
}
/// 获取指定节点的所有下游节点 ID
pub fn successors(&self, node_id: &NodeId) -> Vec<NodeId> {
self.edges
.iter()
.filter(|e| &e.source == node_id)
.map(|e| e.target.clone())
.collect()
}
/// 拓扑排序 — 返回按执行顺序排列的节点 ID 层级
///
/// 返回 Vec<Vec<NodeId>>,每层内的节点可以并行执行
pub fn topological_layers(&self) -> Result<Vec<Vec<NodeId>>> {
let node_ids: HashSet<NodeId> = self.nodes.keys().cloned().collect();
let mut in_degree: HashMap<NodeId, usize> = 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<NodeId> = in_degree
.iter()
.filter(|(_, &deg)| deg == 0)
.map(|(id, _)| id.clone())
.collect();
let mut processed = 0usize;
while !queue.is_empty() {
let mut layer: Vec<NodeId> = 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()
}
}

View File

@@ -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<String, NodeDef>,
pub edges: Vec<EdgeDef>,
}
/// 节点定义
#[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<String>,
}
/// 边定义
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EdgeDef {
pub source: String,
pub target: String,
#[serde(default)]
pub condition: Option<String>,
}
impl DagDef {
/// 创建空的 DAG 定义
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
}
}
/// 添加节点定义
pub fn add_node(&mut self, id: impl Into<String>, node_type: impl Into<String>, 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<String>, target: impl Into<String>) {
self.edges.push(EdgeDef {
source: source.into(),
target: target.into(),
condition: None,
});
}
/// 添加带条件的边定义
pub fn add_edge_with_condition(
&mut self,
source: impl Into<String>,
target: impl Into<String>,
condition: impl Into<String>,
) {
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<EdgeDef> = dag.edges.iter().map(|e| EdgeDef {
source: e.source.clone(),
target: e.target.clone(),
condition: e.condition.clone(),
}).collect();
let nodes: HashMap<String, NodeDef> = 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()
}
}

View File

@@ -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<WorkflowEvent>,
}
/// 事件订阅者
pub type EventSubscriber = broadcast::Receiver<WorkflowEvent>;
/// 默认事件通道容量
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<usize, SendError<WorkflowEvent>> {
self.sender.send(event)
}
/// 尝试获取人工审批响应(简化版本,实际需要实现状态存储)
pub fn try_recv_human_approval(&self, _execution_id: &str, _node_id: &str) -> Option<HumanApprovalResponse> {
// 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(),
}
}
}

View File

@@ -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<HashMap<NodeId, NodeOutput>> {
let layers = dag.topological_layers()?;
let mut outputs: HashMap<NodeId, NodeOutput> = 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<anyhow::Error> = 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);
}
}

View File

@@ -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;

View File

@@ -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<String, NodeOutput>,
/// 节点配置参数
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<String, String>,
}
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<NodeOutput>;
/// 节点参数 SchemaJSON 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;
}

View File

@@ -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<dyn Fn(&serde_json::Value) -> Box<dyn Node> + Send + Sync>;
/// 节点注册表 — 根据 node_type 字符串创建节点实例
pub struct NodeRegistry {
factories: HashMap<String, NodeFactory>,
}
impl NodeRegistry {
/// 创建空的注册表
pub fn new() -> Self {
Self {
factories: HashMap::new(),
}
}
/// 注册一个节点工厂
pub fn register<F>(&mut self, type_name: &str, factory: F)
where
F: Fn(&serde_json::Value) -> Box<dyn Node> + 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<Box<dyn Node>> {
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<Dag> {
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
}
}

View File

@@ -0,0 +1,147 @@
//! 节点状态机 — 管理节点的状态转换
//!
//! 合法转换Pending → RunningRunning → Completed / Failed
//! 非法转换返回错误,避免状态被随意覆盖。
use std::collections::HashMap;
use df_core::types::{NodeId, NodeStatus};
/// 节点状态机
#[derive(Debug, Clone)]
pub struct StateMachine {
/// 节点状态映射
states: HashMap<NodeId, NodeStatus>,
}
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(&current, &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<NodeId, NodeStatus> {
&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());
}
}

View File

@@ -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<T>` 接口
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` — 统一错误类型

View File

@@ -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<T, String>` 返回给前端
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

View File

@@ -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` — 当前进度与全局性问题

Some files were not shown because too many files have changed in this diff Show More