新增: 初始化 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:
49
.gitignore
vendored
Normal file
49
.gitignore
vendored
Normal 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
542
ARCHITECTURE.md
Normal 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
5797
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal 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
397
PROGRESS.md
Normal 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 真实可用(接 Shell),human_node 半实现(审批响应 TODO),其余 6 种骨架 |
|
||||
| df-stages | 6 | 287 | 209 | 🔩 全骨架 — 11个阶段节点 Schema 完整,execute() 全空 |
|
||||
| **合计** | **71** | **~4,108** | **~2,625** | |
|
||||
|
||||
### 前端 (9 页面 + Store + i18n)
|
||||
|
||||
| 类别 | 文件数 | 行数 | 状态 |
|
||||
|------|--------|------|------|
|
||||
| Vue 页面 | 9 | ~3,372 | ✅ UI 全部真实 — 无空壳 |
|
||||
| Pinia Store | 4 | ~362 | ⚠️ 有 state/getter 但**无 action,View 未引用** |
|
||||
| Router | 1 | 63 | ✅ 9 条路由含动态路由 |
|
||||
| i18n | 2 | ~94 | ⚠️ 翻译完整但**未接入 main.ts** |
|
||||
| 设计系统 | 1 | 105 | ✅ CSS 变量/动画/Arco 暗色覆盖 |
|
||||
| Tauri Commands | 1 | 13 | 🔩 仅 `greet` 示例,无业务逻辑 |
|
||||
|
||||
**前端关键问题**:
|
||||
- 所有 View 组件数据**硬编码** (`ref([...])`),与 Store 完全独立
|
||||
- 所有按钮事件处理函数为**空函数** `{}`
|
||||
- i18n 插件未注册到 Vue app
|
||||
- Arco Design 组件库未实际使用(仅 CSS 变量覆盖)
|
||||
|
||||
---
|
||||
|
||||
## 三、架构分层与依赖关系
|
||||
|
||||
```
|
||||
真实可用路径(有端到端逻辑):
|
||||
df-core (类型基础)
|
||||
→ df-workflow (DAG 拓扑排序 + 执行器 + 状态机 + EventBus)
|
||||
→ df-execute/shell (tokio::process Shell 执行)
|
||||
→ df-storage (SQLite 连接 + 迁移 + 建表)
|
||||
|
||||
待打通路径(骨架就位,需填充):
|
||||
df-workflow → df-nodes (8种节点 execute() 待实现)
|
||||
df-workflow → df-stages (11种阶段节点 execute() 待实现)
|
||||
df-ai → LlmProvider 实现 (无 HTTP client)
|
||||
df-storage → 所有 crate 的 CRUD 操作
|
||||
Tauri commands → Rust 业务逻辑
|
||||
Vue Views → Pinia Store → Tauri IPC → Rust
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、全局性问题
|
||||
|
||||
| # | 问题 | 影响 | 优先级 | 状态 |
|
||||
|---|------|------|--------|------|
|
||||
| 1 | **无 CRUD 层** — df-storage 只有建表 | 所有 crate 无法持久化数据 | P0 | ✅ Sprint 2 |
|
||||
| 2 | **Tauri IPC 未建** — 只有 greet 示例 command | 前端无法调用后端 | P0 | ✅ Sprint 3(17 commands) |
|
||||
| 3 | **Store 未接入 View** — 两套独立硬编码数据 | 数据流断裂 | P0 | ✅ Sprint 4 |
|
||||
| 4 | **AI Provider 无实现** — trait 定义完整但无 HTTP client | AI 功能全部不可用 | P1 | ✅ Sprint 5 |
|
||||
| 5 | **同层节点未并行** — DagExecutor 有 TODO 注释 | 工作流执行效率 | P1 | ✅ Sprint 3(join_all) |
|
||||
| 6 | **条件表达式引擎** — 仅支持 true/false 字面量 | 条件分支不可用 | P2 | ⬜ 待办 |
|
||||
| 7 | **i18n 未注册** — 翻译文件存在但未挂载 | 多语言不生效 | P2 | ✅ Sprint 7 |
|
||||
| 8 | **未 git commit** — 代码全 untracked | 无版本基线 | P0 | ✅ Sprint 2 |
|
||||
|
||||
---
|
||||
|
||||
## 五、Phase 规划与当前进度
|
||||
|
||||
### Phase 1 — 引擎骨架 ✅ 完成
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| df-core 类型系统 | ✅ 完成 | 错误/事件/状态枚举/ID生成 |
|
||||
| df-workflow DAG 引擎 | ✅ 完成 | 拓扑排序/执行器/状态机/EventBus |
|
||||
| df-storage SQLite 基础表 | ✅ 完成 | 6 张表 + 4 索引,缺 CRUD |
|
||||
| df-execute Shell 执行 | ✅ 完成 | tokio::process 实现 |
|
||||
| 最小前端:项目列表 + 工作流日志 | ✅ 完成 | Sprint 4:5 页面接 Store + 创建/运行交互 |
|
||||
| 验证:3 节点简单工作流 | ⬜ 待验证 | 后端就绪,需端到端跑通 |
|
||||
| df-storage CRUD 层 | ✅ 完成 | Sprint 2:6 Repo + impl_repo! 宏;Sprint 3 增 BranchRepo |
|
||||
| 对抗式评估系统 | ✅ 完成 | Sprint 6:AdversarialEngine + 正反方辩论 + AI 分析师 |
|
||||
| Migrations V2(关联字段 + branches 表) | ✅ 完成 | Sprint 3 |
|
||||
| Executor 同层并行 + 状态转换校验 | ✅ 完成 | Sprint 3,4 单测全绿 |
|
||||
| Tauri IPC 命令 | ✅ 完成 | Sprint 3:17 commands + 事件转发 |
|
||||
| Store 接入 View | ✅ 完成 | Sprint 4:API 层 + composable Store + 5 页面改造 |
|
||||
|
||||
### Phase 2 — AI 集成 ✅ 核心完成
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| df-ai Provider 实现 | ✅ | OpenAI 兼容 HTTP client,流式 SSE 解析 |
|
||||
| AI 工具注册 + V3 迁移 | ✅ | AiToolRegistry + 12 工具(list/create/update/delete + 文件系统) |
|
||||
| AI IPC 命令 | ✅ | 17 个 AI command(对话/工具执行/审批门控) |
|
||||
| AI Chat 面板 | ✅ | 侧边栏 + 分离窗口 + 拖拽调宽 + 对话管理 |
|
||||
| Agentic Loop | ✅ | 多轮工具调用循环(max 10 轮)+ 中/高风险人工审批 |
|
||||
| AI Node 实现 | ✅ | df-nodes 接 df-ai:config 驱动 provider(base_url/api_key/model/prompt)→ OpenAICompatProvider.complete → 输出 text/model/usage;注册 "ai" 节点;未端到端实测 |
|
||||
| **AI Chat LLM 优化** | 🚧 P0 完成 | 任务 #43:P0 可靠性(超时/断连/停止生成)✅;P1 token 控制(usage采集/滑动窗口/摘要)⬜ | |
|
||||
|
||||
### Phase 3 — 想法池 + 多项目 (3-4 周) | ⬜ 未开始
|
||||
### Phase 4 — 节点丰富 + 阶段插件 (3-4 周) | ⬜ 未开始
|
||||
### Phase 5 — 体验打磨 (4-6 周) | ⬜ 未开始
|
||||
|
||||
---
|
||||
|
||||
## 六、工作迭代日志
|
||||
|
||||
### [Sprint 1] 2026-06-10 — 项目初始化
|
||||
|
||||
**工作内容**:
|
||||
- 创建项目骨架:`bun create vite` + Tauri v2 + Cargo Workspace
|
||||
- 编写完整架构文档 `ARCHITECTURE.md` (22,745 字)
|
||||
- 13 个 Rust crate 全部创建,类型系统和接口设计完整
|
||||
- 9 个 Vue 页面 UI 全部编写(~3,372 行),设计系统完善
|
||||
- 4 个 Pinia Store 定义(state + computed)
|
||||
- i18n 中英文翻译
|
||||
|
||||
**代码规模**:Rust ~4,108 行 / Vue+TS ~3,896 行 / 总计 ~8,000 行
|
||||
|
||||
**遗留问题**:
|
||||
1. 所有数据硬编码,无持久化
|
||||
2. 前后端未桥接
|
||||
3. 未做 git commit
|
||||
|
||||
**下一步建议**:
|
||||
1. 首次 git commit 建立基线
|
||||
2. 优先实现 df-storage CRUD 层(所有持久化的前置条件)
|
||||
3. 实现 Tauri IPC 命令层
|
||||
4. 将 Store 接入 View,替换硬编码数据
|
||||
5. 端到端验证:3 节点简单工作流跑通
|
||||
|
||||
---
|
||||
|
||||
### [Sprint 2] 2026-06-10/11 — 设计审查 + 对抗论证 + 阻塞项修复
|
||||
|
||||
**工作内容**:
|
||||
- 三路功能审查(架构/前端/数据模型):发现 4 条核心路径全断、缺 10 张表、状态枚举三套不一致
|
||||
- 三路对抗论证(市场/技术/需求"魔法打败魔法"):综合结论 = 方向有价值、scope 必须砍 60%
|
||||
- 修复 Phase 1 阻塞项:Shell Windows 兼容(cmd /C)、状态枚举统一(types.rs 为准)、DAG 序列化(DagDef + NodeRegistry)
|
||||
- 实现 df-storage CRUD 层(6 Repo + impl_repo! 宏 + spawn_blocking + 列名白名单)
|
||||
- ScriptNode 接入 df-execute Shell 执行器
|
||||
- 建立 docs/ 8 层文档体系(中文命名)
|
||||
- 新增设计文档:业务系统设计、对抗论证裁决报告、想法探索-对抗式评估
|
||||
|
||||
**关键决策(详见 docs/02-架构设计/对抗论证裁决报告.md)**:
|
||||
- 定位调整:"产研操作系统" → "本地优先的个人开发流程驾驶舱"
|
||||
- 砍掉:插件系统、经验进化(自动)、需求-测试追溯、多模型路由、Agent 协作
|
||||
- 降级:想法池(简化+对抗式评估)、标注(TODO 扫描器)、决策留痕(字段级)
|
||||
- Phase 1 只激活 6 个 crate,其余 7 个标记预留
|
||||
- 验证策略:先自用 3 个月再决定是否对外
|
||||
|
||||
**遗留问题**:
|
||||
1. Executor 同层并行化 + StateMachine 转换校验(任务 #8)
|
||||
2. Migrations V2 补字段(promoted_to/workflow_def_id/project_id 等)+ branches 表(任务 #9)
|
||||
3. Tauri IPC / 前端 Store 对接尚未开始
|
||||
4. Sprint 2 的代码变更未 commit
|
||||
|
||||
**下一步**:
|
||||
1. commit Sprint 2 变更
|
||||
2. Migrations V2 + Executor 并行化
|
||||
3. Tauri IPC(AppState + project/task/workflow commands)
|
||||
4. 前端类型对齐 + Store 接 View
|
||||
|
||||
### [Sprint 3] 2026-06-11 — Migrations V2 + Executor 并行化 + Tauri IPC(三代理并行)
|
||||
|
||||
**工作内容**(三个子代理并行,目录互不重叠):
|
||||
- **Migrations V2**(df-storage/df-core):版本推进到 2 — ideas 补 promoted_to/ai_analysis/scores、tasks 补 workflow_def_id/base_branch、workflow_executions 补 project_id/task_id、新建 branches 表(含 2 索引);BranchRecord/BranchRepo/BranchStatus 枚举同步落地,ALLOWED_COLUMNS 白名单 +8 列
|
||||
- **Executor 并行化**(df-workflow):同层节点改 futures::join_all 并发,三阶段模式(串行准备→并发执行→串行收尾)规避借用冲突;StateMachine 增加转换校验(仅 Pending→Running、Running→Completed/Failed 合法,set_xxx 改返回 Result);4 个单测全绿(含并行耗时断言 <180ms、失败中止下游层)
|
||||
- **Tauri IPC**(src-tauri):AppState(db + 6 Repo + EventBus + NodeRegistry)+ 17 个 command(project/task/idea CRUD + run_workflow/list/get executions);run_workflow 先落库 status=running 立即返回 execution_id,事件经 app.emit("workflow-event") 转发前端,完成后回写状态。注意:NodeRegistry 不能用 default()(script 工厂是 unimplemented! 占位),改为 new() + 手动注册真实 ScriptNode
|
||||
|
||||
**验证**:cargo check 通过(仅 1 个 dead_code 警告:releases/node_executions Repo 暂无 command 使用);cargo test -p df-workflow 4 passed
|
||||
|
||||
**遗留问题**:
|
||||
1. executor 失败路径只发 NodeFailed 不发 WorkflowFailed,目前由 run_workflow 在 IPC 层补发——后续应下沉到 executor
|
||||
2. 前端 Store/View 仍硬编码,未调用任何 command
|
||||
3. NodeRegistry::default() 的 unimplemented! 占位是隐患,应改为注册真实节点或移除
|
||||
|
||||
**下一步**:
|
||||
1. 前端类型对齐(id: string、状态枚举对齐 types.rs)+ src/api/ 封装层
|
||||
2. Store 加 action 接 API,核心 4 页面(Dashboard/Projects/ProjectDetail/Tasks)接 Store
|
||||
3. 端到端验证:tauri dev 创建项目 → 运行 3 节点工作流 → 日志实时展示
|
||||
|
||||
### [Sprint 4] 2026-06-11 — 前端交互功能(API 层 + Store + 5 页面)
|
||||
|
||||
**工作内容**:
|
||||
- **API 层**:新建 `src/api/` 目录 — types.ts(与 Rust Record 严格对齐)+ project/task/idea/workflow 四个 invoke 封装 + 事件监听(listen workflow-event)
|
||||
- **Store 重写**:`stores/project.ts` 改为 composable 模式(reactive 全局状态 + 12 个 actions + computed stats),不依赖 Pinia
|
||||
- **Projects.vue**:接 Store 真数据 + 创建项目模态框(名称/描述)+ 卡片点击跳详情页 + status 中文映射
|
||||
- **ProjectDetail.vue**:路由参数加载项目 + 真实任务列表 + **3 节点 Shell DAG 工作流运行**(环境检查→运行测试→构建产物)+ 实时事件日志 + 创建任务模态框 + onUnmounted 清理监听
|
||||
- **Tasks.vue**:项目/状态双筛选联动 + 创建任务模态框(选择项目+标题)+ 状态/优先级中文映射
|
||||
- **Ideas.vue**:捕捉想法模态框 + 选中详情(tags JSON 解析、scores 展示)+ 删除功能
|
||||
- **Dashboard.vue**:统计卡从 Store computed 计算 + Active Projects/Idea Pool 接真数据 + quickCapture 跳 Ideas
|
||||
|
||||
**代码变更**:13 files, +1059/-486
|
||||
|
||||
**遗留问题**:
|
||||
1. 端到端验证未完成:需 tauri dev 实际创建项目→运行工作流→确认日志展示
|
||||
2. 无数据时空状态展示较简陋
|
||||
3. ~~Settings/Knowledge/Decisions 三个页面仍为硬编码 demo~~ ✅ Sprint 7 已解决(三 view 接 stores/{knowledge,settings}.ts,无 ref([ 硬编码)
|
||||
|
||||
**下一步**:
|
||||
1. 端到端验证:tauri dev → 创建项目 → 运行 3 节点工作流 → 日志实时展示
|
||||
2. 修复验证中发现的问题
|
||||
3. 考虑 Phase 2 规划
|
||||
|
||||
### [Sprint 5] 2026-06-11 — AI Chat 全功能(Provider + 工具执行 + Agentic Loop)
|
||||
|
||||
**工作内容**:
|
||||
- **df-ai Provider**:OpenAI 兼容 HTTP client,SSE 流式解析,ChatMessage/CompletionRequest 类型系统
|
||||
- **V3 存储迁移**:ai_providers、ai_conversations、ai_tool_executions 三张表 + 对应 Repo
|
||||
- **AI IPC 命令层**:17 个 command — 对话发送/流式推送、工具执行/审批门控(低风险自动/中高风险人工)、对话管理(CRUD + 切换)
|
||||
- **AI 工具注册**:12 个工具 — list_projects/tasks/ideas(只读)、create_project/task/idea(创建)、update_project(更新)、delete_project(删除)、read_file/list_directory/write_file(文件系统)
|
||||
- **Agentic Loop**:多轮工具调用循环(max 10 轮),流式输出→检测工具调用→执行→结果回传→继续,中高风险暂停等审批后恢复
|
||||
- **AI Chat 前端**:侧边栏面板 + 拖拽调宽 + 分离独立窗口 + 对话列表管理 + 工具审批弹窗 + AgentRound 多轮消息
|
||||
- **Tauri Capabilities**:ai-detached 窗口权限配置
|
||||
|
||||
**代码变更**:20+ files,新增 ~2000 行
|
||||
|
||||
**遗留问题**:
|
||||
1. ~~空白屏问题:Vite 启动正常(122s)、HTML 200,但 webview 显示空白~~ ✅ Sprint 7 已修复(index.html 主题防闪烁脚本 + 启动计时埋点 __APP_T0 + body 背景色)
|
||||
2. AI Node 未实现 — 目前只有骨架,需接入 df-ai 的 LlmProvider(任务 #44)
|
||||
3. AI Chat LLM 调用逻辑待优化:长对话可靠性、大任务执行稳定性、token 消耗控制(任务 #43)— **P0 可靠性已在 Sprint 6 完成,P1 token 控制待办**
|
||||
|
||||
**下一步**:
|
||||
1. 排查并修复空白屏问题
|
||||
2. 实际验证 AI Chat 端到端(选 Provider → 发消息 → 工具调用 → 审批)
|
||||
3. HumanNode + Settings 页面
|
||||
4. AI Chat LLM 调用逻辑优化(任务 #43)
|
||||
5. AI Node 实现(任务 #44)
|
||||
|
||||
### [Sprint 6] 2026-06-11 — AI Chat 调用可靠性 P0 + 前端类型修复
|
||||
|
||||
**工作内容**:
|
||||
|
||||
**A. AI Chat 调用可靠性 P0(df-ai + src-tauri,跨前后端 6 文件)** — 三路代理核对(调用链/上下文-token/前端)后定位可靠性死锁类问题,本轮按"先保命"只做 P0 五项(P1 token 控制设计已定,留待下轮):
|
||||
- **P0-1 连接超时**:`openai_compat.rs` Client 加 `connect_timeout(30s)` 防连接阶段无限 hang;不设总 timeout(避免 reqwest 总时长限制误砍流式长生成任务)
|
||||
- **P0-2 流式 idle timeout**:`stream_llm` 用 `tokio::time::timeout(120s)` 包 `stream.next()`,防"连上后中途静默"无限 hang
|
||||
- **P0-3 断连检测丢弃残缺**:维护 `finished_received` 标志,流尽未收到 finished 信号 → emit AiError + **丢弃残缺响应不当完整入库**(防脏历史污染)
|
||||
- **P0-4 停止生成**:`AiSession.stop_flag: Arc<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 轮 review:ai.rs、openai_compat.rs)
|
||||
|
||||
**验证**:cargo check -p devflow ✓(仅既有 warning)、vue-tsc --noEmit ✓、bun run build ✓(built in 2.47s)
|
||||
|
||||
**遗留问题 / 下一步**:
|
||||
1. **P1 token 控制(任务 #43 续)**:流式 usage 采集(`stream_options:{include_usage:true}` + SSE usage 字段)+ token 预算自适应窗口(chars≈/3.5 启发式估算)+ 历史摘要压缩(早期摘要 + 近期全文)—— 设计已定,未实施。**⚠️ usage 采集 2026-06-11 用户叫停暂缓(结构层写一半已回退),P1 从 token 估算/裁剪/摘要续**
|
||||
2. **实测验证**:`cargo tauri dev` 跑起来验证停止按钮/超时/断连真实生效(本轮仅编译 + 类型验证)
|
||||
3. 停止生成边界:LLM 正常输出时停止立即生效(<1s);idle(无输出)时最多等 120s idle timeout —— P2 可用 `tokio::sync::Notify` 优化为绝对即时
|
||||
4. ~~Sprint 5 遗留的空白屏问题仍待排查~~ ✅ Sprint 7 已修复(用户实测确认)
|
||||
|
||||
### [Sprint 7] 2026-06-11 — 空白屏修复 + i18n 接入 + 剩余页面接 Store
|
||||
|
||||
**工作内容**:
|
||||
- **空白屏修复**(前端):`index.html` 加主题防闪烁脚本(localStorage `df-theme` 渲染前置 `data-theme`)+ 启动计时埋点 `window.__APP_T0` + body 背景 `#0c0e1a`;`main.ts` 加 `.use(i18n)` 并打印 mount 耗时日志。用户实测确认空白屏消失
|
||||
- **i18n 接入**:`main.ts` 注册 `i18n` 插件;`i18n/index.ts` 完整(legacy:false / globalInjection / zh-CN 默认 + en fallback / localStorage `df-language` 持久化);App.vue 导航与各 view 改 `$t()`
|
||||
- **剩余 3 页面接 Store**:`Knowledge.vue`/`Settings.vue`/`Decisions` 去硬编码 `ref([...])`,对接新建 `stores/{knowledge,settings,ai}.ts`
|
||||
- **新路由**:`/` redirect → `/ai-home`,新增 `AiHome.vue`(默认落地页)/`AiDetached.vue`(分离窗口);路由 9→11 条
|
||||
- **df-nodes 实现度修正**:核对发现 `script_node.rs` 早为真实实现(接 df_execute Shell + 非零退出码判断),`human_node.rs` 半实现(发 `HumanApprovalRequest` 事件 + 超时循环,但审批响应 TODO 直接返回"同意")—— 非文档原记"全骨架"
|
||||
|
||||
**遗留问题 / 下一步**:
|
||||
1. **P1 token 控制(任务 #43 续)**:仍待办(usage 采集用户叫停,从 token 估算/裁剪/摘要续)
|
||||
2. **条件表达式引擎**(conditions.rs 仍 true/false 字面量)— P2 未动
|
||||
3. ~~AI Node 实现(任务 #44)~~ ✅ 本轮已实现:`ai_node.rs` 接 df-ai `OpenAICompatProvider`,从 `ctx.config` 读 base_url/api_key/model/prompt/system_prompt/temperature/max_tokens 调 `complete()`,prompt 支持上游输入优先;`df-nodes/Cargo.toml` 加 df-ai 依赖;`state.rs` build_registry 注册 "ai"。cargo check ✓(未端到端实测真实 LLM 调用)
|
||||
4. **df-stages** 11 个 execute() 全空骨架 — Phase 4
|
||||
5. **HumanNode 审批响应闭环**:目前直接返回"同意",需前端审批弹窗回写结果
|
||||
|
||||
### [Sprint 8] 2026-06-12 — Anthropic 协议 + 切对话不中断 + 技能联想需求调研
|
||||
|
||||
**工作内容**:
|
||||
|
||||
**A. aichat 升级 A 路线(UX 快赢,已实现并验证)** — 详见 memory `aichat-roadmap-ab-split`:
|
||||
- **Anthropic 协议 Provider**:新增 `crates/df-ai/src/anthropic_compat.rs`,实现 `LlmProvider` trait 覆盖 Anthropic Messages API(`x-api-key` + `anthropic-version`、顶层 `system`、必填 `max_tokens`、SSE event 类型解析、`content_block` tool_use)。按 `provider_type` 路由分发(`run_agentic_loop` / `ai_node`)。支持 GLM 订阅端点 `https://open.bigmodel.cn/api/anthropic` — **用户实测对话流式 OK**
|
||||
- **切对话不打断生成**:后端 `AiChatEvent` 全变体加 `conversation_id`,agentic loop spawn 前快照 conv_id,所有 emit 填充;`ai_conversation_switch` 生成中只读返回;前端 `stores/ai.ts` 按 conversation_id 路由(后台对话事件不污染当前视图,完成/错误时刷新侧边栏)
|
||||
- **审查项落地**:删全屏 Tool Approval Modal(保留行内卡片审批)、错误友好化(`friendlyError` 正则映射 404/401/timeout/network → 中文提示 + `isError` 红色气泡)、智能滚动(`isNearBottom` < 80px 阈值)、provider bar 点击切换、未配 provider 空状态引导、删死 class
|
||||
- **B 路线(规划式多智能体)单独立项**:填 `coordinator.rs` + `conditions.rs` 空壳,详见 memory `aichat-decision-capability`
|
||||
|
||||
**B. 技能联想功能需求(本轮重点,未实现,需求阶段)**:
|
||||
- **需求**:aichat 输入框输入 `/` → 联想列出本机 Claude 技能(SKILL.md),按输入过滤,选中插入 `/name`;发送时后端读 SKILL.md 全文注入 system prompt / context
|
||||
- **首批数据源 = Claude 3 类**(SKILL.md frontmatter 统一 `name` / `description` / `user_invocable` / `metadata.argument-hint` / `triggers[]`,只取 `user_invocable: true`):
|
||||
1. Claude skills:`~/.claude/skills/*/SKILL.md`(23 个:sleep / mission-control / idea / cpa 等)
|
||||
2. Claude commands:`~/.claude/commands/*.md`(1 个:idea)
|
||||
3. Claude plugins:`~/.claude/plugins/marketplaces/**/skills/*/SKILL.md`(caveman 全家、官方 frontend-design / skill-creator / mcp-server-dev、wuming prompt-optimizer;`cache/` 与 `marketplaces/` 重复需按 path 去重)
|
||||
- 加项目级 `.claude/skills` + `.claude/commands`(DevFlow 暂无)
|
||||
- **实现要点**:① 后端新 IPC `list_skills()` 扫 Claude 3 类来源解析 frontmatter + 按 path 去重,返回 `[{name, description, argument_hint, triggers, source}]`;② 前端输入框 `/` 触发联想浮层(名称 + description);③ `/name` 发送 → 后端读 SKILL.md 注入 context;④ 跨平台 home 路径(dirs crate)
|
||||
|
||||
**C. 多工具 skill 体系调研结论(仅记录,后续按需扩展)**:
|
||||
| 工具 | 本机状态 | skill 机制 | 是否纳入 |
|
||||
|------|---------|-----------|---------|
|
||||
| Claude Code | ✅ 已装 | SKILL.md(skills + commands + plugins 三类) | ✅ 首批数据源 |
|
||||
| Codex (OpenAI) | ✅ 已装 | SKILL.md(`~/.codex/vendor_imports/skills/skills/.curated/*/SKILL.md`,39 个:figma / notion / playwright / security / vercel·netlify·render-deploy / latex 等;frontmatter 与 Claude 一致,可统一解析) | ⏳ 后续可扩展 |
|
||||
| openclaw | ✅ 已装 | 多 agent gateway(`~/.openclaw/openclaw.json` 注册 8 agent 绑各 workspace + glm-5,经飞书 channel binding),非 SKILL.md;`agents/` 实体目录暂空 | ❌ 属 agent 选择层,非 skill 联想,暂不纳入 |
|
||||
| opencode | ❌ 未装 | AGENTS.md + custom agents + slash commands(机制待确认) | ⏳ 待装后调研 |
|
||||
|
||||
**代码变更**:A 路线 — `crates/df-ai/src/{anthropic_compat.rs, lib.rs}`、`src-tauri/src/commands/ai.rs`、`crates/df-nodes/src/ai_node.rs`、`src/views/Settings.vue`、`src/stores/ai.ts`、`src/components/AiChat.vue`、`src/api/types.ts`
|
||||
|
||||
**遗留问题 / 下一步**:
|
||||
1. **技能联想(B)未实现** — 需求已记,首批聚焦 Claude 3 类,等排期(工作量中:后端扫文件 + IPC、前端联想浮层)
|
||||
2. **A 路线剩余场景实测**:切对话不中断(场景 2/3)、错误友好化(场景 4)待用户实测
|
||||
3. **B 路线(规划式能力)**:coordinator / conditions 空壳,单独立项
|
||||
|
||||
<!--
|
||||
|
||||
### [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
301
bun.lock
Normal 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
18
crates/df-ai/Cargo.toml
Normal 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"
|
||||
166
crates/df-ai/src/ai_tools.rs
Normal file
166
crates/df-ai/src/ai_tools.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
//! AI 工具注册 — 将现有 CRUD 操作注册为 LLM 可调用的 Tool
|
||||
//!
|
||||
//! 风险分级:
|
||||
//! - Low: 只读操作(list/get),AI 自动执行
|
||||
//! - Medium: 创建操作(create),显示意图,可配置自动批准
|
||||
//! - High: 破坏性操作(delete / run_workflow),必须人工批准
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::provider::ToolDefinition;
|
||||
|
||||
// ============================================================
|
||||
// 风险级别
|
||||
// ============================================================
|
||||
|
||||
/// 工具调用风险级别
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RiskLevel {
|
||||
/// 只读操作 — AI 自动执行
|
||||
Low,
|
||||
/// 创建操作 — 可配置自动批准
|
||||
Medium,
|
||||
/// 破坏性操作 — 必须人工批准
|
||||
High,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具定义
|
||||
// ============================================================
|
||||
|
||||
/// 已注册的 AI 工具
|
||||
pub struct AiTool {
|
||||
/// 工具定义(发送给 LLM 的 JSON Schema)
|
||||
pub definition: ToolDefinition,
|
||||
/// 风险级别
|
||||
pub risk_level: RiskLevel,
|
||||
/// 执行处理器
|
||||
pub handler: AiToolHandler,
|
||||
}
|
||||
|
||||
/// 工具处理器类型 — 异步函数,接收 JSON 参数,返回 JSON 结果
|
||||
pub type AiToolHandler =
|
||||
Box<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,
|
||||
})
|
||||
}
|
||||
426
crates/df-ai/src/anthropic_compat.rs
Normal file
426
crates/df-ai/src/anthropic_compat.rs
Normal 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_result(role=Tool)→ user 消息含 tool_result 块;连续多个合并为一条 user
|
||||
/// - tool_definitions 的 parameters → input_schema
|
||||
fn convert_request(&self, req: CompletionRequest) -> AnthropicRequest {
|
||||
let model = if req.model.is_empty() {
|
||||
self.default_model.clone()
|
||||
} else {
|
||||
req.model
|
||||
};
|
||||
|
||||
// 抽离 system 消息
|
||||
let system: Option<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
59
crates/df-ai/src/context.rs
Normal file
59
crates/df-ai/src/context.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
28
crates/df-ai/src/coordinator.rs
Normal file
28
crates/df-ai/src/coordinator.rs
Normal 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
10
crates/df-ai/src/lib.rs
Normal 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;
|
||||
410
crates/df-ai/src/openai_compat.rs
Normal file
410
crates/df-ai/src/openai_compat.rs
Normal 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/completions(OpenAI 约定)
|
||||
fn chat_url(&self) -> String {
|
||||
let base = self.base_url.trim_end_matches('/');
|
||||
if base.ends_with("/chat/completions") {
|
||||
return base.to_string();
|
||||
}
|
||||
if Self::ends_with_version(base) {
|
||||
return format!("{}/chat/completions", base);
|
||||
}
|
||||
format!("{}/v1/chat/completions", base)
|
||||
}
|
||||
|
||||
/// base_url 是否以 `/v<数字>` 结尾(如 /v1 /v4)
|
||||
fn ends_with_version(base: &str) -> bool {
|
||||
match base.rsplit_once('/') {
|
||||
Some((_, last)) if last.starts_with('v') && last.len() > 1 => {
|
||||
last[1..].bytes().all(|b| b.is_ascii_digit())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 将通用请求转换为 OpenAI 格式
|
||||
fn convert_request(&self, req: CompletionRequest) -> OpenAiRequest {
|
||||
let model = if req.model.is_empty() {
|
||||
self.default_model.clone()
|
||||
} else {
|
||||
req.model
|
||||
};
|
||||
|
||||
let messages: Vec<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
207
crates/df-ai/src/provider.rs
Normal file
207
crates/df-ai/src/provider.rs
Normal 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,
|
||||
/// 工具调用 ID(role=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;
|
||||
}
|
||||
51
crates/df-ai/src/router.rs
Normal file
51
crates/df-ai/src/router.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
45
crates/df-ai/src/stream.rs
Normal file
45
crates/df-ai/src/stream.rs
Normal 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
11
crates/df-core/Cargo.toml
Normal 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 }
|
||||
49
crates/df-core/src/error.rs
Normal file
49
crates/df-core/src/error.rs
Normal 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>;
|
||||
74
crates/df-core/src/events.rs
Normal file
74
crates/df-core/src/events.rs
Normal 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>,
|
||||
},
|
||||
}
|
||||
5
crates/df-core/src/lib.rs
Normal file
5
crates/df-core/src/lib.rs
Normal 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
301
crates/df-core/src/types.rs
Normal 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()
|
||||
}
|
||||
12
crates/df-evolve/Cargo.toml
Normal file
12
crates/df-evolve/Cargo.toml
Normal 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 }
|
||||
58
crates/df-evolve/src/evolve_engine.rs
Normal file
58
crates/df-evolve/src/evolve_engine.rs
Normal 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![]
|
||||
}
|
||||
}
|
||||
98
crates/df-evolve/src/knowledge.rs
Normal file
98
crates/df-evolve/src/knowledge.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
12
crates/df-evolve/src/lib.rs
Normal file
12
crates/df-evolve/src/lib.rs
Normal 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};
|
||||
47
crates/df-evolve/src/pattern.rs
Normal file
47
crates/df-evolve/src/pattern.rs
Normal 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
|
||||
}
|
||||
}
|
||||
41
crates/df-evolve/src/prompt_template.rs
Normal file
41
crates/df-evolve/src/prompt_template.rs
Normal 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
|
||||
}
|
||||
}
|
||||
45
crates/df-evolve/src/review_rule.rs
Normal file
45
crates/df-evolve/src/review_rule.rs
Normal 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,
|
||||
}
|
||||
13
crates/df-execute/Cargo.toml
Normal file
13
crates/df-execute/Cargo.toml
Normal 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 }
|
||||
46
crates/df-execute/src/docker.rs
Normal file
46
crates/df-execute/src/docker.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
47
crates/df-execute/src/git_ops.rs
Normal file
47
crates/df-execute/src/git_ops.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
6
crates/df-execute/src/lib.rs
Normal file
6
crates/df-execute/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! df-execute: 执行运行时 — Shell、Docker、SSH、Git 操作
|
||||
|
||||
pub mod docker;
|
||||
pub mod git_ops;
|
||||
pub mod shell;
|
||||
pub mod ssh;
|
||||
73
crates/df-execute/src/shell.rs
Normal file
73
crates/df-execute/src/shell.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
38
crates/df-execute/src/ssh.rs
Normal file
38
crates/df-execute/src/ssh.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
13
crates/df-ideas/Cargo.toml
Normal file
13
crates/df-ideas/Cargo.toml
Normal 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 }
|
||||
308
crates/df-ideas/src/adversarial.rs
Normal file
308
crates/df-ideas/src/adversarial.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
98
crates/df-ideas/src/capture.rs
Normal file
98
crates/df-ideas/src/capture.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
79
crates/df-ideas/src/evaluator.rs
Normal file
79
crates/df-ideas/src/evaluator.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
76
crates/df-ideas/src/graph.rs
Normal file
76
crates/df-ideas/src/graph.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
8
crates/df-ideas/src/lib.rs
Normal file
8
crates/df-ideas/src/lib.rs
Normal 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;
|
||||
91
crates/df-ideas/src/promotion.rs
Normal file
91
crates/df-ideas/src/promotion.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
75
crates/df-ideas/src/scoring.rs
Normal file
75
crates/df-ideas/src/scoring.rs
Normal 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
|
||||
}
|
||||
}
|
||||
16
crates/df-nodes/Cargo.toml
Normal file
16
crates/df-nodes/Cargo.toml
Normal 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 }
|
||||
167
crates/df-nodes/src/ai_node.rs
Normal file
167
crates/df-nodes/src/ai_node.rs
Normal 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 缺少必填参数: prompt(config 或上游输入均无)"))?;
|
||||
|
||||
// ── 可选参数 ──
|
||||
let model = ctx
|
||||
.config
|
||||
.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let temperature = ctx
|
||||
.config
|
||||
.get("temperature")
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|f| f as f32);
|
||||
let max_tokens = ctx
|
||||
.config
|
||||
.get("max_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|n| n as u32);
|
||||
let system_prompt = ctx
|
||||
.config
|
||||
.get("system_prompt")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// 协议类型:默认 openai_compat,可设 anthropic(GLM 订阅端点 / Claude 官方)
|
||||
let protocol = ctx
|
||||
.config
|
||||
.get("protocol")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("openai_compat")
|
||||
.to_string();
|
||||
|
||||
// default_model:留空时给一个占位,避免 provider 构造 panic
|
||||
let default_model = if model.is_empty() {
|
||||
"gpt-4o-mini".to_string()
|
||||
} else {
|
||||
model.clone()
|
||||
};
|
||||
|
||||
let provider: Box<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(默认)或 anthropic(GLM订阅/Claude官方)" },
|
||||
"base_url": { "type": "string", "description": "API 地址,OpenAI 兼容如 https://api.deepseek.com;Anthropic 如 https://open.bigmodel.cn/api/anthropic" },
|
||||
"api_key": { "type": "string", "description": "API 密钥" },
|
||||
"model": { "type": "string", "description": "模型名(可选,留空用默认)" },
|
||||
"temperature": { "type": "number", "description": "温度 0.0~2.0(可选)" },
|
||||
"max_tokens": { "type": "integer", "description": "最大生成 token(可选,anthropic 协议无值时默认 4096)" }
|
||||
},
|
||||
"required": ["prompt", "base_url", "api_key"]
|
||||
}),
|
||||
output: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": { "type": "string" },
|
||||
"model": { "type": "string" },
|
||||
"usage": { "type": "object" }
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"ai"
|
||||
}
|
||||
}
|
||||
41
crates/df-nodes/src/docker_node.rs
Normal file
41
crates/df-nodes/src/docker_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
41
crates/df-nodes/src/git_node.rs
Normal file
41
crates/df-nodes/src/git_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
43
crates/df-nodes/src/http_node.rs
Normal file
43
crates/df-nodes/src/http_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
96
crates/df-nodes/src/human_node.rs
Normal file
96
crates/df-nodes/src/human_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
10
crates/df-nodes/src/lib.rs
Normal file
10
crates/df-nodes/src/lib.rs
Normal 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;
|
||||
41
crates/df-nodes/src/notify_node.rs
Normal file
41
crates/df-nodes/src/notify_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
94
crates/df-nodes/src/script_node.rs
Normal file
94
crates/df-nodes/src/script_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
39
crates/df-nodes/src/subflow_node.rs
Normal file
39
crates/df-nodes/src/subflow_node.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
14
crates/df-plugin/Cargo.toml
Normal file
14
crates/df-plugin/Cargo.toml
Normal 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 }
|
||||
66
crates/df-plugin/src/host.rs
Normal file
66
crates/df-plugin/src/host.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
5
crates/df-plugin/src/lib.rs
Normal file
5
crates/df-plugin/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! df-plugin: 插件系统 — 插件加载、宿主环境、SDK
|
||||
|
||||
pub mod host;
|
||||
pub mod loader;
|
||||
pub mod sdk;
|
||||
94
crates/df-plugin/src/loader.rs
Normal file
94
crates/df-plugin/src/loader.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
90
crates/df-plugin/src/sdk.rs
Normal file
90
crates/df-plugin/src/sdk.rs
Normal 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: 实现宏
|
||||
};
|
||||
}
|
||||
13
crates/df-project/Cargo.toml
Normal file
13
crates/df-project/Cargo.toml
Normal 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 }
|
||||
52
crates/df-project/src/context.rs
Normal file
52
crates/df-project/src/context.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
6
crates/df-project/src/lib.rs
Normal file
6
crates/df-project/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! df-project: 项目管理 — 项目创建、调度、上下文、时间线
|
||||
|
||||
pub mod context;
|
||||
pub mod manager;
|
||||
pub mod scheduler;
|
||||
pub mod timeline;
|
||||
84
crates/df-project/src/manager.rs
Normal file
84
crates/df-project/src/manager.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
66
crates/df-project/src/scheduler.rs
Normal file
66
crates/df-project/src/scheduler.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
64
crates/df-project/src/timeline.rs
Normal file
64
crates/df-project/src/timeline.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
14
crates/df-stages/Cargo.toml
Normal file
14
crates/df-stages/Cargo.toml
Normal 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 }
|
||||
50
crates/df-stages/src/coding.rs
Normal file
50
crates/df-stages/src/coding.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
57
crates/df-stages/src/idea.rs
Normal file
57
crates/df-stages/src/idea.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
7
crates/df-stages/src/lib.rs
Normal file
7
crates/df-stages/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! df-stages: 阶段插件 — 开发流程各阶段的节点模板注册
|
||||
|
||||
pub mod coding;
|
||||
pub mod idea;
|
||||
pub mod release;
|
||||
pub mod requirement;
|
||||
pub mod testing;
|
||||
73
crates/df-stages/src/release.rs
Normal file
73
crates/df-stages/src/release.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
50
crates/df-stages/src/requirement.rs
Normal file
50
crates/df-stages/src/requirement.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
50
crates/df-stages/src/testing.rs
Normal file
50
crates/df-stages/src/testing.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
13
crates/df-storage/Cargo.toml
Normal file
13
crates/df-storage/Cargo.toml
Normal 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 }
|
||||
614
crates/df-storage/src/crud.rs
Normal file
614
crates/df-storage/src/crud.rs
Normal 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()))?
|
||||
}
|
||||
}
|
||||
61
crates/df-storage/src/db.rs
Normal file
61
crates/df-storage/src/db.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
6
crates/df-storage/src/lib.rs
Normal file
6
crates/df-storage/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! df-storage: 存储层 — SQLite 数据库、模型定义、迁移
|
||||
|
||||
pub mod crud;
|
||||
pub mod db;
|
||||
pub mod migrations;
|
||||
pub mod models;
|
||||
174
crates/df-storage/src/migrations.rs
Normal file
174
crates/df-storage/src/migrations.rs
Normal 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);
|
||||
";
|
||||
170
crates/df-storage/src/models.rs
Normal file
170
crates/df-storage/src/models.rs
Normal 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
13
crates/df-task/Cargo.toml
Normal 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 }
|
||||
74
crates/df-task/src/branch.rs
Normal file
74
crates/df-task/src/branch.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
6
crates/df-task/src/lib.rs
Normal file
6
crates/df-task/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! df-task: 任务/分支管理 — 任务 CRUD、分支管理、合并协调、发布计划
|
||||
|
||||
pub mod branch;
|
||||
pub mod merge;
|
||||
pub mod release;
|
||||
pub mod task;
|
||||
91
crates/df-task/src/merge.rs
Normal file
91
crates/df-task/src/merge.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
132
crates/df-task/src/release.rs
Normal file
132
crates/df-task/src/release.rs
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
87
crates/df-task/src/task.rs
Normal file
87
crates/df-task/src/task.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
11
crates/df-traceability/Cargo.toml
Normal file
11
crates/df-traceability/Cargo.toml
Normal 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 }
|
||||
130
crates/df-traceability/src/annotation.rs
Normal file
130
crates/df-traceability/src/annotation.rs
Normal 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![])
|
||||
}
|
||||
}
|
||||
95
crates/df-traceability/src/decision.rs
Normal file
95
crates/df-traceability/src/decision.rs
Normal 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![]
|
||||
}
|
||||
}
|
||||
9
crates/df-traceability/src/lib.rs
Normal file
9
crates/df-traceability/src/lib.rs
Normal 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;
|
||||
78
crates/df-traceability/src/traceability.rs
Normal file
78
crates/df-traceability/src/traceability.rs
Normal 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![]
|
||||
}
|
||||
}
|
||||
15
crates/df-workflow/Cargo.toml
Normal file
15
crates/df-workflow/Cargo.toml
Normal 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 }
|
||||
33
crates/df-workflow/src/conditions.rs
Normal file
33
crates/df-workflow/src/conditions.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
141
crates/df-workflow/src/dag.rs
Normal file
141
crates/df-workflow/src/dag.rs
Normal 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 == 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()
|
||||
}
|
||||
}
|
||||
104
crates/df-workflow/src/dag_def.rs
Normal file
104
crates/df-workflow/src/dag_def.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
68
crates/df-workflow/src/eventbus.rs
Normal file
68
crates/df-workflow/src/eventbus.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
235
crates/df-workflow/src/executor.rs
Normal file
235
crates/df-workflow/src/executor.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
10
crates/df-workflow/src/lib.rs
Normal file
10
crates/df-workflow/src/lib.rs
Normal 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;
|
||||
83
crates/df-workflow/src/node.rs
Normal file
83
crates/df-workflow/src/node.rs
Normal 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>;
|
||||
|
||||
/// 节点参数 Schema(JSON Schema 格式)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeSchema {
|
||||
/// 参数 JSON Schema
|
||||
pub params: serde_json::Value,
|
||||
/// 输出 JSON Schema
|
||||
pub output: serde_json::Value,
|
||||
}
|
||||
|
||||
/// 节点 trait — 所有工作流节点必须实现
|
||||
#[async_trait]
|
||||
pub trait Node: Send + Sync {
|
||||
/// 执行节点逻辑
|
||||
async fn execute(&self, ctx: NodeContext) -> NodeResult;
|
||||
|
||||
/// 返回节点的参数 Schema
|
||||
fn schema(&self) -> NodeSchema;
|
||||
|
||||
/// 该节点是否为阻塞节点(阻塞工作流,等待外部输入)
|
||||
fn is_blocking(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// 节点类型名称
|
||||
fn node_type(&self) -> &str;
|
||||
}
|
||||
87
crates/df-workflow/src/registry.rs
Normal file
87
crates/df-workflow/src/registry.rs
Normal 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
|
||||
}
|
||||
}
|
||||
147
crates/df-workflow/src/state.rs
Normal file
147
crates/df-workflow/src/state.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
//! 节点状态机 — 管理节点的状态转换
|
||||
//!
|
||||
//! 合法转换:Pending → Running,Running → Completed / Failed,
|
||||
//! 非法转换返回错误,避免状态被随意覆盖。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use df_core::types::{NodeId, NodeStatus};
|
||||
|
||||
/// 节点状态机
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StateMachine {
|
||||
/// 节点状态映射
|
||||
states: HashMap<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(¤t, &target) {
|
||||
anyhow::bail!(
|
||||
"节点 {} 状态转换非法:{} -> {}",
|
||||
node_id,
|
||||
current.as_str(),
|
||||
target.as_str()
|
||||
);
|
||||
}
|
||||
self.states.insert(node_id, target);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 设置节点为运行中(仅允许 Pending -> Running)
|
||||
pub fn set_running(&mut self, node_id: NodeId) -> anyhow::Result<()> {
|
||||
self.transition(node_id, NodeStatus::Running)
|
||||
}
|
||||
|
||||
/// 设置节点为已完成(仅允许 Running -> Completed)
|
||||
pub fn set_completed(&mut self, node_id: NodeId) -> anyhow::Result<()> {
|
||||
self.transition(node_id, NodeStatus::Completed)
|
||||
}
|
||||
|
||||
/// 设置节点为失败(仅允许 Running -> Failed)
|
||||
pub fn set_failed(&mut self, node_id: NodeId) -> anyhow::Result<()> {
|
||||
self.transition(node_id, NodeStatus::Failed)
|
||||
}
|
||||
|
||||
/// 设置节点为等待中(暂未纳入转换校验)
|
||||
pub fn set_waiting(&mut self, node_id: NodeId) {
|
||||
self.states.insert(node_id, NodeStatus::Waiting);
|
||||
}
|
||||
|
||||
/// 设置节点为已跳过(暂未纳入转换校验)
|
||||
pub fn set_skipped(&mut self, node_id: NodeId) {
|
||||
self.states.insert(node_id, NodeStatus::Skipped);
|
||||
}
|
||||
|
||||
/// 获取所有状态快照
|
||||
pub fn snapshot(&self) -> &HashMap<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());
|
||||
}
|
||||
}
|
||||
38
docs/01-技术文档/SQLite-CRUD模式.md
Normal file
38
docs/01-技术文档/SQLite-CRUD模式.md
Normal 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` — 统一错误类型
|
||||
47
docs/01-技术文档/Tauri-IPC模式.md
Normal file
47
docs/01-技术文档/Tauri-IPC模式.md
Normal 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
|
||||
46
docs/02-架构设计/Phase1架构决策.md
Normal file
46
docs/02-架构设计/Phase1架构决策.md
Normal 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
Reference in New Issue
Block a user