新增: Phase2 阶段收尾(Sprint 1-20)
重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
@@ -1,298 +1,234 @@
|
||||
# df-ai - AI 集成模块
|
||||
# df-ai — AI 集成模块
|
||||
|
||||
> Provider 抽象层与流式响应处理
|
||||
> 创建: 2026-06-10 | 最后更新: 2026-06-13
|
||||
|
||||
## 📋 模块概览
|
||||
---
|
||||
|
||||
`df-ai` 负责 AI 功能的核心集成,支持多个 AI Provider,提供统一的接口和流式响应处理。
|
||||
## 概述
|
||||
|
||||
### 主要特性
|
||||
- 多 Provider 支持(OpenAI、Anthropic、DeepSeek)
|
||||
- 流式响应处理
|
||||
- 工具调用支持
|
||||
- 错误处理和重试机制
|
||||
`df-ai` 是 DevFlow 的 AI 核心层,提供 LLM Provider 抽象、双协议实现(OpenAI 兼容 + Anthropic)、流式 SSE 解析、工具调用、上下文窗口管理和 embedding 支持。
|
||||
|
||||
## 🏗️ 架构设计
|
||||
---
|
||||
|
||||
### 核心组件
|
||||
```rust
|
||||
// Provider trait 定义
|
||||
pub trait AIProvider: Send + Sync {
|
||||
async fn chat_completion(&self, request: ChatRequest) -> Result<ChatResponse>;
|
||||
async fn create_embedding(&self, text: &str) -> Result<Vec<f32>>;
|
||||
}
|
||||
## 当前状态
|
||||
|
||||
// 流式响应处理
|
||||
pub struct StreamProcessor {
|
||||
event_sender: mpsc::UnboundedSender<StreamEvent>,
|
||||
}
|
||||
| 能力 | 状态 |
|
||||
|------|------|
|
||||
| LlmProvider trait | ✅ |
|
||||
| OpenAI 兼容 Provider(流式/非流式/embed)| ✅ |
|
||||
| Anthropic Provider(流式)| ✅ Sprint 8 |
|
||||
| ContextManager(分组滑窗)| ✅ Sprint 11 |
|
||||
| embed() 向量生成 | ✅ Sprint 15 |
|
||||
| AiToolRegistry(基础设施)| ✅ Sprint 5(12 工具注册在 commands/ai.rs)|
|
||||
| coordinator | ⬜ 空壳(B 路线待填)|
|
||||
|
||||
// 统一的 AI 服务
|
||||
pub struct AIService {
|
||||
providers: HashMap<String, Box<dyn AIProvider>>,
|
||||
default_provider: String,
|
||||
}
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
crates/df-ai/src/
|
||||
├── lib.rs — 公共导出
|
||||
├── provider.rs — LlmProvider trait(含 embed 默认实现)
|
||||
├── openai_compat.rs — OpenAI 兼容实现(chat + embed)
|
||||
├── anthropic_compat.rs — Anthropic Messages API 实现(Sprint 8)
|
||||
├── context.rs — ContextManager 分组滑动窗口(Sprint 11)
|
||||
├── ai_tools.rs — AiToolRegistry + 12 工具定义
|
||||
├── coordinator.rs — AgentCoordinator 空壳(B 路线)
|
||||
├── router.rs — ModelRouter 模型路由空壳(route() 按 TaskType 选模型,当前全返回 default_model)
|
||||
└── stream.rs — StreamCollector 流式辅助(累积 chunk.delta 文本 + 跟踪 finished 标志)
|
||||
```
|
||||
|
||||
### Provider 实现
|
||||
- **OpenAIProvider**: OpenAI GPT 系列模型
|
||||
- **AnthropicProvider**: Claude 系列模型
|
||||
- **DeepSeekProvider**: DeepSeek 模型
|
||||
|
||||
## 🔧 使用方法
|
||||
---
|
||||
|
||||
## LlmProvider Trait
|
||||
|
||||
### 基础聊天
|
||||
```rust
|
||||
use df_ai::AIService;
|
||||
pub type StreamResult = Pin<Box<dyn Stream<Item = anyhow::Result<StreamChunk>> + Send>>;
|
||||
|
||||
let ai_service = AIService::new(config);
|
||||
let request = ChatRequest {
|
||||
model: "gpt-4".to_string(),
|
||||
messages: vec![Message {
|
||||
role: "user".to_string(),
|
||||
content: "Hello, world!".to_string(),
|
||||
}],
|
||||
};
|
||||
#[async_trait]
|
||||
pub trait LlmProvider: Send + Sync {
|
||||
// 非流式完整响应(用于标题生成/知识提炼)
|
||||
async fn complete(&self, request: CompletionRequest)
|
||||
-> anyhow::Result<CompletionResponse>;
|
||||
|
||||
let response = ai_service.chat_completion(request).await?;
|
||||
```
|
||||
// 流式 SSE(主对话)
|
||||
async fn stream(&self, request: CompletionRequest)
|
||||
-> anyhow::Result<StreamResult>;
|
||||
|
||||
### 流式响应
|
||||
```rust
|
||||
use df_ai::stream_chat;
|
||||
|
||||
let (mut receiver, mut stream) = stream_chat(&ai_service, request).await?;
|
||||
|
||||
while let Some(event) = receiver.recv().await {
|
||||
match event {
|
||||
StreamEvent::Content(chunk) => {
|
||||
print!("{}", chunk);
|
||||
}
|
||||
StreamEvent::Done => {
|
||||
println!("\n完成");
|
||||
}
|
||||
StreamEvent::Error(e) => {
|
||||
eprintln!("错误: {}", e);
|
||||
}
|
||||
// 向量生成(默认 bail,openai_compat 覆盖实现)
|
||||
async fn embed(&self, _model: &str, _texts: Vec<String>)
|
||||
-> anyhow::Result<Vec<Vec<f32>>> {
|
||||
anyhow::bail!("该 Provider 不支持 embedding({})", self.name())
|
||||
}
|
||||
|
||||
// Provider 名称(必填)
|
||||
fn name(&self) -> &str;
|
||||
|
||||
// 支持的特性(必填:streaming / function_calling / vision)
|
||||
fn supported_features(&self) -> ProviderFeatures;
|
||||
}
|
||||
```
|
||||
|
||||
### 工具调用
|
||||
```rust
|
||||
let request = ChatRequest {
|
||||
model: "gpt-4".to_string(),
|
||||
messages: vec![Message {
|
||||
role: "user".to_string(),
|
||||
content: "创建一个文件".to_string(),
|
||||
}],
|
||||
tools: vec![Tool {
|
||||
r#type: "function".to_string(),
|
||||
function: FunctionDef {
|
||||
name: "create_file".to_string(),
|
||||
description: "创建文件".to_string(),
|
||||
parameters: Parameters {
|
||||
r#type: "object".to_string(),
|
||||
properties: serde_json::json!({
|
||||
"path": {"type": "string"},
|
||||
"content": {"type": "string"}
|
||||
}),
|
||||
required: vec!["path".to_string()],
|
||||
},
|
||||
},
|
||||
}],
|
||||
tool_choice: "auto".to_string(),
|
||||
};
|
||||
```
|
||||
|
||||
## ⚙️ 配置
|
||||
|
||||
### Provider 配置
|
||||
```yaml
|
||||
# config/ai.yaml
|
||||
providers:
|
||||
openai:
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
base_url: "https://api.openai.com/v1"
|
||||
model: "gpt-4"
|
||||
max_tokens: 4000
|
||||
temperature: 0.7
|
||||
|
||||
anthropic:
|
||||
api_key: ${ANTHROPIC_API_KEY}
|
||||
model: "claude-3-sonnet-20240229"
|
||||
max_tokens: 4000
|
||||
temperature: 0.7
|
||||
|
||||
deepseek:
|
||||
api_key: ${DEEPSEEK_API_KEY}
|
||||
model: "deepseek-chat"
|
||||
max_tokens: 4000
|
||||
temperature: 0.7
|
||||
|
||||
default_provider: "openai"
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
```bash
|
||||
export OPENAI_API_KEY="sk-your-key"
|
||||
export ANTHROPIC_API_KEY="sk-ant-key"
|
||||
export DEEPSEEK_API_KEY="your-key"
|
||||
```
|
||||
|
||||
## 🔄 错误处理
|
||||
|
||||
### 错误类型
|
||||
```rust
|
||||
pub enum AIError {
|
||||
APIError(String), // API 调用失败
|
||||
Timeout, // 请求超时
|
||||
RateLimit, // 达到速率限制
|
||||
InvalidResponse, // 响应格式错误
|
||||
ProviderNotFound, // Provider 不存在
|
||||
ConfigurationError, // 配置错误
|
||||
}
|
||||
```
|
||||
|
||||
### 重试机制
|
||||
```rust
|
||||
let config = RetryConfig {
|
||||
max_attempts: 3,
|
||||
backoff: ExponentialBackoff::from_millis(1000),
|
||||
retryable_errors: vec![
|
||||
AIError::Timeout,
|
||||
AIError::RateLimit,
|
||||
],
|
||||
};
|
||||
|
||||
let response = ai_service.chat_with_retry(request, &config).await?;
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 缓存机制
|
||||
```rust
|
||||
pub struct CachedAIService {
|
||||
inner: AIService,
|
||||
cache: Arc<Mutex<HashMap<String, ChatResponse>>>,
|
||||
}
|
||||
|
||||
// 缓存键生成
|
||||
fn cache_key(request: &ChatRequest) -> String {
|
||||
format!("{:?}-{:?}", request.model, request.messages)
|
||||
}
|
||||
```
|
||||
|
||||
### 连接池
|
||||
```rust
|
||||
pub struct ConnectionPool {
|
||||
connections: HashMap<String, Vec<Client>>,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
pub async fn get_client(&self, provider: &str) -> Result<Client> {
|
||||
// 从连接池获取或创建新连接
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 监控与日志
|
||||
|
||||
### 请求追踪
|
||||
```rust
|
||||
pub struct RequestTracer {
|
||||
request_id: String,
|
||||
start_time: Instant,
|
||||
metrics: RequestMetrics,
|
||||
}
|
||||
|
||||
impl RequestTracer {
|
||||
pub fn log_request(&self, provider: &str, duration: Duration) {
|
||||
metrics.record_request(provider, duration);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 指标收集
|
||||
```rust
|
||||
pub struct RequestMetrics {
|
||||
total_requests: AtomicU64,
|
||||
successful_requests: AtomicU64,
|
||||
failed_requests: AtomicU64,
|
||||
average_duration: AtomicDuration,
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 单元测试
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completion() {
|
||||
let ai_service = AIService::new(test_config());
|
||||
let request = test_request();
|
||||
let response = ai_service.chat_completion(request).await;
|
||||
assert!(response.is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_multiple_providers() {
|
||||
let providers = vec!["openai", "anthropic"];
|
||||
for provider in providers {
|
||||
let ai_service = AIService::new(config_for_provider(provider));
|
||||
let response = test_chat(&ai_service).await;
|
||||
assert!(response.is_ok(), "Provider {} failed", provider);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 最佳实践
|
||||
|
||||
### 1. 错误处理
|
||||
```rust
|
||||
// ✅ 正确
|
||||
match ai_service.chat_completion(request).await {
|
||||
Ok(response) => handle_response(response),
|
||||
Err(AIError::RateLimit) => wait_and_retry(),
|
||||
Err(e) => log_error_and_notify(e),
|
||||
}
|
||||
|
||||
// ❌ 错误 - 忽略错误
|
||||
let _ = ai_service.chat_completion(request).await;
|
||||
```
|
||||
|
||||
### 2. 资源管理
|
||||
```rust
|
||||
// ✅ 正确 - 使用连接池
|
||||
let client = connection_pool.get_client("openai").await?;
|
||||
|
||||
// ❌ 错误 - 每次创建新连接
|
||||
let client = Client::new(config);
|
||||
```
|
||||
|
||||
### 3. 并发控制
|
||||
```rust
|
||||
// ✅ 正确 - 使用信号量
|
||||
let semaphore = Arc::new(Semaphore::new(10));
|
||||
let permit = semaphore.acquire().await?;
|
||||
let response = ai_service.chat_completion(request).await;
|
||||
|
||||
// ❌ 错误 - 无限制并发
|
||||
let handles: Vec<_> = requests.into_iter().map(|req| {
|
||||
tokio::spawn(ai_service.chat_completion(req))
|
||||
}).collect();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**相关文档**:
|
||||
- [df-storage - 存储层](./df-storage-存储层.md)
|
||||
- [df-workflow - 工作流引擎](./df-workflow-工作流引擎.md)
|
||||
- [df-nodes - 节点集合](./df-nodes-节点集合.md)
|
||||
## OpenAI 兼容 Provider(openai_compat.rs)
|
||||
|
||||
支持 OpenAI / DeepSeek / GLM / 本地 Ollama 等任意 OpenAI 兼容端点。
|
||||
|
||||
| 特性 | 实现 |
|
||||
|------|------|
|
||||
| base_url 智能拼接 | `chat_url()` 三分支:已含 `/chat/completions` 直用;以 `/v<数字>` 结尾(如 `/api/paas/v4`,由 `ends_with_version` 判定)补 `/chat/completions`;仅域名(如 `api.openai.com`)补 `/v1/chat/completions` |
|
||||
| 流式 | `stream: true` + SSE 逐 chunk 解析(`apply_openai_sse` 纯函数)|
|
||||
| 工具调用 | tool_calls 按 index 排序(消 HashMap 迭代乱序)|
|
||||
| usage 解析 | `stream_options: {include_usage: true}`,末 chunk 读累计值 |
|
||||
| finish 判定 | 逐 chunk 判 `finish_reason`:`stop`/`tool_calls`/`length` 三者同等视为正常 finished(`length` = max_tokens 截断,属正常终止而非断连)|
|
||||
| embed | POST `/v1/embeddings`,响应按 index 排序返回 `Vec<Vec<f32>>` |
|
||||
| connect timeout | `connect_timeout(30s)`,不设总 timeout(避免误砍流式长任务)|
|
||||
|
||||
> 注:idle timeout(120s)与断连丢弃(finished_received)属上层 `stream_llm`(src-tauri/commands/ai.rs)的职责,不在本 Provider 层。
|
||||
|
||||
SSE 解析抽成 `pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>) -> StreamChunk` 纯函数,与 HTTP/eventsource 解耦,便于单测(喂构造 data 字符串验证 `[DONE]`/usage 覆盖/tool_calls 等分支)。
|
||||
|
||||
---
|
||||
|
||||
## Anthropic Provider(anthropic_compat.rs,Sprint 8)
|
||||
|
||||
| 特性 | 实现 |
|
||||
|------|------|
|
||||
| 认证 | `x-api-key` + `anthropic-version: 2023-06-01` |
|
||||
| 消息格式 | 顶层 `system` 字段,`max_tokens` 必填(请求缺省时兜底 `DEFAULT_MAX_TOKENS = 4096`)|
|
||||
| 流式 SSE | event 类型解析(全集见下)|
|
||||
| 工具调用 | `content_block` type=tool_use |
|
||||
| usage | `message_start` 初始化累加器(input_tokens)、`message_delta` 覆盖 completion(output_tokens 为累计值,非增量)、`message_stop` 经 `take()` 带出累积 usage |
|
||||
| embed | 不支持(Anthropic 无 embed API),继承 trait 默认 Err |
|
||||
|
||||
SSE 事件全集(按 `type` 字段分发):
|
||||
|
||||
| event type | 处理 |
|
||||
|------------|------|
|
||||
| `message_start` | 用 `message.usage.input_tokens` 初始化累加器(output 置 0)|
|
||||
| `message_delta` | `usage.output_tokens` 是累计值(非增量),直接覆盖 completion 并重算 total |
|
||||
| `content_block_delta`(text_delta) | 文本增量 |
|
||||
| `content_block_delta`(input_json_delta) | 工具入参增量(带 index)|
|
||||
| `content_block_start`(tool_use) | 工具块开始,带 id + name |
|
||||
| `message_stop` | 返回 `finished=true` 终态 chunk,usage 经 `take()` 带出 |
|
||||
| `error` | 返回 `finished=true` 终态空 chunk(不清空累加器)|
|
||||
| 其它(content_block_stop / ping)| 空 chunk |
|
||||
|
||||
SSE 解析抽成 `pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUsage>) -> StreamChunk` 纯函数,与 HTTP/eventsource 解耦,便于单测(喂构造 data 字符串验证事件分支与 usage 累积)。
|
||||
|
||||
支持 GLM 订阅端点(`https://open.bigmodel.cn/api/anthropic`)。
|
||||
|
||||
---
|
||||
|
||||
## ContextManager 分组滑动窗口(context.rs,Sprint 11)
|
||||
|
||||
解决长对话无限增长导致 `context_length_exceeded` 死锁。
|
||||
|
||||
### 核心设计
|
||||
|
||||
- **TokenEstimator**:字符粗估,零依赖,保守 ±15%。单条公式 = `content(chars×0.35 ceil)` + `per_message 4` + 每个 `tool_call(per_tool_call 30 + name/arguments 各按 chars×0.35 ceil)` + 有 `tool_call_id` 时 `+3`
|
||||
- **ContextConfig**:max_tokens 128k / output_reserve 8192 / safety 0.85;预算公式 `(max_tokens − output_reserve) × safety_ratio`,由 `ContextConfig::budget_limit()` 提供(`ContextManager::budget_limit()` 仅转发它)
|
||||
- **分组淘汰单元**:工具调用三元组(ToolCallHead + ToolResultTail* + 紧随文本 Assistant)原子性同进同出,防上下文语义断裂
|
||||
- **保护区**:`PROTECT_COUNT = 6`(编译期常量,≈ 最近 2 个完整用户轮次),最后 6 条消息永不裁
|
||||
- **裁剪范围**:仅影响发送视图(`build_for_request`),全量历史(`all_messages_clone`)用于持久化
|
||||
|
||||
### 关键方法
|
||||
|
||||
| 方法 | 用途 |
|
||||
|------|------|
|
||||
| `push(message: ChatMessage)` | 追加消息并计 token、更新 `history_tokens` 缓存(push 不裁剪,裁剪统一在 build_for_request)|
|
||||
| `clear()` | 清空消息历史并重置 token 计数 |
|
||||
| `len() -> usize` | 历史消息条数 |
|
||||
| `is_empty() -> bool` | 历史是否为空 |
|
||||
| `history_tokens() -> u32` | 当前历史累计 token(不含 system prompt)|
|
||||
| `budget_limit() -> u32` | 上下文预算上限,转发 `config.budget_limit()` = (max_tokens − output_reserve) × safety_ratio |
|
||||
| `iter() -> impl Iterator<Item = &ChatMessage>` | 只读迭代历史消息(如标题生成 filter)|
|
||||
| `build_for_request(sys_tokens) -> (Vec<ChatMessage>, bool)` | 返回裁剪后的消息列表 + 是否发生裁剪(用于 LLM 调用)|
|
||||
| `all_messages_clone()` | 返回全量消息(用于 save_conversation / 标题生成)|
|
||||
| `restore_from_messages(msgs)` | 切换对话时重建 ContextManager 缓存 |
|
||||
| `replace_tool_result_content(tool_call_id, new_content) -> bool` | 原子更新工具结果内容(审批通过/拒绝时回填),返回是否找到并替换 |
|
||||
|
||||
---
|
||||
|
||||
## AI 工具注册
|
||||
|
||||
> 归属:`ai_tools.rs` 仅提供基础设施(`RiskLevel` / `AiTool` / `AiToolRegistry`);12 个工具的具体定义与注册在 `src-tauri/src/commands/ai.rs::build_ai_tool_registry`,handler 即唯一执行路径(schema+risk+实现同源)。
|
||||
|
||||
12 个内置工具,按风险分级:
|
||||
|
||||
| 风险 | 工具 |
|
||||
|------|------|
|
||||
| Low(自动执行)| list_projects / list_tasks / list_ideas / read_file / list_directory |
|
||||
| Medium(需审批)| update_project / create_project / create_task / create_idea / write_file |
|
||||
| High(需审批)| delete_project / run_workflow |
|
||||
|
||||
工具执行结果写 `ai_tool_executions` 表(审计日志)。
|
||||
|
||||
---
|
||||
|
||||
## 知识库集成(Sprint 15,逻辑在 src-tauri/commands/ai.rs)
|
||||
|
||||
### 知识注入
|
||||
|
||||
`build_knowledge_context(state, query, config) -> String`:
|
||||
- `config.auto_inject == false` → 立即返回空(零开销)
|
||||
- `hybrid_search()` top-3(LIKE 或 LIKE+向量混合)
|
||||
- 每条命中调 `increment_reuse_count`(fire-and-forget)
|
||||
- 格式化为 markdown,注入 system prompt 头部
|
||||
|
||||
### 知识提炼
|
||||
|
||||
`extract_knowledge_from_conversation(db, conv_id, provider_cfg)`:
|
||||
- 后台 spawn(`tauri::async_runtime::spawn`),提炼失败仅 warn 不阻断
|
||||
- 取最后 6 条 user/assistant 消息 → LLM JSON 输出(强制 JSON schema)
|
||||
- parse 失败整批丢弃;成功则逐条写 candidate
|
||||
|
||||
`maybe_spawn_extraction(...)`:agentic loop 两处正常退出路径统一调用。
|
||||
|
||||
### 向量 embedding(Phase 5.5)
|
||||
|
||||
`generate_embedding(state, text, config) -> Option<Vec<f32>>`:
|
||||
- 截断 8000 字 → 找 `embedding_provider_id` 对应 provider → `embed()`
|
||||
- 失败返回 None(降级 LIKE)
|
||||
|
||||
`hybrid_search(state, query, limit, config)`:三层降级链:
|
||||
1. `vector_enabled == false` → 纯 LIKE
|
||||
2. embed 调用失败 → 纯 LIKE
|
||||
3. 正常 → 双信号合并排序(同时 LIKE+向量 cos≥0.3 > 仅 LIKE > 仅向量 cos≥0.3)
|
||||
|
||||
---
|
||||
|
||||
## LLM 并发控制(state.rs,Sprint 11 Part C)
|
||||
|
||||
`LlmConcurrency`:双层 Semaphore,运行时可调。
|
||||
|
||||
| Semaphore | 默认 permits | 限流对象 |
|
||||
|-----------|------------|---------|
|
||||
| global | 3 | 全部 LLM 调用(stream_llm / 标题 / 提炼)|
|
||||
| per_conv | 2 | 单对话并发 |
|
||||
|
||||
本地工具执行(`tools.execute`)不限流,无外部成本。
|
||||
|
||||
---
|
||||
|
||||
## 决策能力缺口(B 路线,待立项)
|
||||
|
||||
| 能力 | 现状 | 需补 |
|
||||
|------|------|------|
|
||||
| Planning(任务规划)| 缺失 | 意图 → LLM 规划子任务 DAG |
|
||||
| Coordinator(多 agent)| `coordinator.rs::run()` 全 TODO | 实现多 agent 协作拆 DAG |
|
||||
| Conditions(条件分支)| 缺失(df-ai 无实现,条件求值属 df-workflow crate)| JSON Path / 比较 / and-or-not |
|
||||
| Reflection(自纠)| 缺失 | 执行后自检 / 重试 |
|
||||
|
||||
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划.md)。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [df-storage 存储层](./df-storage-存储层.md)
|
||||
- [df-workflow 工作流引擎](./df-workflow-工作流引擎.md)
|
||||
- [df-nodes 节点集合](./df-nodes-节点集合.md)
|
||||
|
||||
135
docs/03-模块文档/df-knowledge-知识库.md
Normal file
135
docs/03-模块文档/df-knowledge-知识库.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 知识库模块
|
||||
|
||||
> 创建: 2026-06-13 | 阶段: Tier 1 已实现
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
知识库是 DevFlow 的"共享记忆层",被动积累 AI 对话中产生的可复用经验,供后续对话注入使用。外部工具(Claude Code / CodeX / Cursor)可通过相同 IPC 接口读写,内外零差异。
|
||||
|
||||
---
|
||||
|
||||
## 实现状态
|
||||
|
||||
| 功能 | 状态 |
|
||||
|------|------|
|
||||
| candidate→published 状态机 | ✅ Tier 1 |
|
||||
| 手动录入(Knowledge.vue)| ✅ Tier 1 |
|
||||
| AI 自动提炼(对话完成后)| ✅ Tier 1 |
|
||||
| LIKE 关键词检索 + 注入 system prompt | ✅ Tier 1 |
|
||||
| 审核收件箱(人工门控)| ✅ Tier 1 |
|
||||
| 向量 embedding + 混合检索 | ✅ Phase 5.5(开关控制,默认关)|
|
||||
| ai_node prompt 注入 | ⬜ Tier 2 |
|
||||
| MCP 对外 API | ⬜ Tier 2 |
|
||||
|
||||
---
|
||||
|
||||
## 状态机
|
||||
|
||||
```
|
||||
candidate ──→ pending_review ──→ published ──→ archived
|
||||
│ │ │
|
||||
└────────────────┴───────────────┘
|
||||
(可直接到 archived)
|
||||
```
|
||||
|
||||
- AI 提炼只产 **candidate**,绝不自动 published(人工门控)
|
||||
- `knowledge_archive`:软删除(status=archived),不物理删除
|
||||
|
||||
---
|
||||
|
||||
## 知识类型(KnowledgeKind)
|
||||
|
||||
7 种:`pitfall`(踩坑)/ `review_rule`(审查规则)/ `prompt_template`(Prompt 模板)/ `architecture_pattern`(架构模式)/ `diagnosis`(诊断知识)/ `deployment_note`(部署经验)/ `workflow_optimization`(工作流优化)
|
||||
|
||||
---
|
||||
|
||||
## IPC 命令(11 个)
|
||||
|
||||
| Command | 说明 |
|
||||
|---------|------|
|
||||
| `knowledge_list(status?)` | 全量列表,默认排除 archived |
|
||||
| `knowledge_get(id)` | 单条查询 |
|
||||
| `knowledge_search(query, kind?, limit?)` | LIKE 检索,top-N≤3 |
|
||||
| `knowledge_create(input)` | 创建(status=candidate)|
|
||||
| `knowledge_update_status(id, status)` | 状态转换(含合法矩阵校验)|
|
||||
| `knowledge_record_reuse(id)` | reuse_count +1 |
|
||||
| `knowledge_list_candidates()` | 审核收件箱(按 confidence 排序)|
|
||||
| `knowledge_archive(id)` | 软删除 |
|
||||
| `knowledge_get_config()` | 读取 KnowledgeConfig |
|
||||
| `knowledge_save_config(config)` | 保存 KnowledgeConfig |
|
||||
| `knowledge_extract_now()` | 手动触发提炼(ManualOnly 模式)|
|
||||
|
||||
---
|
||||
|
||||
## KnowledgeConfig
|
||||
|
||||
```rust
|
||||
pub struct KnowledgeConfig {
|
||||
pub auto_extract: bool, // 提炼总开关,默认 true
|
||||
pub trigger_mode: ExtractTrigger, // on_complete | on_idle | manual_only
|
||||
pub min_messages: u32, // 守卫:最少消息数,默认 4
|
||||
pub idle_timeout_ms: u64, // 闲置触发超时,默认 30000
|
||||
pub auto_inject: bool, // 聊天注入开关,默认 true
|
||||
pub vector_enabled: bool, // 向量检索开关,默认 false
|
||||
pub embedding_provider_id: Option<String>, // 仅 openai_compat 类型
|
||||
pub embedding_model: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
存储:`AppState.knowledge_config: Arc<Mutex<KnowledgeConfig>>`(内存,重启恢复默认值)。
|
||||
|
||||
---
|
||||
|
||||
## 检索与注入
|
||||
|
||||
### LIKE 检索(默认)
|
||||
|
||||
`search(query, kind, limit=3)` → `WHERE title LIKE ? OR content LIKE ?` → `ORDER BY reuse_count DESC`
|
||||
|
||||
### 混合检索(vector_enabled=true)
|
||||
|
||||
三层降级链:
|
||||
1. 开关关 → 纯 LIKE
|
||||
2. embed 调用失败 → 纯 LIKE
|
||||
3. 正常 → 双信号排序(同时命中 LIKE+向量 cos≥0.3 > 仅 LIKE > 仅向量 cos≥0.3)
|
||||
|
||||
嵌入时机:知识**发布时**(不在 candidate 阶段浪费 embed 调用),`spawn_embedding_for_knowledge` fire-and-forget。
|
||||
|
||||
### 注入位置
|
||||
|
||||
system prompt 头部([知识库上下文] --- [技能指令] --- [原始 system prompt]),仅 auto_inject=true 时生效。
|
||||
|
||||
---
|
||||
|
||||
## AI 自动提炼流程
|
||||
|
||||
1. `run_agentic_loop` 正常退出 → `maybe_spawn_extraction()` 守卫检查
|
||||
2. 守卫:`auto_extract=true` + `messages.len() >= min_messages`
|
||||
3. `tauri::async_runtime::spawn` 后台执行,不 await(不阻断聊天)
|
||||
4. 取最后 6 条 user/assistant 消息 → 构造 JSON schema prompt → LLM `complete()`
|
||||
5. `serde_json::from_str<Vec<ExtractedItem>>` 解析,失败整批丢弃(warn 不报错)
|
||||
6. 逐条写 `knowledges`(status=candidate,source_ref="conv:{id}")
|
||||
|
||||
---
|
||||
|
||||
## 矛盾知识处理
|
||||
|
||||
不做结构层消歧,在内容和 tags 中自述限制范围。检索时两条知识都可能返回,由 LLM 上下文理解取舍。
|
||||
|
||||
---
|
||||
|
||||
## 外部工具访问(规划)
|
||||
|
||||
Tier 1+ 目标:MCP Shell 封装(`mcp-server` 转发 IPC),外部工具使用逻辑与内部零差异:
|
||||
- 外部写入:走 candidate → 人工审核流程
|
||||
- 外部读取:`knowledge_search` / `knowledge_list`(published)
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [df-storage 存储层](./df-storage-存储层.md) — knowledges 表结构 + 向量工具函数
|
||||
- [df-ai AI 集成模块](./df-ai-AI集成模块.md) — hybrid_search / generate_embedding / extract
|
||||
- [功能决策记录](../02-架构设计/功能决策记录.md) — 检索方案演进决策
|
||||
@@ -1,52 +1,140 @@
|
||||
# df-storage 存储层
|
||||
|
||||
> 创建: 2026-06-10 | 状态: 初稿
|
||||
> 创建: 2026-06-10 | 最后更新: 2026-06-13
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
df-storage 是 DevFlow 的数据持久化层,基于 SQLite (rusqlite),负责连接管理、Schema 迁移和 CRUD 操作。
|
||||
df-storage 是 DevFlow 的数据持久化层,基于 SQLite (rusqlite),负责连接管理、Schema 迁移(V1-V8)和 CRUD 操作。全部 Repo 由 `impl_repo!` 宏自动生成。
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
| 功能 | 状态 |
|
||||
|------|------|
|
||||
| SQLite 连接管理 | ✅ 已实现 |
|
||||
| Schema 迁移 (6 张表) | ✅ 已实现 |
|
||||
| CRUD 操作 | ⬜ 待实施 |
|
||||
| 事务支持 | ⬜ 待实施 |
|
||||
| SQLite 连接管理 | ✅ |
|
||||
| Schema 迁移 V1-V8 | ✅ |
|
||||
| impl_repo! 宏 CRUD | ✅ |
|
||||
| KnowledgeRepo(含向量)| ✅ Sprint 15 |
|
||||
| 事务支持 | ⬜ 按需 |
|
||||
|
||||
## 数据表
|
||||
---
|
||||
|
||||
Phase 1 已创建的 6 张核心表:
|
||||
## 数据表(V1-V8 迁移历史)
|
||||
|
||||
1. **ideas** — 想法池
|
||||
2. **projects** — 项目
|
||||
3. **tasks** — 任务
|
||||
4. **workflow_defs** — 工作流定义
|
||||
5. **workflow_runs** — 工作流执行
|
||||
6. **artifacts** — 产出物
|
||||
| 版本 | 新增/变更 |
|
||||
|------|-----------|
|
||||
| V1 | ideas / projects / tasks / releases / workflow_executions / node_executions(6 张基础表 + 4 索引)|
|
||||
| V2 | ideas 加 promoted_to/ai_analysis/scores;tasks 加 workflow_def_id/base_branch;workflow_executions 加 project_id/task_id;新建 branches 表(含 2 索引)|
|
||||
| V3 | ai_providers / ai_conversations / ai_tool_executions(AI 功能 3 张表)|
|
||||
| V4 | 幂等补列:ai_conversations.archived(PRAGMA 探测,兼容坏库)|
|
||||
| V5 | 幂等补列:ai_conversations.prompt_tokens / completion_tokens / model / models(Token 用量)|
|
||||
| V6 | 幂等补列:ai_conversations.skill(技能注入)|
|
||||
| V7 | 新建 **knowledges** 表(Sprint 15)|
|
||||
| V8 | 幂等补列:knowledges.embedding BLOB(Phase 5.5 向量检索)|
|
||||
|
||||
完整表结构见 `ARCHITECTURE.md` 数据模型章节。
|
||||
|
||||
## 依赖关系
|
||||
### knowledges 表(V7)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS knowledges (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL DEFAULT 'pitfall',
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
tags TEXT, -- JSON array string
|
||||
status TEXT NOT NULL DEFAULT 'candidate',
|
||||
confidence TEXT, -- 'high'|'medium'|'low'
|
||||
reuse_count INTEGER NOT NULL DEFAULT 0,
|
||||
verified INTEGER NOT NULL DEFAULT 0, -- 0/1
|
||||
source_project TEXT,
|
||||
source_ref TEXT,
|
||||
created_at TEXT NOT NULL, -- 毫秒字符串
|
||||
updated_at TEXT NOT NULL,
|
||||
embedding BLOB -- V8 补列,f32 little-endian
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_status ON knowledges(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_kind ON knowledges(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledges_reuse_count ON knowledges(reuse_count DESC);
|
||||
```
|
||||
df-core (错误类型、ID 生成)
|
||||
← df-storage
|
||||
|
||||
---
|
||||
|
||||
## impl_repo! 宏
|
||||
|
||||
自动生成以下方法:`insert` / `get_by_id` / `list_all` / `query` / `update_field` / `update_full` / `delete`
|
||||
|
||||
列名白名单(ALLOWED_COLUMNS)防 SQL 注入,各 Repo 声明各自允许的列。
|
||||
|
||||
### 已注册 Repo 列表
|
||||
|
||||
| Repo | 表 |
|
||||
|------|----|
|
||||
| IdeaRepo | ideas |
|
||||
| ProjectRepo | projects |
|
||||
| TaskRepo | tasks |
|
||||
| ReleaseRepo | releases |
|
||||
| WorkflowRepo | workflow_executions |
|
||||
| NodeExecutionRepo | node_executions |
|
||||
| BranchRepo | branches |
|
||||
| AiProviderRepo | ai_providers |
|
||||
| AiConversationRepo | ai_conversations |
|
||||
| AiToolExecutionRepo | ai_tool_executions |
|
||||
| **KnowledgeRepo** | **knowledges** |
|
||||
|
||||
---
|
||||
|
||||
## KnowledgeRepo 自定义方法(Sprint 15)
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `search(query, kind?, limit)` | LIKE 双分支(有/无 kind 过滤),均含 `WHERE status='published'`,`ORDER BY reuse_count DESC LIMIT ?`,top-N≤3 用于注入 |
|
||||
| `list_by_status(status)` | CASE WHEN confidence 语义排序(High→Medium→Low),用于审核收件箱 |
|
||||
| `increment_reuse_count(id)` | `UPDATE SET reuse_count = reuse_count + 1, updated_at = ?`(SQL 原子操作,连带刷 updated_at)|
|
||||
| `top_used(limit)` | published 按 reuse_count DESC,热门列表 |
|
||||
| `set_embedding(id, &[f32])` | UPDATE embedding BLOB(f32 little-endian)|
|
||||
| `search_vector(query_vec, limit)` | SELECT published + embedding IS NOT NULL → 纯 Rust 余弦批量比较,skip 维度不匹配 |
|
||||
| `list_non_archived()` | `WHERE status != 'archived'` 全量,CASE confidence 语义排序(high>medium>low),次 created_at DESC → `Vec<KnowledgeRecord>` |
|
||||
|
||||
### 向量工具函数
|
||||
|
||||
```rust
|
||||
fn f32s_to_blob(v: &[f32]) -> Vec<u8> // f32 → little-endian bytes
|
||||
fn blob_to_f32s(b: &[u8]) -> Vec<f32> // bytes → f32(chunks_exact(4),尾部非 4 倍数残字节截断丢弃)
|
||||
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 // 点积 / (‖a‖‖b‖ + 1e-8 防零除),零向量返回有限值
|
||||
```
|
||||
|
||||
> **不引入 sqlite-vec**:规避 Windows MSVC 下编译 C 扩展的风险。纯 Rust 实现,零外部 C 依赖。
|
||||
|
||||
---
|
||||
|
||||
## 迁移幂等设计
|
||||
|
||||
迁移机制:`schema_version` 表记录当前版本,`MIGRATION_VERSION` 常量为目标版本,`run()` 按 `current_version < N` 顺序应用各版本。
|
||||
|
||||
### v4 解法:PRAGMA 探测列存在性
|
||||
|
||||
关键列补建不依赖版本号,用 `PRAGMA table_info(<table>)` 探测实际 schema,缺列才 `ALTER TABLE ADD COLUMN`。
|
||||
|
||||
- 对新库(列已由建表带入)、老库(DDL 正常生效)、坏库(版本号已写入但 DDL 漏生效)三种情况都安全幂等。
|
||||
- V5/V6/V8 均复用此模式。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
crates/df-storage/src/
|
||||
├── lib.rs — 模块入口,导出公共 API
|
||||
├── connection.rs — SQLite 连接管理
|
||||
├── schema.rs — Schema 定义与迁移
|
||||
└── crud.rs — CRUD 操作 (待创建)
|
||||
├── lib.rs — 模块入口,导出公共 API
|
||||
├── db.rs — SQLite 连接管理(Database struct)
|
||||
├── migrations.rs — V1-V8 迁移逻辑,MIGRATION_VERSION=8
|
||||
├── models.rs — 全部 *Record struct(含 KnowledgeRecord)
|
||||
└── crud.rs — impl_repo! 宏 + 全部 Repo(含 KnowledgeRepo)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [SQLite CRUD 模式](../01-技术文档/SQLite-CRUD模式.md)
|
||||
|
||||
@@ -15,43 +15,171 @@ df-workflow 是 DevFlow 的核心引擎,负责 DAG 定义、拓扑排序、节
|
||||
| DAG 数据结构 | ✅ 已实现 |
|
||||
| 拓扑排序 | ✅ 已实现 |
|
||||
| DagExecutor (顺序执行) | ✅ 已实现 |
|
||||
| 同层节点并行执行 | ⬜ 有 TODO 注释 |
|
||||
| 同层节点并行执行 | ✅ 已实现 |
|
||||
| Node trait 定义 | ✅ 已实现 |
|
||||
| 状态机 (WorkflowRunStatus) | ✅ 已实现 |
|
||||
| EventBus (broadcast) | ✅ 已实现 |
|
||||
| 条件表达式引擎 | ⚡ 仅支持 true/false |
|
||||
| 断点续跑 | ⬜ 待实施 |
|
||||
| 断点续跑 | ⬜ 待实施(引擎整体无暂停/恢复/快照机制,缺乏底层基础设施支撑) |
|
||||
|
||||
## 核心设计
|
||||
|
||||
### Node trait
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait Node: Send + Sync {
|
||||
fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput>;
|
||||
async fn execute(&self, ctx: NodeContext) -> NodeResult;
|
||||
fn schema(&self) -> NodeSchema;
|
||||
fn is_blocking(&self) -> bool { true }
|
||||
fn is_blocking(&self) -> bool { false }
|
||||
fn node_type(&self) -> &str;
|
||||
}
|
||||
|
||||
// NodeResult = anyhow::Result<NodeOutput> 的别名
|
||||
// ctx 按值传递(非引用),由 executor 在每层构建
|
||||
```
|
||||
|
||||
### NodeContext / NodeOutput(node.rs)
|
||||
|
||||
执行上下文与输出结构,由 executor 在每层为每个节点构建。
|
||||
|
||||
**NodeContext 字段**:
|
||||
|
||||
| 字段 | 类型 | 职责 |
|
||||
|------|------|------|
|
||||
| `node_id` | `NodeId` | 当前节点 ID |
|
||||
| `inputs` | `HashMap<String, NodeOutput>` | 上游节点输出,key 为上游节点 ID |
|
||||
| `config` | `serde_json::Value` | 节点配置参数 |
|
||||
| `execution_id` | `String` | 工作流执行 ID |
|
||||
| `event_bus` | `EventBus` | 事件总线(可 Clone,广播状态变更) |
|
||||
| `node_status` | `StateMachine` | 节点状态机(用于检查取消状态) |
|
||||
|
||||
**NodeOutput 字段与构造器**:
|
||||
|
||||
| 成员 | 签名 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | `serde_json::Value` | 输出数据 |
|
||||
| `metadata` | `HashMap<String, String>` | 输出元数据 |
|
||||
| `empty()` | `() -> Self` | 空输出(`data = Null`,空 metadata) |
|
||||
| `from_value(data)` | `(Value) -> Self` | 从 JSON 值构造(空 metadata) |
|
||||
|
||||
> `NodeOutput` derive `Debug/Clone/Serialize/Deserialize`;`NodeContext` 仅 `Debug/Clone`(`StateMachine` 非 Serialize)。
|
||||
|
||||
### 节点注册机制(NodeRegistry)
|
||||
|
||||
节点工厂注册表,据 `DagDef.node_type` 字符串创建 `Box<dyn Node>` 实例,桥接可序列化定义与运行时 trait object。
|
||||
|
||||
| 方法 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `new` | `() -> Self` | 创建空注册表 |
|
||||
| `register` | `(&mut self, type_name: &str, factory: F)` | 注册一个节点工厂(`F: Fn(&Value) -> Box<dyn Node>`) |
|
||||
| `create` | `(&self, type_name: &str, config: &Value) -> Result<Box<dyn Node>>` | 按类型名 + 配置创建节点实例,未注册则报错 |
|
||||
| `build_dag` | `(&self, def: &DagDef) -> Result<Dag>` | 从 `DagDef` 构建完整运行时 `Dag`(建节点 + 加边,自动分发条件边) |
|
||||
| `is_registered` | `(&self, type_name: &str) -> bool` | 检查类型是否已注册 |
|
||||
| `registered_types` | `(&self) -> Vec<&str>` | 列出所有已注册类型名 |
|
||||
|
||||
> `Default` 实现仅注册占位 `script` 工厂(`unimplemented!`),实际 `ScriptNode` 由 `df-nodes` crate 注册。
|
||||
|
||||
### DAG 执行流程
|
||||
|
||||
```
|
||||
1. 接收 WorkflowDef (DAG 定义)
|
||||
2. 拓扑排序 → 得到执行层 (layers)
|
||||
3. 逐层执行:
|
||||
- 同层节点并行 (TODO)
|
||||
- 阻塞节点等待人工操作
|
||||
- 非阻塞节点异步完成
|
||||
- 同层节点并行(`futures::future::join_all`,已实现)
|
||||
- 阻塞/非阻塞节点当前同等异步执行(`is_blocking` 未被 executor 消费,人工等待逻辑规划中、当前未实现)
|
||||
4. 状态变更通过 EventBus 广播
|
||||
5. 每个节点完成后持久化快照
|
||||
5. 节点完成后仅更新内存态(`StateMachine` + `outputs` HashMap),无持久化快照(规划中)
|
||||
```
|
||||
|
||||
### DAG 定义序列化(dag_def.rs)
|
||||
|
||||
区分两层表示:运行时 `Dag` 持 `Box<dyn Node>`(trait object,不可序列化);可持久化的 `DagDef` / `NodeDef` / `EdgeDef` 均 `#[derive(Serialize, Deserialize)]`,用于模板与存盘。`NodeRegistry::build_dag` 负责从 `DagDef` 还原运行时 `Dag`。
|
||||
|
||||
| 方法 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `DagDef::new` | `() -> Self` | 空定义 |
|
||||
| `DagDef::add_node` | `(&mut self, id, node_type, config: Value)` | 加节点定义(label 默认 None) |
|
||||
| `DagDef::add_edge` | `(&mut self, source, target)` | 加普通边(condition=None) |
|
||||
| `add_edge_with_condition` | `(&mut self, source, target, condition)` | 加带条件表达式的边 |
|
||||
| `from_dag_edges` | `(&dag: &Dag) -> Self` | 从运行时 `Dag` 反推定义;**注意**只能还原边的 condition 与节点的 `node_type`,`config` 一律填 `Value::Null`(无法从 trait object 反推) |
|
||||
|
||||
### 事件类型
|
||||
|
||||
- `WorkflowStarted` / `WorkflowCompleted` / `WorkflowFailed`
|
||||
事件枚举定义在 `df-core::events::WorkflowEvent`,`DagExecutor` 通过 `EventBus::send` 广播。
|
||||
|
||||
**执行器实际广播的(executor.rs)**:
|
||||
|
||||
- `NodeStarted` / `NodeCompleted` / `NodeFailed`
|
||||
- `WorkflowPaused` / `WorkflowResumed`
|
||||
- `WorkflowCompleted`
|
||||
|
||||
**枚举已定义但执行器当前未触发**(`df-core::events` 中存在,DagExecutor 不发):
|
||||
|
||||
- `NodeProgress`(节点进度)
|
||||
- `NodeOutput`(节点输出流)
|
||||
- `WorkflowPaused`(暂停等待外部输入)
|
||||
- `WorkflowFailed`(工作流失败,带 failed_node)
|
||||
- `HumanApprovalRequest` / `HumanApprovalResponse`(人工审批)
|
||||
|
||||
> 注:枚举中无 `WorkflowStarted` / `WorkflowResumed`,旧文档所述为误。
|
||||
|
||||
### EventBus(eventbus.rs)
|
||||
|
||||
基于 `tokio::sync::broadcast` 的发布/订阅,内部持 `broadcast::Sender<WorkflowEvent>`。
|
||||
|
||||
| 方法/成员 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `DEFAULT_CAPACITY` | `const usize = 256` | 默认通道容量 |
|
||||
| `new` | `() -> Self` | 用默认容量建总线 |
|
||||
| `with_capacity` | `(usize) -> Self` | 指定容量建总线 |
|
||||
| `send` | `(&self, WorkflowEvent) -> ()` | 广播事件(忽略接收者已关闭错误,异步) |
|
||||
| `subscribe` | `(&self) -> broadcast::Receiver<WorkflowEvent>` | 订阅事件流 |
|
||||
| `emit_human_approval_request` | `(&self, WorkflowEvent) -> Result<usize, SendError>` | 发送人工审批请求(返回接收者计数) |
|
||||
| `try_recv_human_approval` | `(&self, execution_id, node_id) -> Option<HumanApprovalResponse>` | **TODO 占位**:当前恒返回 `None`,审批响应存储/检索未实现,需配合前端 |
|
||||
| `Default` / `Clone` | — | `Default` 走 `new`;`Clone` 复刻 `sender`(broadcast sender 可 clone,共享通道) |
|
||||
|
||||
> `emit_human_approval_request` 与 `try_recv_human_approval` 为人工审批占位接口,后者**未实现**,执行器当前不消费审批响应(对应 `is_blocking` 等待逻辑亦未落地)。
|
||||
|
||||
### 状态机(state.rs)
|
||||
|
||||
`StateMachine` 维护 `HashMap<NodeId, NodeStatus>`,校验节点状态转换,非法转换返回错误。
|
||||
|
||||
**合法转换链**(`is_legal`):`Pending → Running`,`Running → Completed`,`Running → Failed`。其余均拒绝。
|
||||
|
||||
| 方法 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `new` / `get` | `... -> Self` / `(&NodeId) -> NodeStatus` | 创建;取状态(缺失默认 `Pending`) |
|
||||
| `set_running` | `(&mut self, NodeId) -> Result<()>` | `Pending → Running` |
|
||||
| `set_completed` | `(&mut self, NodeId) -> Result<()>` | `Running → Completed` |
|
||||
| `set_failed` | `(&mut self, NodeId) -> Result<()>` | `Running → Failed` |
|
||||
| `set_waiting` | `(&mut self, NodeId)` | 设为 `Waiting`,**绕过转换校验**(直接 set) |
|
||||
| `set_skipped` | `(&mut self, NodeId)` | 设为 `Skipped`,**绕过转换校验** |
|
||||
| `is_cancelled` | `(&NodeId) -> bool` | 是否为 `Cancelled`(同样无对应 setter,外部直接 set) |
|
||||
| `snapshot` | `() -> &HashMap<NodeId, NodeStatus>` | 全量状态快照引用 |
|
||||
|
||||
> `Waiting` / `Skipped` / `Cancelled` 三态暂未纳入 `is_legal` 校验链,对应的 `set_*` 直接 `insert`,可从任意态跳转。
|
||||
|
||||
### Dag 对外 API(dag.rs)
|
||||
|
||||
| 方法 | 签名 | 职责 |
|
||||
|------|------|------|
|
||||
| `new` | `() -> Self` | 空 DAG |
|
||||
| `add_node` | `(&mut self, id: NodeId, node: Box<dyn Node>)` | 加节点 |
|
||||
| `add_edge` | `(&mut self, source, target)` | 加普通边(condition=None) |
|
||||
| `add_edge_with_condition` | `(&mut self, source, target, condition: String)` | 加带条件边 |
|
||||
| `predecessors` | `(&NodeId) -> Vec<NodeId>` | 上游节点 ID |
|
||||
| `successors` | `(&NodeId) -> Vec<NodeId>` | 下游节点 ID |
|
||||
| `topological_layers` | `() -> Result<Vec<Vec<NodeId>>>` | BFS 分层拓扑排序,同层可并行;**检测到环时报 `Workflow` 错误**("DAG 中存在环") |
|
||||
|
||||
`Edge { source, target, condition: Option<String> }` — condition 为可选条件表达式,由 `conditions.rs` 求值。
|
||||
|
||||
### 条件表达式引擎(conditions.rs)
|
||||
|
||||
`ConditionEngine::evaluate(expr: &str, context: &Value) -> Result<bool>` — 仅支持 `"true"` / `"false"` 字面量(区分大小写,先 `trim()` 去空白)。
|
||||
|
||||
**默认放行(安全风险)**:空串、`"True"`/`"FALSE"` 等大小写不匹配字面量、以及任意非 `true`/`false` 字面量(如 `"yes"`、`"1"`、`"$.status == 'completed'"`)均回退为 `Ok(true)` 并 `tracing::warn!`。即**条件不匹配时放行而非阻断**,DAG 边全通 —— 无法据上游输出做条件分支。
|
||||
|
||||
> 现状:JSON Path、比较运算、`contains`、逻辑组合均未实现(`context` 参数当前未被使用,仅占位对齐签名)。详见 [B 路线决策接入需求](#🔮-决策能力接入需求b-路线)。
|
||||
|
||||
## 依赖关系
|
||||
|
||||
@@ -66,15 +194,33 @@ df-core (类型、事件、错误)
|
||||
|
||||
```
|
||||
crates/df-workflow/src/
|
||||
├── lib.rs — 模块入口
|
||||
├── dag.rs — DAG 数据结构与拓扑排序
|
||||
├── executor.rs — DagExecutor
|
||||
├── node.rs — Node trait + NodeContext/Output
|
||||
├── state.rs — 状态机
|
||||
├── event.rs — EventBus
|
||||
└── condition.rs — 条件表达式引擎
|
||||
├── lib.rs — 模块入口
|
||||
├── dag.rs — DAG 数据结构与拓扑排序
|
||||
├── dag_def.rs — 可序列化 DAG 定义(DagDef/NodeDef/EdgeDef)
|
||||
├── executor.rs — DagExecutor
|
||||
├── node.rs — Node trait + NodeContext/Output
|
||||
├── state.rs — 状态机
|
||||
├── eventbus.rs — EventBus(broadcast)
|
||||
├── registry.rs — NodeRegistry(节点类型注册表)
|
||||
└── conditions.rs — 条件表达式引擎
|
||||
```
|
||||
|
||||
## 🔮 决策能力接入需求(B 路线)
|
||||
|
||||
> 2026-06-12 记录。executor 分层并行已具骨架,conditions 为最大空壳。
|
||||
|
||||
AI Chat(B 路线)从单链 ReAct 升级为规划式协作时,本引擎需补两处:
|
||||
|
||||
1. **`condition.rs::ConditionEngine::evaluate()`** —— 当前只认 `"true"` / `"false"` 字面量,默认 `Ok(true)`,DAG 边全通,无法据 AI 输出做条件分支。需补:
|
||||
- JSON Path 取值(从上游节点输出读字段)
|
||||
- 比较运算(`==` `!=` `>` `<` `>=` `<=`)
|
||||
- `contains` / 字符串匹配
|
||||
- `and` / `or` / `not` 组合
|
||||
|
||||
2. **executor 分层并行接入 agentic loop** —— `topological_layers` + `join_all` 已实现(测试 `test_same_layer_runs_in_parallel`),但仅喂静态 DAG。需让 `run_agentic_loop` 内动态生成的 DAG 走这条并行通道,而非单轮串行 `process_tool_calls`。
|
||||
|
||||
详见 [Phase 2 计划 - 决策能力升级](../07-项目管理/Phase2计划.md)。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [df-nodes 节点集合](./df-nodes-节点集合.md)
|
||||
|
||||
Reference in New Issue
Block a user