新增: 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:
@@ -4,12 +4,14 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::sync::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::ai_tools::AiToolRegistry;
|
||||
use df_storage::crud::{
|
||||
AiConversationRepo, AiProviderRepo, AiToolExecutionRepo, IdeaRepo, NodeExecutionRepo,
|
||||
ProjectRepo, ReleaseRepo, TaskRepo, WorkflowRepo,
|
||||
AiConversationRepo, AiProviderRepo, AiToolExecutionRepo, IdeaRepo, KnowledgeEventsRepo,
|
||||
KnowledgeRepo, NodeExecutionRepo, ProjectRepo, ReleaseRepo, SettingsRepo, TaskRepo,
|
||||
WorkflowRepo,
|
||||
};
|
||||
use df_storage::db::Database;
|
||||
use df_workflow::eventbus::EventBus;
|
||||
@@ -17,6 +19,116 @@ use df_workflow::registry::NodeRegistry;
|
||||
|
||||
use crate::commands::ai::AiSession;
|
||||
|
||||
// ============================================================
|
||||
// 知识库配置(提取 + 注入)
|
||||
// ============================================================
|
||||
|
||||
/// AI 提炼触发方式
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExtractTrigger {
|
||||
/// 对话正常完成时(默认)
|
||||
OnComplete,
|
||||
/// 对话闲置 N 秒后
|
||||
OnIdle,
|
||||
/// 仅手动按钮触发
|
||||
ManualOnly,
|
||||
}
|
||||
|
||||
/// 知识库行为配置(存 AppState 内存,前后端通过 IPC 读写)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KnowledgeConfig {
|
||||
/// 提炼总开关,默认 true
|
||||
pub auto_extract: bool,
|
||||
/// 提炼触发方式,默认 OnComplete
|
||||
pub trigger_mode: ExtractTrigger,
|
||||
/// 最少消息数守卫(防闲聊噪音),默认 4
|
||||
pub min_messages: u32,
|
||||
/// 闲置触发超时(ms),默认 30000
|
||||
pub idle_timeout_ms: u64,
|
||||
/// 聊天时自动注入相关知识开关,默认 true
|
||||
pub auto_inject: bool,
|
||||
/// 语义检索(向量)总开关,默认 false——关闭时纯 LIKE 零外部依赖
|
||||
#[serde(default)]
|
||||
pub vector_enabled: bool,
|
||||
/// embedding 用的 provider id(仅 openai_compat 类型,Anthropic 无 embed API)
|
||||
#[serde(default)]
|
||||
pub embedding_provider_id: Option<String>,
|
||||
/// embedding 模型名(如 embedding-3 / text-embedding-3-small)
|
||||
#[serde(default)]
|
||||
pub embedding_model: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for KnowledgeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_extract: true,
|
||||
trigger_mode: ExtractTrigger::OnComplete,
|
||||
min_messages: 4,
|
||||
idle_timeout_ms: 30_000,
|
||||
auto_inject: true,
|
||||
vector_enabled: false,
|
||||
embedding_provider_id: None,
|
||||
embedding_model: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LLM 调用并发控制(双层 Semaphore)
|
||||
// ============================================================
|
||||
|
||||
/// LLM 调用并发控制 — 全局 + 单对话双层 Semaphore
|
||||
///
|
||||
/// 限流对象:所有真实 LLM 调用(主循环 stream_llm / 标题生成 / 知识提炼)。
|
||||
/// 不限流本地工具执行(tools.execute)——本地操作无外部成本、不受 RPM 约束。
|
||||
///
|
||||
/// 运行时调整:tokio Semaphore 的 permits 数构造时固定、不可增减,
|
||||
/// 故用 `Arc<Mutex<Arc<Semaphore>>>` 双层包装——替换内层 Arc 即重建 Semaphore。
|
||||
/// 已持有旧 permit 的任务不受影响(permit 绑定旧 Semaphore,软收敛),
|
||||
/// 新请求 lock 后克隆到最新 Arc、自动走新限制。旧 Semaphore 随最后 permit 释放而 drop。
|
||||
///
|
||||
/// 注意:per_conv 当前是应用级单一信号量(非 per-conv map)。因 AiSession 为单例 +
|
||||
/// generating 互斥,同一时刻仅一个对话的 loop 在跑,per_conv 退化为"单对话内并发"
|
||||
/// (主循环 stream_llm + 标题生成 + 知识提炼)。未来若支持多对话并发,
|
||||
/// 需改为 HashMap<conv_id, Semaphore>。
|
||||
#[derive(Clone)]
|
||||
pub struct LlmConcurrency {
|
||||
global: Arc<Mutex<Arc<Semaphore>>>,
|
||||
per_conv: Arc<Mutex<Arc<Semaphore>>>,
|
||||
}
|
||||
|
||||
impl LlmConcurrency {
|
||||
pub fn new(global: usize, per_conv: usize) -> Self {
|
||||
Self {
|
||||
global: Arc::new(Mutex::new(Arc::new(Semaphore::new(global)))),
|
||||
per_conv: Arc::new(Mutex::new(Arc::new(Semaphore::new(per_conv)))),
|
||||
}
|
||||
}
|
||||
|
||||
/// 取全局并发 permit(重建后新请求自动走最新 Semaphore)
|
||||
pub async fn acquire_global(&self) -> tokio::sync::OwnedSemaphorePermit {
|
||||
let sema = self.global.lock().await.clone();
|
||||
sema.acquire_owned().await.expect("llm global semaphore closed")
|
||||
}
|
||||
|
||||
/// 取单对话并发 permit
|
||||
pub async fn acquire_per_conv(&self) -> tokio::sync::OwnedSemaphorePermit {
|
||||
let sema = self.per_conv.lock().await.clone();
|
||||
sema.acquire_owned().await.expect("llm per_conv semaphore closed")
|
||||
}
|
||||
|
||||
/// 重建全局 Semaphore(软收敛:旧 permit 不回收,待其释放后新限制完全生效)
|
||||
pub async fn set_global(&self, permits: usize) {
|
||||
*self.global.lock().await = Arc::new(Semaphore::new(permits));
|
||||
}
|
||||
|
||||
/// 重建单对话 Semaphore
|
||||
pub async fn set_per_conv(&self, permits: usize) {
|
||||
*self.per_conv.lock().await = Arc::new(Semaphore::new(permits));
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用全局状态 — 通过 `app.manage()` 注入,command 中以 `State<'_, AppState>` 取用
|
||||
pub struct AppState {
|
||||
/// 数据库句柄(Arc 包装,便于在异步任务中重建 Repo)
|
||||
@@ -48,14 +160,26 @@ pub struct AppState {
|
||||
pub ai_tools: Arc<AiToolRegistry>,
|
||||
/// AI 会话状态
|
||||
pub ai_session: Arc<Mutex<AiSession>>,
|
||||
// ── 知识库 ──
|
||||
/// 知识库 Repo
|
||||
pub knowledge: KnowledgeRepo,
|
||||
/// 知识生命线事件 Repo(产生/审核/引用/归档审计)
|
||||
pub knowledge_events: KnowledgeEventsRepo,
|
||||
/// 知识库行为配置(提取 + 注入)
|
||||
pub knowledge_config: Arc<Mutex<KnowledgeConfig>>,
|
||||
/// 通用应用设置 KV Repo(前端 localStorage 迁移目标)
|
||||
pub settings: SettingsRepo,
|
||||
// ── LLM 并发控制 ──
|
||||
/// LLM 调用并发上限(全局 + 单对话双层 Semaphore,运行时可调)
|
||||
pub llm_concurrency: LlmConcurrency,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// 初始化应用状态:打开(或创建)数据库并执行迁移,构建各 Repo 与节点注册表
|
||||
pub async fn init(db_path: &Path) -> Result<Self> {
|
||||
let db = Arc::new(Database::open(db_path).await?);
|
||||
let ai_tools = Arc::new(build_ai_tool_registry());
|
||||
Ok(Self {
|
||||
let ai_tools = Arc::new(crate::commands::ai::build_ai_tool_registry(&db));
|
||||
let state = Self {
|
||||
ideas: IdeaRepo::new(&db),
|
||||
projects: ProjectRepo::new(&db),
|
||||
tasks: TaskRepo::new(&db),
|
||||
@@ -66,11 +190,20 @@ impl AppState {
|
||||
ai_conversations: AiConversationRepo::new(&db),
|
||||
ai_tool_executions: AiToolExecutionRepo::new(&db),
|
||||
ai_session: Arc::new(Mutex::new(AiSession::new())),
|
||||
knowledge: KnowledgeRepo::new(&db),
|
||||
knowledge_events: KnowledgeEventsRepo::new(&db),
|
||||
knowledge_config: Arc::new(Mutex::new(KnowledgeConfig::default())),
|
||||
settings: SettingsRepo::new(&db),
|
||||
llm_concurrency: LlmConcurrency::new(3, 2),
|
||||
db,
|
||||
event_bus: EventBus::new(),
|
||||
registry: Arc::new(build_registry()),
|
||||
ai_tools,
|
||||
})
|
||||
};
|
||||
// 启动恢复:重启前卡 pending 的工具审批(内存 pending_approvals 已丢)从审计表重建,
|
||||
// 使重启后待审批不丢。前端经 ai_pending_tool_calls + switchConversation 恢复 toolCard 态。
|
||||
crate::commands::ai::restore_pending_approvals(&state).await;
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,191 +225,3 @@ fn build_registry() -> NodeRegistry {
|
||||
registry
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// AI 工具注册
|
||||
// ============================================================
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
fn object_schema(fields: Vec<(&str, &str, bool)>) -> serde_json::Value {
|
||||
let mut props = serde_json::Map::new();
|
||||
let mut required = Vec::new();
|
||||
|
||||
for (name, typ, req) in &fields {
|
||||
props.insert(name.to_string(), json!({ "type": typ }));
|
||||
if *req {
|
||||
required.push(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
"required": required,
|
||||
})
|
||||
}
|
||||
|
||||
/// 构建 AI 工具注册表 — 注册所有 CRUD 操作为可调用工具
|
||||
fn build_ai_tool_registry() -> AiToolRegistry {
|
||||
let mut registry = AiToolRegistry::new();
|
||||
|
||||
// ── 只读工具 (Low risk) ──
|
||||
|
||||
registry.register(
|
||||
"list_projects",
|
||||
"列出所有项目,返回项目列表(ID、名称、状态、描述)",
|
||||
object_schema(vec![]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"list_tasks",
|
||||
"列出任务,可按 project_id 筛选",
|
||||
object_schema(vec![("project_id", "string", false)]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"list_ideas",
|
||||
"列出所有想法",
|
||||
object_schema(vec![]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
// ── 创建工具 (Medium risk) ──
|
||||
|
||||
registry.register(
|
||||
"update_project",
|
||||
"更新项目的指定字段(name/status/description),需要提供项目 ID、字段名和新值",
|
||||
object_schema(vec![
|
||||
("id", "string", true),
|
||||
("field", "string", true),
|
||||
("value", "string", true),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"create_project",
|
||||
"创建新项目",
|
||||
object_schema(vec![
|
||||
("name", "string", true),
|
||||
("description", "string", false),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"create_task",
|
||||
"在指定项目下创建新任务",
|
||||
object_schema(vec![
|
||||
("project_id", "string", true),
|
||||
("title", "string", true),
|
||||
("description", "string", false),
|
||||
("priority", "integer", false),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"create_idea",
|
||||
"捕获一个新想法",
|
||||
object_schema(vec![
|
||||
("title", "string", true),
|
||||
("description", "string", false),
|
||||
("tags", "string", false),
|
||||
("source", "string", false),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
// ── 高风险工具 (High risk) ──
|
||||
|
||||
registry.register(
|
||||
"delete_project",
|
||||
"删除项目及其所有关联数据",
|
||||
object_schema(vec![("id", "string", true)]),
|
||||
RiskLevel::High,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"run_workflow",
|
||||
"运行指定的工作流 DAG",
|
||||
object_schema(vec![
|
||||
("name", "string", true),
|
||||
("dag", "object", true),
|
||||
]),
|
||||
RiskLevel::High,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
// ── 文件系统工具 ──
|
||||
|
||||
registry.register(
|
||||
"read_file",
|
||||
"读取文件内容,返回文本内容。支持 offset 和 limit 参数分页读取大文件",
|
||||
object_schema(vec![
|
||||
("path", "string", true),
|
||||
("offset", "integer", false),
|
||||
("limit", "integer", false),
|
||||
]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"list_directory",
|
||||
"列出目录内容,返回文件和子目录列表(名称、类型、大小)",
|
||||
object_schema(vec![
|
||||
("path", "string", true),
|
||||
("recursive", "boolean", false),
|
||||
]),
|
||||
RiskLevel::Low,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry.register(
|
||||
"write_file",
|
||||
"写入或创建文件,自动创建不存在的父目录",
|
||||
object_schema(vec![
|
||||
("path", "string", true),
|
||||
("content", "string", true),
|
||||
]),
|
||||
RiskLevel::Medium,
|
||||
Box::new(|_args| {
|
||||
Box::pin(async { Ok(json!({"note": "工具执行在 IPC 层处理"})) })
|
||||
}),
|
||||
);
|
||||
|
||||
registry
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user