feat: 新增智能体与工作流选项卡(预制/自定义、画布编辑器、执行引擎);约定每次改动提交推送并打新 tag

This commit is contained in:
xianrenge
2026-08-16 23:28:55 +08:00
parent 10ecf526ec
commit 9b9e810ff1
24 changed files with 3550 additions and 118 deletions
+128
View File
@@ -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)?,
})
}
+1
View File
@@ -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)
}
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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)?,
})
}
+162
View File
@@ -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)?,
})
}