From cca913b610a1a85be63de45c2e9a833e7508aa6a Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Sun, 7 Jun 2026 20:16:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AE=B0=E5=BF=86=20?= =?UTF-8?q?CRUD=20=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E3=80=81=E6=9B=B4=E6=96=B0=E5=92=8C=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E8=AE=B0=E5=BF=86=EF=BC=8C=E4=BC=98=E5=8C=96=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/command/handlers/memory_crud.rs | 118 ++++++++++ src/command/handlers/mod.rs | 1 + src/command/mod.rs | 16 ++ src/gateway/ws.rs | 22 ++ web/src/App.tsx | 12 + web/src/components/Panel/MemoryPanel.tsx | 270 ++++++++++++++--------- web/src/hooks/useChat.ts | 18 ++ web/src/types/protocol.ts | 21 ++ 8 files changed, 372 insertions(+), 106 deletions(-) create mode 100644 src/command/handlers/memory_crud.rs diff --git a/src/command/handlers/memory_crud.rs b/src/command/handlers/memory_crud.rs new file mode 100644 index 0000000..3439de9 --- /dev/null +++ b/src/command/handlers/memory_crud.rs @@ -0,0 +1,118 @@ +use crate::command::context::CommandContext; +use crate::command::handler::{CommandHandler, CommandMetadata}; +use crate::command::response::{CommandError, CommandResponse}; +use crate::command::Command; +use crate::storage::{MemoryUpsert, SessionStore, GLOBAL_SCOPE_KEY}; +use async_trait::async_trait; +use std::sync::Arc; + +pub struct MemoryCrudCommandHandler { + store: Arc, +} + +impl MemoryCrudCommandHandler { + pub fn new(store: Arc) -> Self { + Self { store } + } +} + +/// 通过 ID 查找记忆的 namespace 和 memory_key +fn find_by_id( + store: &SessionStore, + id: &str, +) -> Result, CommandError> { + let records = store + .list_memories_for_scope("user", GLOBAL_SCOPE_KEY) + .map_err(|e| CommandError::new("LIST_ERROR", e.to_string()))?; + Ok(records + .iter() + .find(|r| r.id == id) + .map(|r| (r.namespace.clone(), r.memory_key.clone()))) +} + +#[async_trait] +impl CommandHandler for MemoryCrudCommandHandler { + fn can_handle(&self, cmd: &Command) -> bool { + matches!( + cmd, + Command::CreateMemory { .. } | Command::UpdateMemory { .. } | Command::DeleteMemory { .. } + ) + } + + fn metadata(&self) -> Option { + Some(CommandMetadata { + name: "memory_crud", + description: "创建、更新、删除记忆", + usage: "/memory_crud", + }) + } + + async fn handle( + &self, + cmd: Command, + ctx: CommandContext, + ) -> Result { + let scope_key = GLOBAL_SCOPE_KEY.to_string(); + + match cmd { + Command::CreateMemory { + namespace, + key, + content, + } => { + let input = MemoryUpsert { + scope_kind: "user".to_string(), + scope_key, + namespace, + memory_key: key, + content, + source_type: "manual".to_string(), + source_session_id: None, + source_message_id: None, + source_message_seq: None, + source_channel_name: None, + source_chat_id: None, + }; + self.store + .put_memory(&input) + .map_err(|e| CommandError::new("CREATE_ERROR", e.to_string()))?; + } + + Command::UpdateMemory { id, content } => { + let Some((namespace, memory_key)) = find_by_id(&self.store, &id)? else { + return Err(CommandError::new("NOT_FOUND", "记忆不存在")); + }; + let input = MemoryUpsert { + scope_kind: "user".to_string(), + scope_key, + namespace, + memory_key, + content, + source_type: "manual".to_string(), + source_session_id: None, + source_message_id: None, + source_message_seq: None, + source_channel_name: None, + source_chat_id: None, + }; + self.store + .update_memory(&input) + .map_err(|e| CommandError::new("UPDATE_ERROR", e.to_string()))?; + } + + Command::DeleteMemory { id } => { + let Some((namespace, memory_key)) = find_by_id(&self.store, &id)? else { + return Err(CommandError::new("NOT_FOUND", "记忆不存在")); + }; + self.store + .delete_memory("user", &scope_key, &namespace, &memory_key) + .map_err(|e| CommandError::new("DELETE_ERROR", e.to_string()))?; + } + + _ => unreachable!(), + } + + Ok(CommandResponse::success(ctx.request_id) + .with_metadata("memory_updated", "true")) + } +} diff --git a/src/command/handlers/mod.rs b/src/command/handlers/mod.rs index 32befa8..d6dbe5a 100644 --- a/src/command/handlers/mod.rs +++ b/src/command/handlers/mod.rs @@ -4,6 +4,7 @@ pub mod help; pub mod list_channels; pub mod list_memories; pub mod list_scheduler_jobs; +pub mod memory_crud; pub mod list_sessions; pub mod list_sessions_by_channel; pub mod list_topics; diff --git a/src/command/mod.rs b/src/command/mod.rs index 0af5bac..6578617 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -58,6 +58,19 @@ pub enum Command { StopExecution, /// 列出所有记忆 ListMemories, + /// 创建新记忆 + CreateMemory { + namespace: String, + key: String, + content: String, + }, + /// 更新已有记忆 + UpdateMemory { + id: String, + content: String, + }, + /// 删除记忆 + DeleteMemory { id: String }, } impl Command { @@ -81,6 +94,9 @@ impl Command { Command::DeleteTopic { .. } => "delete_topic", Command::StopExecution => "stop_execution", Command::ListMemories => "list_memories", + Command::CreateMemory { .. } => "create_memory", + Command::UpdateMemory { .. } => "update_memory", + Command::DeleteMemory { .. } => "delete_memory", } } } diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index ea4a095..5ed0799 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -11,6 +11,7 @@ use crate::command::handlers::help::HelpCommandHandler; use crate::command::handlers::list_channels::ListChannelsCommandHandler; use crate::command::handlers::list_memories::ListMemoriesCommandHandler; use crate::command::handlers::list_scheduler_jobs::ListSchedulerJobsCommandHandler; +use crate::command::handlers::memory_crud::MemoryCrudCommandHandler; use crate::command::handlers::list_sessions::ListSessionsCommandHandler; use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler; use crate::command::handlers::list_topics::ListTopicsCommandHandler; @@ -417,6 +418,8 @@ async fn handle_inbound( router.register(Box::new(ListSchedulerJobsCommandHandler::new(store.clone()))); // 注册 list_memories 处理器 router.register(Box::new(ListMemoriesCommandHandler::new(store.clone()))); + // 注册 memory_crud 处理器 + router.register(Box::new(MemoryCrudCommandHandler::new(store.clone()))); // 注册 load_chat_messages 处理器 router.register(Box::new(LoadChatMessagesCommandHandler::new())); // 注册 stop_execution 处理器 @@ -512,6 +515,25 @@ async fn handle_inbound( } } + // 记忆 CRUD 后自动刷新列表 + if response.metadata.get("memory_updated").map(|v| v.as_str()) == Some("true") { + if let Ok(records) = store.list_memories_for_scope("user", crate::storage::GLOBAL_SCOPE_KEY) { + let memories: Vec = records + .into_iter() + .filter(|m| m.namespace != "_meta") + .map(|m| crate::protocol::MemorySummary { + id: m.id, + namespace: m.namespace, + memory_key: m.memory_key, + content: m.content, + created_at: m.created_at, + updated_at: m.updated_at, + }) + .collect(); + let _ = sender.send(WsOutbound::MemoryList { memories }).await; + } + } + // 处理加载聊天消息请求 if let Some(load_chat_id) = response.metadata.get("load_chat_id") { let load_chat_channel = response.metadata.get("load_chat_channel") diff --git a/web/src/App.tsx b/web/src/App.tsx index b573ed3..8d33a35 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -35,6 +35,9 @@ function App() { // 记忆 memories, requestMemoryList, + createMemory, + updateMemory, + deleteMemory, // 定时任务 schedulerJobs, sidebarTab, @@ -314,6 +317,11 @@ function App() { sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) }, [handleCommand, sendMessage, requestMemoryList]) + const sendMemoryCommand = useCallback((cmd: Command) => { + handleCommand(cmd) + sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) + }, [handleCommand, sendMessage]) + const handleRefreshSchedulerJobs = useCallback(() => { const cmd = requestSchedulerJobList() handleCommand(cmd) @@ -572,6 +580,10 @@ function App() { memories={memories} onRefresh={handleRefreshMemories} onClose={() => toggleMemoryPanel(false)} + onCreateMemory={createMemory} + onUpdateMemory={updateMemory} + onDeleteMemory={deleteMemory} + sendCommand={sendMemoryCommand} /> diff --git a/web/src/components/Panel/MemoryPanel.tsx b/web/src/components/Panel/MemoryPanel.tsx index 2000132..23fa6f2 100644 --- a/web/src/components/Panel/MemoryPanel.tsx +++ b/web/src/components/Panel/MemoryPanel.tsx @@ -1,182 +1,240 @@ import { useState } from 'react' -import { Brain, Lightbulb, Sparkles, RefreshCw, X, ChevronDown, ChevronRight, User, ListTodo, FileText, Tag } from 'lucide-react' -import type { MemorySummary } from '../../types/protocol' +import { Brain, User, Library, History, Cpu, Globe, Star, Package, RefreshCw, X, ChevronDown, ChevronRight, Plus, Pencil, Trash2, Check } from 'lucide-react' +import type { MemorySummary, Command } from '../../types/protocol' + +/* ── types ────────────────────────────────────────────── */ interface MemoryPanelProps { memories: MemorySummary[] onRefresh: () => void onClose?: () => void + onCreateMemory: (ns: string, key: string, content: string) => Command + onUpdateMemory: (id: string, content: string) => Command + onDeleteMemory: (id: string) => Command + sendCommand: (cmd: Command) => void } -interface NamespaceConfig { - label: string - icon: typeof Brain - accent: string - accentBorder: string +interface NamespaceConfig { label: string; icon: typeof Brain; accent: string; accentBorder: string } + +const NS: Record = { + user: { label: '用户记忆', icon: User, accent: 'text-cyan-400', accentBorder: 'border-cyan-400/40' }, + semantic: { label: '语义记忆', icon: Library, accent: 'text-amber-400', accentBorder: 'border-amber-400/40' }, + episodic: { label: '情景记忆', icon: History, accent: 'text-purple-400', accentBorder: 'border-purple-400/40' }, + skill: { label: '技能记忆', icon: Cpu, accent: 'text-green-400', accentBorder: 'border-green-400/40' }, + environment: { label: '环境记忆', icon: Globe, accent: 'text-sky-400', accentBorder: 'border-sky-400/40' }, + reflection: { label: '反思记忆', icon: Star, accent: 'text-rose-400', accentBorder: 'border-rose-400/40' }, + other: { label: '其他', icon: Package, accent: 'text-stone-400', accentBorder: 'border-stone-400/40' }, } -const NAMESPACE_CONFIG: Record = { - user: { label: '用户事实', icon: Brain, accent: 'text-emerald-400', accentBorder: 'border-emerald-400/40' }, - preferences:{ label: '偏好', icon: Lightbulb, accent: 'text-amber-400', accentBorder: 'border-amber-400/40' }, - patterns: { label: '行为模式', icon: Sparkles, accent: 'text-violet-400', accentBorder: 'border-violet-400/40' }, - profile: { label: '档案', icon: User, accent: 'text-cyan-400', accentBorder: 'border-cyan-400/40' }, - tasks: { label: '任务', icon: ListTodo, accent: 'text-rose-400', accentBorder: 'border-rose-400/40' }, - notes: { label: '笔记', icon: FileText, accent: 'text-sky-400', accentBorder: 'border-sky-400/40' }, - tags: { label: '标签', icon: Tag, accent: 'text-pink-400', accentBorder: 'border-pink-400/40' }, +function cfg(ns: string): NamespaceConfig { + return NS[ns] ?? { label: ns, icon: Package, accent: 'text-[var(--text-secondary)]', accentBorder: 'border-[var(--border-color)]' } } -function getNsConfig(ns: string): NamespaceConfig { - return NAMESPACE_CONFIG[ns] ?? { - label: ns, - icon: Tag, - accent: 'text-[var(--text-secondary)]', - accentBorder: 'border-[var(--border-color)]', +function fmtKey(k: string) { return k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) } + +const NS_OPTIONS = Object.entries(NS) + +/* ── Memory card with edit/delete ──────────────────────── */ + +function MemoryCard({ memory, config, onUpdate, onDelete }: + { memory: MemorySummary; config: NamespaceConfig; onUpdate: (id: string, content: string) => void; onDelete: (id: string) => void }) { + + const [editing, setEditing] = useState(false) + const [editContent, setEditContent] = useState(memory.content) + const [confirmDelete, setConfirmDelete] = useState(false) + + const handleSave = () => { + if (editContent.trim() && editContent !== memory.content) { + onUpdate(memory.id, editContent.trim()) + } + setEditing(false) } -} -function fmtKey(key: string): string { - return key - .replace(/_/g, ' ') - .replace(/\b\w/g, c => c.toUpperCase()) -} + const handleDelete = () => { + if (confirmDelete) { + onDelete(memory.id) + } else { + setConfirmDelete(true) + setTimeout(() => setConfirmDelete(false), 3000) + } + } -function MemoryCard({ memory, config }: { memory: MemorySummary; config: NamespaceConfig }) { return ( -
+
- - {fmtKey(memory.memory_key)} - -

- {memory.content} -

+ {/* header row */} +
+ + {fmtKey(memory.memory_key)} + + {/* action buttons — visible on hover */} +
+ + +
+
+ + {/* content */} + {editing ? ( +
+