Compare commits

..

No commits in common. "cca913b610a1a85be63de45c2e9a833e7508aa6a" and "1f04c62d0d35a2f74ec96b5ed6fc7fdb8d471caa" have entirely different histories.

10 changed files with 7 additions and 653 deletions

View File

@ -1,74 +0,0 @@
use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::response::{CommandError, CommandResponse};
use crate::command::Command;
use crate::storage::SessionStore;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// Memory 摘要信息(发送给前端)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemorySummary {
pub id: String,
pub namespace: String,
pub memory_key: String,
pub content: String,
pub created_at: i64,
pub updated_at: i64,
}
pub struct ListMemoriesCommandHandler {
store: Arc<SessionStore>,
}
impl ListMemoriesCommandHandler {
pub fn new(store: Arc<SessionStore>) -> Self {
Self { store }
}
}
#[async_trait]
impl CommandHandler for ListMemoriesCommandHandler {
fn can_handle(&self, cmd: &Command) -> bool {
matches!(cmd, Command::ListMemories)
}
fn metadata(&self) -> Option<CommandMetadata> {
Some(CommandMetadata {
name: "list_memories",
description: "列出所有记忆",
usage: "/list_memories",
})
}
async fn handle(
&self,
_cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
let records = self
.store
.list_memories_for_scope("user", crate::storage::GLOBAL_SCOPE_KEY)
.map_err(|e| CommandError::new("LIST_MEMORIES_ERROR", e.to_string()))?;
let summaries: Vec<MemorySummary> = records
.into_iter()
.filter(|m| m.namespace != "_meta")
.map(|m| 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 memories_json = serde_json::to_string(&summaries)
.map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?;
Ok(CommandResponse::success(ctx.request_id)
.with_metadata("memories", &memories_json))
}
}

View File

@ -1,118 +0,0 @@
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<SessionStore>,
}
impl MemoryCrudCommandHandler {
pub fn new(store: Arc<SessionStore>) -> Self {
Self { store }
}
}
/// 通过 ID 查找记忆的 namespace 和 memory_key
fn find_by_id(
store: &SessionStore,
id: &str,
) -> Result<Option<(String, String)>, 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<CommandMetadata> {
Some(CommandMetadata {
name: "memory_crud",
description: "创建、更新、删除记忆",
usage: "/memory_crud",
})
}
async fn handle(
&self,
cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
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"))
}
}

View File

@ -2,9 +2,7 @@ pub mod delete_topic;
pub mod get_current;
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;

View File

@ -56,21 +56,6 @@ pub enum Command {
DeleteTopic { topic_id: String },
/// 停止当前正在执行的 Agent
StopExecution,
/// 列出所有记忆
ListMemories,
/// 创建新记忆
CreateMemory {
namespace: String,
key: String,
content: String,
},
/// 更新已有记忆
UpdateMemory {
id: String,
content: String,
},
/// 删除记忆
DeleteMemory { id: String },
}
impl Command {
@ -93,10 +78,6 @@ impl Command {
Command::LoadChatMessages { .. } => "load_chat_messages",
Command::DeleteTopic { .. } => "delete_topic",
Command::StopExecution => "stop_execution",
Command::ListMemories => "list_memories",
Command::CreateMemory { .. } => "create_memory",
Command::UpdateMemory { .. } => "update_memory",
Command::DeleteMemory { .. } => "delete_memory",
}
}
}

View File

@ -9,9 +9,7 @@ use crate::command::handlers::delete_topic::DeleteTopicCommandHandler;
use crate::command::handlers::get_current::GetCurrentSessionCommandHandler;
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;
@ -416,10 +414,6 @@ async fn handle_inbound(
router.register(Box::new(HelpCommandHandler::new(metadata)));
// 注册 list_scheduler_jobs 处理器
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 处理器
@ -508,32 +502,6 @@ async fn handle_inbound(
}
}
// 处理记忆列表
if let Some(memories_json) = response.metadata.get("memories") {
if let Ok(memories) = serde_json::from_str::<Vec<crate::protocol::MemorySummary>>(memories_json) {
let _ = sender.send(WsOutbound::MemoryList { memories }).await;
}
}
// 记忆 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<crate::protocol::MemorySummary> = 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")

View File

@ -63,17 +63,6 @@ pub struct SchedulerJobSessionLookup {
pub chat_id: String,
}
/// Memory 摘要(发送给前端)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemorySummary {
pub id: String,
pub namespace: String,
pub memory_key: String,
pub content: String,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchedulerJobSummary {
pub id: String,
@ -233,10 +222,6 @@ pub enum WsOutbound {
SchedulerJobList {
jobs: Vec<SchedulerJobSummary>,
},
#[serde(rename = "memory_list")]
MemoryList {
memories: Vec<MemorySummary>,
},
#[serde(rename = "execution_cancelled")]
ExecutionCancelled { message: String },
#[serde(rename = "pong")]

View File

@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen } from 'lucide-react'
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon } from 'lucide-react'
import { ChatContainer } from './components/Chat/ChatContainer'
import { TopicList } from './components/Sidebar/TopicList'
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
import { MemoryPanel } from './components/Panel/MemoryPanel'
import { ToolPanel } from './components/Panel/ToolPanel'
import { ConnectionStatus } from './components/ConnectionStatus'
import { ChannelSelector } from './components/Header/ChannelSelector'
import { SessionSelector } from './components/Header/SessionSelector'
@ -32,12 +32,6 @@ function App() {
isReadOnly,
// 子智能体视图
subAgentView,
// 记忆
memories,
requestMemoryList,
createMemory,
updateMemory,
deleteMemory,
// 定时任务
schedulerJobs,
sidebarTab,
@ -79,19 +73,6 @@ function App() {
// ---- 主题状态 ----
const [memoryPanelOpen, setMemoryPanelOpen] = useState(() => {
try {
return localStorage.getItem('picobot-memory-panel-open') !== 'false'
} catch {
return false
}
})
const toggleMemoryPanel = useCallback((open: boolean) => {
setMemoryPanelOpen(open)
localStorage.setItem('picobot-memory-panel-open', String(open))
}, [])
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
const saved = localStorage.getItem('picobot-theme')
return saved === 'light' ? 'light' : 'dark'
@ -302,26 +283,6 @@ function App() {
}
}, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList])
// 连接就绪时自动拉取记忆列表
useEffect(() => {
if (status === 'connected') {
const cmd = requestMemoryList()
handleCommand(cmd)
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
}
}, [status, handleCommand, sendMessage, requestMemoryList])
const handleRefreshMemories = useCallback(() => {
const cmd = requestMemoryList()
handleCommand(cmd)
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)
@ -399,6 +360,7 @@ function App() {
return result
}, [messages])
const toolMessages = messages
return (
<div className="flex h-screen flex-col bg-[var(--bg-primary)] text-[var(--text-primary)] overflow-hidden">
@ -440,7 +402,7 @@ function App() {
</header>
{/* Main Content */}
<div className="flex flex-1 overflow-hidden relative">
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar */}
<div className={`w-72 shrink-0 border-r border-[var(--border-color)] bg-[var(--bg-secondary)]/50 flex flex-col ${subAgentView || schedulerView ? 'opacity-50 pointer-events-none' : ''}`}>
{/* Tab 栏 */}
@ -573,33 +535,10 @@ function App() {
</div>
</div>
{/* Right Sidebar - Memory Panel (collapsible) */}
<div className={`shrink-0 border-l border-[var(--border-color)] bg-[var(--bg-secondary)]/50 transition-all duration-300 ease-out overflow-hidden ${memoryPanelOpen ? 'w-80' : 'w-0 border-l-0'}`}>
<div className={`w-80 ${memoryPanelOpen ? '' : 'invisible'}`}>
<MemoryPanel
memories={memories}
onRefresh={handleRefreshMemories}
onClose={() => toggleMemoryPanel(false)}
onCreateMemory={createMemory}
onUpdateMemory={updateMemory}
onDeleteMemory={deleteMemory}
sendCommand={sendMemoryCommand}
/>
</div>
{/* Right Sidebar - Tool Panel */}
<div className="w-80 shrink-0 border-l border-[var(--border-color)] bg-[var(--bg-secondary)]/50">
<ToolPanel messages={toolMessages} />
</div>
{/* Reopen button — visible when panel is collapsed */}
{!memoryPanelOpen && (
<div className="absolute right-0 top-1/2 -translate-y-1/2 z-10">
<button
onClick={() => toggleMemoryPanel(true)}
className="flex items-center gap-1.5 px-2 py-4 rounded-l-xl bg-[var(--bg-secondary)]/80 backdrop-blur-sm border border-r-0 border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:border-[var(--accent-cyan)]/30 transition-all duration-300 shadow-lg"
title="展开记忆面板"
>
<PanelRightOpen className="h-4 w-4" />
</button>
</div>
)}
</div>
</div>
)

View File

@ -1,247 +0,0 @@
import { useState } from 'react'
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 }
const NS: Record<string, NamespaceConfig> = {
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' },
}
function cfg(ns: string): NamespaceConfig {
return NS[ns] ?? { label: ns, icon: Package, 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)
}
const handleDelete = () => {
if (confirmDelete) {
onDelete(memory.id)
} else {
setConfirmDelete(true)
setTimeout(() => setConfirmDelete(false), 3000)
}
}
return (
<div className={`group rounded-lg bg-[var(--overlay-hover)] border-l-2 ${config.accentBorder} border border-[var(--border-color)] overflow-hidden transition-all duration-200 hover:border-[var(--border-accent)]`}>
<div className="px-3 py-2.5">
{/* header row */}
<div className="flex items-center justify-between mb-0.5">
<span className={`text-[10px] font-mono uppercase tracking-wider ${config.accent} opacity-60`}>
{fmtKey(memory.memory_key)}
</span>
{/* action buttons — visible on hover */}
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => { setEditing(!editing); setEditContent(memory.content) }}
className={`p-1 rounded hover:bg-[var(--overlay-subtle)] ${editing ? 'text-[var(--accent-cyan)]' : 'text-[var(--text-muted)]'} hover:text-[var(--accent-cyan)] transition-colors`} title="编辑">
<Pencil className="h-3 w-3" />
</button>
<button onClick={handleDelete}
className={`p-1 rounded hover:bg-red-500/10 ${confirmDelete ? 'text-red-400' : 'text-[var(--text-muted)]'} hover:text-red-400 transition-colors`} title={confirmDelete ? '再次点击确认删除' : '删除'}>
<Trash2 className="h-3 w-3" />
</button>
</div>
</div>
{/* content */}
{editing ? (
<div className="flex gap-1.5 mt-1">
<textarea value={editContent} onChange={e => setEditContent(e.target.value)}
className="flex-1 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg px-2 py-1.5 text-[var(--text-primary)] resize-none focus:outline-none focus:border-[var(--accent-cyan)] min-h-[120px]"
autoFocus onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSave() } }} />
<button onClick={handleSave}
className="shrink-0 p-1.5 rounded-lg bg-[var(--accent-cyan)]/10 text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan)]/20 transition-colors" title="保存">
<Check className="h-3.5 w-3.5" />
</button>
</div>
) : (
<p className="text-sm text-[var(--text-secondary)] leading-relaxed">{memory.content}</p>
)}
</div>
</div>
)
}
/* ── Add memory form ───────────────────────────────────── */
function AddMemoryForm({ onAdd, onCancel }: { onAdd: (ns: string, key: string, content: string) => void; onCancel: () => void }) {
const [ns, setNs] = useState('user')
const [key, setKey] = useState('')
const [content, setContent] = useState('')
const handleSubmit = () => {
if (!key.trim() || !content.trim()) return
onAdd(ns, key.trim(), content.trim())
}
return (
<div className="rounded-xl border border-[var(--border-accent)] bg-[var(--bg-tertiary)]/80 p-3 space-y-2.5 animate-fade-in">
<div className="flex gap-2">
<select value={ns} onChange={e => setNs(e.target.value)}
className="text-xs bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg px-2 py-1.5 text-[var(--text-primary)]">
{NS_OPTIONS.map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
</select>
<input value={key} onChange={e => setKey(e.target.value)} placeholder="键名 (如 work_preference)"
className="flex-1 text-xs bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg px-2 py-1.5 text-[var(--text-primary)] placeholder:text-[var(--text-muted)]" />
</div>
<textarea value={content} onChange={e => setContent(e.target.value)} placeholder="内容..."
className="w-full text-sm bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg px-2.5 py-2 text-[var(--text-primary)] placeholder:text-[var(--text-muted)] resize-none min-h-[120px]"
autoFocus onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit() } }} />
<div className="flex justify-end gap-1.5">
<button onClick={onCancel} className="px-3 py-1 rounded-lg text-xs text-[var(--text-muted)] hover:bg-[var(--overlay-hover)] transition-colors"></button>
<button onClick={handleSubmit} disabled={!key.trim() || !content.trim()}
className="px-3 py-1 rounded-lg text-xs bg-[var(--accent-cyan)]/10 text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan)]/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"></button>
</div>
</div>
)
}
/* ── Section header ────────────────────────────────────── */
function SectionHeader({ config, count, isCollapsed, onClick }:
{ config: NamespaceConfig; count: number; isCollapsed: boolean; onClick: () => void }) {
const Icon = config.icon
return (
<button onClick={onClick} className="group flex items-center gap-2 w-full py-1.5 rounded-lg transition-colors hover:bg-[var(--overlay-hover)]">
{isCollapsed ? <ChevronRight className="h-3 w-3 text-[var(--text-muted)]" /> : <ChevronDown className="h-3 w-3 text-[var(--text-muted)]" />}
<div className={`flex items-center justify-center w-5 h-5 rounded-md bg-[var(--overlay-hover)] ${config.accent}`}>
<Icon className="h-3 w-3" />
</div>
<span className="text-xs font-semibold text-[var(--text-primary)] tracking-tight">{config.label}</span>
<span className="text-[10px] text-[var(--text-muted)] font-mono tabular-nums ml-auto">{count}</span>
</button>
)
}
/* ── main component ────────────────────────────────────── */
export function MemoryPanel({ memories, onRefresh, onClose, onCreateMemory, onUpdateMemory, onDeleteMemory, sendCommand }: MemoryPanelProps) {
const [collapsed, setCollapsed] = useState<Set<string>>(() => {
try { const s = localStorage.getItem('picobot-memory-collapsed'); return s ? new Set(JSON.parse(s)) : new Set() }
catch (_) { return new Set() }
})
const [showAddForm, setShowAddForm] = useState(false)
const toggle = (ns: string) => {
setCollapsed(prev => {
const next = new Set(prev)
if (next.has(ns)) { next.delete(ns) } else { next.add(ns) }
localStorage.setItem('picobot-memory-collapsed', JSON.stringify([...next]))
return next
})
}
const grouped = new Map<string, MemorySummary[]>()
for (const m of memories) { const l = grouped.get(m.namespace) || []; l.push(m); grouped.set(m.namespace, l) }
const order = ['user', 'semantic', 'episodic', 'skill', 'environment', 'reflection', 'other']
const sorted = Array.from(grouped.keys()).sort((a, b) => {
const ai = order.indexOf(a); const bi = order.indexOf(b)
if (ai !== -1 && bi !== -1) return ai - bi
if (ai !== -1) return -1; if (bi !== -1) return 1
return a.localeCompare(b)
})
const handleCreate = (ns: string, key: string, content: string) => {
sendCommand(onCreateMemory(ns, key, content))
setShowAddForm(false)
}
const handleUpdate = (id: string, content: string) => { sendCommand(onUpdateMemory(id, content)) }
const handleDelete = (id: string) => { sendCommand(onDeleteMemory(id)) }
return (
<div className="flex h-full flex-col">
{/* title bar */}
<div className="shrink-0 border-b border-[var(--border-color)] px-4 py-3 flex items-center gap-2.5">
<div className="flex items-center justify-center w-6 h-6 rounded-lg bg-[var(--accent-cyan)]/10">
<Brain className="h-3.5 w-3.5 text-[var(--accent-cyan)]" />
</div>
<span className="text-sm font-bold text-[var(--text-primary)] tracking-tight"></span>
{memories.length > 0 && <span className="text-[11px] font-mono text-[var(--text-muted)] tabular-nums ml-0.5">{memories.length}</span>}
<div className="ml-auto flex items-center gap-0.5">
<button onClick={() => setShowAddForm(!showAddForm)} className={`p-1.5 rounded-lg transition-colors ${showAddForm ? 'bg-[var(--accent-cyan)]/10 text-[var(--accent-cyan)]' : 'text-[var(--text-muted)] hover:bg-[var(--overlay-hover)] hover:text-[var(--accent-cyan)]'}`} title="新增记忆">
<Plus className="h-3.5 w-3.5" />
</button>
<button onClick={onRefresh} className="p-1.5 rounded-lg hover:bg-[var(--overlay-hover)] text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors" title="刷新">
<RefreshCw className="h-3.5 w-3.5" />
</button>
{onClose && <button onClick={onClose} className="p-1.5 rounded-lg hover:bg-[var(--overlay-hover)] text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors" title="收起"><X className="h-3.5 w-3.5" /></button>}
</div>
</div>
{/* add form */}
{showAddForm && <div className="px-3 pt-2"><AddMemoryForm onAdd={handleCreate} onCancel={() => setShowAddForm(false)} /></div>}
{/* empty */}
{memories.length === 0 && !showAddForm && (
<div className="flex flex-1 items-center justify-center p-8">
<div className="text-center select-none">
<div className="relative inline-block mb-5">
<div className="absolute inset-0 rounded-full bg-[var(--accent-cyan)]/10 blur-xl animate-pulse" />
<Brain className="relative h-12 w-12 text-[var(--accent-cyan)]/25" />
</div>
<p className="text-sm text-[var(--text-muted)] leading-relaxed">PicoBot <br /></p>
</div>
</div>
)}
{/* list */}
{memories.length > 0 && (
<div className="flex-1 overflow-y-auto px-3 py-2 space-y-3">
{sorted.map(ns => {
const c = cfg(ns)
const items = grouped.get(ns)!
const closed = collapsed.has(ns)
return (
<div key={ns}>
<SectionHeader config={c} count={items.length} isCollapsed={closed} onClick={() => toggle(ns)} />
{!closed && (
<div className="mt-1.5 space-y-1.5">
{items.map(m => <MemoryCard key={m.id} memory={m} config={c} onUpdate={handleUpdate} onDelete={handleDelete} />)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
)
}

View File

@ -16,8 +16,6 @@ import type {
TaskMessagesLoaded,
TaskStarted,
Attachment,
MemorySummary,
MemoryList,
SchedulerJobList,
SchedulerJobSummary,
SchedulerJobSessionLookup,
@ -79,13 +77,6 @@ interface UseChatReturn {
enterSubAgentView: (taskId: string, description: string) => Command
exitSubAgentView: () => void
// 记忆状态
memories: MemorySummary[]
requestMemoryList: () => Command
createMemory: (namespace: string, key: string, content: string) => Command
updateMemory: (id: string, content: string) => Command
deleteMemory: (id: string) => Command
// 定时任务状态
schedulerJobs: SchedulerJobSummary[]
sidebarTab: 'topics' | 'scheduler'
@ -132,7 +123,6 @@ export function useChat(): UseChatReturn {
const [sessions, setSessions] = useState<SessionSummary[]>([])
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null)
const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null)
const [memories, setMemories] = useState<MemorySummary[]>([])
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
@ -522,11 +512,6 @@ export function useChat(): UseChatReturn {
setSchedulerJobs(msg.jobs)
break
}
case 'memory_list': {
const msg = message as MemoryList
setMemories(msg.memories)
break
}
case 'channel_list': {
const msg = message as ChannelList
@ -680,23 +665,6 @@ export function useChat(): UseChatReturn {
setSubAgentView(null)
}, [])
// 记忆方法
const requestMemoryList = useCallback((): Command => {
return { type: 'list_memories' }
}, [])
const createMemory = useCallback((namespace: string, key: string, content: string): Command => {
return { type: 'create_memory', namespace, key, content }
}, [])
const updateMemory = useCallback((id: string, content: string): Command => {
return { type: 'update_memory', id, content }
}, [])
const deleteMemory = useCallback((id: string): Command => {
return { type: 'delete_memory', id }
}, [])
// 定时任务方法
const requestSchedulerJobList = useCallback((): Command => {
return { type: 'list_scheduler_jobs' }
@ -778,11 +746,6 @@ export function useChat(): UseChatReturn {
selectSession,
enterSubAgentView,
exitSubAgentView,
memories,
requestMemoryList,
createMemory,
updateMemory,
deleteMemory,
schedulerJobs,
sidebarTab,
setSidebarTab,

View File

@ -173,20 +173,6 @@ export interface Pong {
type: 'pong'
}
export interface MemorySummary {
id: string
namespace: string
memory_key: string
content: string
created_at: number
updated_at: number
}
export interface MemoryList {
type: 'memory_list'
memories: MemorySummary[]
}
export interface SchedulerJobSessionLookup {
channel: string
chat_id: string
@ -243,7 +229,6 @@ export type WsOutbound =
| ChannelList
| TaskMessagesLoaded
| SchedulerJobList
| MemoryList
| ExecutionCancelled
| Pong
@ -331,28 +316,6 @@ export interface StopExecutionCommand {
type: 'stop_execution'
}
export interface ListMemoriesCommand {
type: 'list_memories'
}
export interface CreateMemoryCommand {
type: 'create_memory'
namespace: string
key: string
content: string
}
export interface UpdateMemoryCommand {
type: 'update_memory'
id: string
content: string
}
export interface DeleteMemoryCommand {
type: 'delete_memory'
id: string
}
export type Command =
| CreateSessionCommand
| ListSessionsCommand
@ -370,10 +333,6 @@ export type Command =
| LoadChatMessagesCommand
| DeleteTopicCommand
| StopExecutionCommand
| ListMemoriesCommand
| CreateMemoryCommand
| UpdateMemoryCommand
| DeleteMemoryCommand
// ============================================================================
// UI Types