feat: 新增智能体与工作流选项卡(预制/自定义、画布编辑器、执行引擎);约定每次改动提交推送并打新 tag
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
use crate::error::Result;
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Agent {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub description: String,
|
||||
pub system_prompt: String,
|
||||
pub model_id: Option<String>,
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
const SELECT_COLUMNS: &str = "id, kind, name, icon, description, system_prompt, model_id, enabled, created_at, updated_at";
|
||||
|
||||
pub fn list(db: &Connection) -> Result<Vec<Agent>> {
|
||||
let mut stmt = db.prepare(&format!(
|
||||
"SELECT {SELECT_COLUMNS} FROM agents
|
||||
ORDER BY (kind = 'preset') DESC, updated_at DESC, created_at DESC"
|
||||
))?;
|
||||
let rows = stmt.query_map([], row_to_agent)?;
|
||||
let mut out = Vec::new();
|
||||
for row in rows {
|
||||
out.push(row?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn get(db: &Connection, id: &str) -> Result<Option<Agent>> {
|
||||
let mut stmt = db.prepare(&format!(
|
||||
"SELECT {SELECT_COLUMNS} FROM agents WHERE id = ?1"
|
||||
))?;
|
||||
let mut rows = stmt.query_map(params![id], row_to_agent)?;
|
||||
match rows.next() {
|
||||
Some(row) => Ok(Some(row?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(db: &Connection, agent: &Agent) -> Result<()> {
|
||||
db.execute(
|
||||
"INSERT INTO agents (id, kind, name, icon, description, system_prompt, model_id, enabled, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, datetime('now'), datetime('now'))",
|
||||
params![
|
||||
agent.id,
|
||||
agent.kind,
|
||||
agent.name,
|
||||
agent.icon,
|
||||
agent.description,
|
||||
agent.system_prompt,
|
||||
agent.model_id,
|
||||
agent.enabled as i32,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(db: &Connection, agent: &Agent) -> Result<()> {
|
||||
db.execute(
|
||||
"UPDATE agents SET kind = ?1, name = ?2, icon = ?3, description = ?4,
|
||||
system_prompt = ?5, model_id = ?6, enabled = ?7, updated_at = datetime('now')
|
||||
WHERE id = ?8",
|
||||
params![
|
||||
agent.kind,
|
||||
agent.name,
|
||||
agent.icon,
|
||||
agent.description,
|
||||
agent.system_prompt,
|
||||
agent.model_id,
|
||||
agent.enabled as i32,
|
||||
agent.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete(db: &Connection, id: &str) -> Result<()> {
|
||||
db.execute("DELETE FROM agents WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_enabled(db: &Connection, id: &str, enabled: bool) -> Result<()> {
|
||||
db.execute(
|
||||
"UPDATE agents SET enabled = ?1, updated_at = datetime('now') WHERE id = ?2",
|
||||
params![enabled as i32, id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 批量写入预制智能体(按 id 忽略已存在的,方便“恢复预制智能体”重复调用)。
|
||||
pub fn insert_presets_if_missing(db: &Connection, presets: &[Agent]) -> Result<usize> {
|
||||
let mut inserted = 0;
|
||||
for p in presets {
|
||||
let exists: bool = db.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM agents WHERE id = ?1)",
|
||||
params![p.id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if !exists {
|
||||
insert(db, p)?;
|
||||
inserted += 1;
|
||||
}
|
||||
}
|
||||
Ok(inserted)
|
||||
}
|
||||
|
||||
fn row_to_agent(row: &rusqlite::Row<'_>) -> rusqlite::Result<Agent> {
|
||||
let enabled: i32 = row.get(7)?;
|
||||
Ok(Agent {
|
||||
id: row.get(0)?,
|
||||
kind: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
icon: row.get(3)?,
|
||||
description: row.get(4)?,
|
||||
system_prompt: row.get(5)?,
|
||||
model_id: row.get(6)?,
|
||||
enabled: enabled != 0,
|
||||
created_at: row.get(8)?,
|
||||
updated_at: row.get(9)?,
|
||||
})
|
||||
}
|
||||
@@ -109,6 +109,7 @@ fn open_db(path: &Path) -> Result<Connection> {
|
||||
ensure_column(&conn, "conversations", "pinned", "INTEGER NOT NULL DEFAULT 0")?;
|
||||
ensure_column(&conn, "conversations", "favorite", "INTEGER NOT NULL DEFAULT 0")?;
|
||||
ensure_column(&conn, "conversations", "tools_json", "TEXT NOT NULL DEFAULT '[]'")?;
|
||||
ensure_column(&conn, "conversations", "agent_id", "TEXT")?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod agents;
|
||||
pub mod app;
|
||||
pub mod error;
|
||||
pub mod mcp_servers;
|
||||
@@ -5,8 +6,11 @@ pub mod models;
|
||||
pub mod sessions;
|
||||
pub mod settings;
|
||||
pub mod skills;
|
||||
pub mod workflows;
|
||||
|
||||
pub use app::CoreApp;
|
||||
pub use error::{CoreError, Result};
|
||||
pub use models::ModelInfo;
|
||||
pub use agents::Agent;
|
||||
pub use sessions::{Conversation, Message};
|
||||
pub use workflows::Workflow;
|
||||
@@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS conversations (
|
||||
title TEXT NOT NULL DEFAULT '新会话',
|
||||
model_id TEXT,
|
||||
system_prompt TEXT,
|
||||
agent_id TEXT,
|
||||
pinned INTEGER NOT NULL DEFAULT 0,
|
||||
favorite INTEGER NOT NULL DEFAULT 0,
|
||||
tools_json TEXT NOT NULL DEFAULT '[]',
|
||||
@@ -35,6 +36,33 @@ CREATE TABLE IF NOT EXISTS conversations (
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL DEFAULT 'custom',
|
||||
name TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🤖',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
system_prompt TEXT NOT NULL DEFAULT '',
|
||||
model_id TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflows (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL DEFAULT 'custom',
|
||||
name TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🔀',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
nodes_json TEXT NOT NULL DEFAULT '[]',
|
||||
edges_json TEXT NOT NULL DEFAULT '[]',
|
||||
model_id TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
|
||||
+28
-16
@@ -8,6 +8,7 @@ pub struct Conversation {
|
||||
pub title: String,
|
||||
pub model_id: Option<String>,
|
||||
pub system_prompt: Option<String>,
|
||||
pub agent_id: Option<String>,
|
||||
pub pinned: bool,
|
||||
pub favorite: bool,
|
||||
pub tools: Vec<String>,
|
||||
@@ -48,20 +49,25 @@ pub fn create_conversation(
|
||||
title: &str,
|
||||
model_id: Option<&str>,
|
||||
system_prompt: Option<&str>,
|
||||
agent_id: Option<&str>,
|
||||
) -> Result<()> {
|
||||
db.execute(
|
||||
"INSERT INTO conversations (id, title, model_id, system_prompt) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![id, title, model_id, system_prompt],
|
||||
"INSERT INTO conversations (id, title, model_id, system_prompt, agent_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![id, title, model_id, system_prompt, agent_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_conversations(db: &Connection) -> Result<Vec<Conversation>> {
|
||||
/// 列出指定作用域的会话:`agent_id = None` 时只返回普通对话(agent_id 为空),
|
||||
/// `Some(id)` 时只返回该智能体的对话。
|
||||
pub fn list_conversations(db: &Connection, agent_id: Option<&str>) -> Result<Vec<Conversation>> {
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT id, title, model_id, system_prompt, pinned, favorite, tools_json, created_at, updated_at
|
||||
FROM conversations ORDER BY pinned DESC, updated_at DESC",
|
||||
"SELECT id, title, model_id, system_prompt, agent_id, pinned, favorite, tools_json, created_at, updated_at
|
||||
FROM conversations
|
||||
WHERE (?1 IS NULL AND agent_id IS NULL) OR agent_id = ?1
|
||||
ORDER BY pinned DESC, updated_at DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], row_to_conversation)?;
|
||||
let rows = stmt.query_map(params![agent_id], row_to_conversation)?;
|
||||
let mut out = Vec::new();
|
||||
for row in rows {
|
||||
out.push(row?);
|
||||
@@ -71,7 +77,7 @@ pub fn list_conversations(db: &Connection) -> Result<Vec<Conversation>> {
|
||||
|
||||
pub fn get_conversation(db: &Connection, id: &str) -> Result<Option<Conversation>> {
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT id, title, model_id, system_prompt, pinned, favorite, tools_json, created_at, updated_at
|
||||
"SELECT id, title, model_id, system_prompt, agent_id, pinned, favorite, tools_json, created_at, updated_at
|
||||
FROM conversations WHERE id = ?1",
|
||||
)?;
|
||||
let mut rows = stmt.query_map(params![id], row_to_conversation)?;
|
||||
@@ -81,16 +87,21 @@ pub fn get_conversation(db: &Connection, id: &str) -> Result<Option<Conversation
|
||||
}
|
||||
}
|
||||
|
||||
/// 查找最近一个还没有任何消息的空会话(用于“新建对话”时复用,避免产生多个空会话)。
|
||||
pub fn find_empty_conversation(db: &Connection) -> Result<Option<Conversation>> {
|
||||
/// 查找指定作用域(普通对话或某个智能体)下最近一个还没有任何消息的空会话,
|
||||
/// 用于“新建对话”时复用,避免产生多个空会话。
|
||||
pub fn find_empty_conversation(
|
||||
db: &Connection,
|
||||
agent_id: Option<&str>,
|
||||
) -> Result<Option<Conversation>> {
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT c.id, c.title, c.model_id, c.system_prompt, c.pinned, c.favorite, c.tools_json, c.created_at, c.updated_at
|
||||
"SELECT c.id, c.title, c.model_id, c.system_prompt, c.agent_id, c.pinned, c.favorite, c.tools_json, c.created_at, c.updated_at
|
||||
FROM conversations c
|
||||
WHERE NOT EXISTS (SELECT 1 FROM messages m WHERE m.conversation_id = c.id)
|
||||
AND ((?1 IS NULL AND c.agent_id IS NULL) OR c.agent_id = ?1)
|
||||
ORDER BY c.updated_at DESC
|
||||
LIMIT 1",
|
||||
)?;
|
||||
let mut rows = stmt.query_map([], row_to_conversation)?;
|
||||
let mut rows = stmt.query_map(params![agent_id], row_to_conversation)?;
|
||||
match rows.next() {
|
||||
Some(row) => Ok(Some(row?)),
|
||||
None => Ok(None),
|
||||
@@ -351,19 +362,20 @@ pub fn apply_message_version(
|
||||
}
|
||||
|
||||
fn row_to_conversation(row: &rusqlite::Row<'_>) -> rusqlite::Result<Conversation> {
|
||||
let pinned: i32 = row.get(4)?;
|
||||
let favorite: i32 = row.get(5)?;
|
||||
let tools_json: String = row.get(6)?;
|
||||
let pinned: i32 = row.get(5)?;
|
||||
let favorite: i32 = row.get(6)?;
|
||||
let tools_json: String = row.get(7)?;
|
||||
Ok(Conversation {
|
||||
id: row.get(0)?,
|
||||
title: row.get(1)?,
|
||||
model_id: row.get(2)?,
|
||||
system_prompt: row.get(3)?,
|
||||
agent_id: row.get(4)?,
|
||||
pinned: pinned != 0,
|
||||
favorite: favorite != 0,
|
||||
tools: serde_json::from_str(&tools_json).unwrap_or_default(),
|
||||
created_at: row.get(7)?,
|
||||
updated_at: row.get(8)?,
|
||||
created_at: row.get(8)?,
|
||||
updated_at: row.get(9)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::error::Result;
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 画布节点位置(前端画布坐标)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodePosition {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
/// 节点类型:start=开始(提供 {{input}})、llm=大模型调用、text=静态文本、end=结束输出。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkflowNode {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
pub label: String,
|
||||
pub position: NodePosition,
|
||||
#[serde(default)]
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkflowEdge {
|
||||
pub id: String,
|
||||
#[serde(rename = "from")]
|
||||
pub from_node: String,
|
||||
#[serde(rename = "to")]
|
||||
pub to_node: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Workflow {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub description: String,
|
||||
pub nodes: Vec<WorkflowNode>,
|
||||
pub edges: Vec<WorkflowEdge>,
|
||||
pub model_id: Option<String>,
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub created_at: String,
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
const SELECT_COLUMNS: &str =
|
||||
"id, kind, name, icon, description, nodes_json, edges_json, model_id, enabled, created_at, updated_at";
|
||||
|
||||
pub fn list(db: &Connection) -> Result<Vec<Workflow>> {
|
||||
let mut stmt = db.prepare(&format!(
|
||||
"SELECT {SELECT_COLUMNS} FROM workflows
|
||||
ORDER BY (kind = 'preset') DESC, updated_at DESC, created_at DESC"
|
||||
))?;
|
||||
let rows = stmt.query_map([], row_to_workflow)?;
|
||||
let mut out = Vec::new();
|
||||
for row in rows {
|
||||
out.push(row?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn get(db: &Connection, id: &str) -> Result<Option<Workflow>> {
|
||||
let mut stmt = db.prepare(&format!(
|
||||
"SELECT {SELECT_COLUMNS} FROM workflows WHERE id = ?1"
|
||||
))?;
|
||||
let mut rows = stmt.query_map(params![id], row_to_workflow)?;
|
||||
match rows.next() {
|
||||
Some(row) => Ok(Some(row?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(db: &Connection, workflow: &Workflow) -> Result<()> {
|
||||
db.execute(
|
||||
"INSERT INTO workflows (id, kind, name, icon, description, nodes_json, edges_json, model_id, enabled, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, datetime('now'), datetime('now'))",
|
||||
params![
|
||||
workflow.id,
|
||||
workflow.kind,
|
||||
workflow.name,
|
||||
workflow.icon,
|
||||
workflow.description,
|
||||
serde_json::to_string(&workflow.nodes).unwrap_or_else(|_| "[]".to_string()),
|
||||
serde_json::to_string(&workflow.edges).unwrap_or_else(|_| "[]".to_string()),
|
||||
workflow.model_id,
|
||||
workflow.enabled as i32,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(db: &Connection, workflow: &Workflow) -> Result<()> {
|
||||
db.execute(
|
||||
"UPDATE workflows SET kind = ?1, name = ?2, icon = ?3, description = ?4,
|
||||
nodes_json = ?5, edges_json = ?6, model_id = ?7, enabled = ?8, updated_at = datetime('now')
|
||||
WHERE id = ?9",
|
||||
params![
|
||||
workflow.kind,
|
||||
workflow.name,
|
||||
workflow.icon,
|
||||
workflow.description,
|
||||
serde_json::to_string(&workflow.nodes).unwrap_or_else(|_| "[]".to_string()),
|
||||
serde_json::to_string(&workflow.edges).unwrap_or_else(|_| "[]".to_string()),
|
||||
workflow.model_id,
|
||||
workflow.enabled as i32,
|
||||
workflow.id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete(db: &Connection, id: &str) -> Result<()> {
|
||||
db.execute("DELETE FROM workflows WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_enabled(db: &Connection, id: &str, enabled: bool) -> Result<()> {
|
||||
db.execute(
|
||||
"UPDATE workflows SET enabled = ?1, updated_at = datetime('now') WHERE id = ?2",
|
||||
params![enabled as i32, id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_presets_if_missing(db: &Connection, presets: &[Workflow]) -> Result<usize> {
|
||||
let mut inserted = 0;
|
||||
for p in presets {
|
||||
let exists: bool = db.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM workflows WHERE id = ?1)",
|
||||
params![p.id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if !exists {
|
||||
insert(db, p)?;
|
||||
inserted += 1;
|
||||
}
|
||||
}
|
||||
Ok(inserted)
|
||||
}
|
||||
|
||||
fn row_to_workflow(row: &rusqlite::Row<'_>) -> rusqlite::Result<Workflow> {
|
||||
let nodes_json: String = row.get(5)?;
|
||||
let edges_json: String = row.get(6)?;
|
||||
let enabled: i32 = row.get(8)?;
|
||||
Ok(Workflow {
|
||||
id: row.get(0)?,
|
||||
kind: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
icon: row.get(3)?,
|
||||
description: row.get(4)?,
|
||||
nodes: serde_json::from_str(&nodes_json).unwrap_or_default(),
|
||||
edges: serde_json::from_str(&edges_json).unwrap_or_default(),
|
||||
model_id: row.get(7)?,
|
||||
enabled: enabled != 0,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user