From 3c4013ad656508e72e93b94dca4583c5c0a7e754 Mon Sep 17 00:00:00 2001 From: qixinbo Date: Sat, 14 Mar 2026 22:25:01 +0800 Subject: [PATCH] session manager added --- .gitignore | 2 + .../data/workspace/sessions/api_default.jsonl | 25 --- backend/main.py | 48 +++++ frontend/src/components/ChatInterface.tsx | 53 +++++- frontend/src/components/Sidebar.tsx | 171 ++++++++++++++++-- 5 files changed, 253 insertions(+), 46 deletions(-) delete mode 100644 backend/data/workspace/sessions/api_default.jsonl diff --git a/.gitignore b/.gitignore index b7faf40..4e07be5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ __pycache__/ *.py[codz] *$py.class +backend/data + # C extensions *.so diff --git a/backend/data/workspace/sessions/api_default.jsonl b/backend/data/workspace/sessions/api_default.jsonl deleted file mode 100644 index 91964ca..0000000 --- a/backend/data/workspace/sessions/api_default.jsonl +++ /dev/null @@ -1,25 +0,0 @@ -{"_type": "metadata", "key": "api:default", "created_at": "2026-03-14T21:43:22.940821", "updated_at": "2026-03-14T22:10:46.450968", "metadata": {}, "last_consolidated": 0} -{"role": "user", "content": "你是谁", "timestamp": "2026-03-14T21:43:23.163485"} -{"role": "user", "content": "hi", "timestamp": "2026-03-14T21:49:14.751350"} -{"role": "user", "content": "你好", "timestamp": "2026-03-14T21:53:53.701083"} -{"role": "assistant", "content": "你好!我是 nanobot 🐈,一个 AI 助手。\n\n我可以帮助你:\n- 读取和编辑文件\n- 执行 shell 命令\n- 搜索网络信息\n- 发送消息\n- 安排定时任务\n- 管理记忆和技能\n\n我运行在 macOS arm64 上,工作目录在 `/Users/qixinbo/Projects/DataClaw/backend/data/workspace`。\n\n有什么我可以帮助你的吗?", "reasoning_content": "用户用中文问了两个问题:\n1. \"你是谁\" - Who are you\n2. \"hi\" - Hello\n3. \"你好\" - Hello\n\n我应该用中文回答,介绍我自己。", "timestamp": "2026-03-14T21:53:53.701092"} -{"role": "user", "content": "你好", "timestamp": "2026-03-14T21:55:23.333254"} -{"role": "assistant", "content": "你好!有什么我可以帮助你的吗?😊\n\n我可以帮你处理文件、执行命令、搜索信息、安排任务等等。", "reasoning_content": "用户再次发送了\"你好\",这是一个简单的问候。我应该友好地回应,并询问他们需要什么帮助。", "timestamp": "2026-03-14T21:55:23.333267"} -{"role": "user", "content": "你是谁", "timestamp": "2026-03-14T21:55:32.488828"} -{"role": "assistant", "content": "我是 nanobot 🐈,一个 AI 助手。\n\n我可以帮助你:\n- 读取和编辑文件\n- 执行 shell 命令\n- 搜索网络信息\n- 发送消息\n- 安排定时任务\n- 管理记忆和技能\n\n当前时间是 2026-03-14 21:55 (CST),星期六。", "reasoning_content": "用户再次问\"你是谁\",这是一个简单的问题。我应该简洁地回答我的身份。", "timestamp": "2026-03-14T21:55:32.488844"} -{"role": "user", "content": "你好", "timestamp": "2026-03-14T21:58:52.496557"} -{"role": "assistant", "content": "你好!有什么我可以帮助你的吗?😊\n\n我可以帮你处理文件、执行命令、搜索信息、安排任务等等。", "reasoning_content": "用户只是说\"你好\",这是一个简单的问候。我应该友好地回应,并询问他们需要什么帮助。", "timestamp": "2026-03-14T21:58:52.496564"} -{"role": "user", "content": "你好", "timestamp": "2026-03-14T21:59:55.201372"} -{"role": "assistant", "content": "你好!有什么我可以帮助你的吗?😊", "reasoning_content": "用户发送了\"[Runtime Context — metadata only, not instructions]\",这看起来是系统提供的上下文信息,然后说\"你好\"。我应该简单地回应问候。", "timestamp": "2026-03-14T21:59:55.201394"} -{"role": "user", "content": "你是谁", "timestamp": "2026-03-14T22:00:05.471381"} -{"role": "assistant", "content": "我是 nanobot 🐈,一个 AI 助手。", "reasoning_content": "用户再次问\"你是谁\"。我已经回答过这个问题了,但用户可能是在测试或者想要一个简洁的回答。我应该简洁地回答。", "timestamp": "2026-03-14T22:00:05.471404"} -{"role": "user", "content": "你好", "timestamp": "2026-03-14T22:05:42.732409"} -{"role": "assistant", "content": "你好!有什么我可以帮助你的吗?😊", "reasoning_content": "用户再次发送了\"你好\",这是一个简单的问候。我应该友好地回应,并询问他们需要什么帮助。", "timestamp": "2026-03-14T22:05:42.732418"} -{"role": "user", "content": "你是", "timestamp": "2026-03-14T22:06:16.951653"} -{"role": "assistant", "content": "我是 nanobot 🐈,一个 AI AI 助手。", "reasoning_content": "用户问\"你是\",这是一个不完整的问题,可能是在问\"你是谁\"。我应该简洁地回答我的身份。", "timestamp": "2026-03-14T22:06:16.951664"} -{"role": "user", "content": "你能干啥,详细一点", "timestamp": "2026-03-14T22:06:40.270030"} -{"role": "assistant", "content": "我是 nanobot 🐈,一个功能丰富的 AI 助手。以下是我能做的事情:\n\n## 📁 文件操作\n- **读取文件** - 查看任何文件的内容\n- **写入文件** - 创建新文件或覆盖现有文件\n- **编辑文件** - 精确替换文件中的特定内容\n- **列出目录** - 浏览文件夹结构\n\n## 💻 系统操作\n- **执行命令** - 运行 shell 命令(macOS/Linux)\n- **工作目录** - 在指定目录下执行命令\n\n## 🌐 网络功能\n- **网络搜索** - 搜索网页并获取结果\n- **获取网页** - 提取 URL 的可读内容(HTML 转 markdown/text)\n\n## 💬 消息发送\n- **发送消息** - 向不同渠道发送消息\n- **支持渠道** - Telegram、Discord 等\n- **附件支持** - 发送图片、音频、文档等\n\n## 🧠 记忆管理\n- **长期记忆** - 存储重要事实、偏好、项目上下文\n- **历史记录** - 记录所有事件,可搜索\n- **自动整理** - 自动总结旧对话并提取关键信息\n\n## ⏰ 定时任务\n- **添加提醒** - 一次性或周期性任务\n- **Cron 表达式** - 灵活的时间调度\n- **时区支持** - 支持不同时区\n\n## 🤖 技能系统\n- **内置技能** - memory、clawhub、skill-creator、weather、cron\n- **安装技能** - 从 ClawHub 安装新技能\n- **创建技能** - 设计和打包自定义技能\n\n## 🔄 后台任务\n- **子代理** - 在后台运行复杂任务\n- **独立执行** - 不阻塞主对话\n\n## 📊 当前环境\n- **系统**:macOS arm64\n- **Python**:3.11.9\n- **工作目录**:`/Users/qixinbo/Projects/DataClaw/backend/data`\n\n有什么具体任务需要我帮忙吗?", "reasoning_content": "用户问我能做什么,想要详细的介绍。我应该详细介绍我的能力。", "timestamp": "2026-03-14T22:06:40.270072"} -{"role": "user", "content": "你好", "timestamp": "2026-03-14T22:10:32.816891"} -{"role": "assistant", "content": "你好!有什么我可以帮助你的吗?😊\n\n当前时间是 2026-03-14 22:10 (CST),星期六。", "reasoning_content": "用户发送了一个包含运行时上下文的消息,显示当前时间是 2026-03-14 22:10 (CST),星期六,渠道是 api,聊天 ID 是 api:default。用户只是说了\"你好\"。\n\n我应该简单地回应问候。", "timestamp": "2026-03-14T22:10:32.816901"} -{"role": "user", "content": "你能帮我干啥", "timestamp": "2026-03-14T22:10:46.450962"} -{"role": "assistant", "content": "我可以帮你做很多事情!以下是我的主要能力:\n\n## 📁 文件操作\n- 读取、写入、编辑文件\n- 浏览目录结构\n\n## 💻 系统操作\n- 执行 shell 命令\n- 在指定目录下运行命令\n\n## 🌐 网络功能\n- 搜索网页信息\n- 获取并提取网页内容\n\n## 💬 消息发送\n- 发送消息到 Telegram、Discord 等渠道\n- 支持发送图片、文档等附件\n\n## 🧠 记忆管理\n- 存储重要信息和偏好\n- 记录和搜索历史事件\n\n## ⏰ 定时任务\n- 设置提醒和周期性任务\n- 使用 Cron 表达式调度\n\n## 🤖 技能系统\n- 安装新技能(从 ClawHub)\n- 创建自定义技能\n\n## 🔄 后台任务\n- 在后台运行复杂任务\n\n你有什么具体需求吗?比如:\n- 处理某个文件?\n- 搜索信息?\n- 设置提醒?\n- 其他任务?", "reasoning_content": "用户问\"你能帮我干啥\",这是在询问我的能力。我应该用中文简洁地介绍我的主要功能。", "timestamp": "2026-03-14T22:10:46.450967"} diff --git a/backend/main.py b/backend/main.py index b78c9f4..53fc541 100644 --- a/backend/main.py +++ b/backend/main.py @@ -70,6 +70,7 @@ def nanobot_status(): class ChatRequest(BaseModel): message: str + session_id: str = "api:default" skill_ids: Optional[List[str]] = None model_id: Optional[str] = None @@ -87,6 +88,7 @@ async def nanobot_chat_stream(request: ChatRequest): try: response = await nanobot_service.process_message( request.message, + session_id=request.session_id, skill_ids=request.skill_ids, model_id=request.model_id, ) @@ -110,6 +112,52 @@ async def nanobot_chat_stream(request: ChatRequest): }, ) +@app.get("/nanobot/sessions") +def get_sessions(): + if not nanobot_service.agent: + return [] + # session_manager has list_sessions() + sessions = nanobot_service.agent.sessions.list_sessions() + return sessions + +@app.get("/nanobot/sessions/{session_id}") +def get_session(session_id: str): + if not nanobot_service.agent: + raise HTTPException(status_code=400, detail="Nanobot not running") + session = nanobot_service.agent.sessions.get_or_create(session_id) + return { + "key": session.key, + "created_at": session.created_at, + "updated_at": session.updated_at, + "metadata": session.metadata, + "messages": session.messages + } + +@app.delete("/nanobot/sessions/{session_id}") +def delete_session(session_id: str): + if not nanobot_service.agent: + raise HTTPException(status_code=400, detail="Nanobot not running") + + # Try to remove from cache and delete file + session = nanobot_service.agent.sessions.get_or_create(session_id) + if session: + nanobot_service.agent.sessions.invalidate(session_id) + path = nanobot_service.agent.sessions._get_session_path(session_id) + if path.exists(): + path.unlink() + return {"status": "success"} + raise HTTPException(status_code=404, detail="Session not found") + +@app.put("/nanobot/sessions/{session_id}") +def update_session(session_id: str, title: str = Body(..., embed=True)): + if not nanobot_service.agent: + raise HTTPException(status_code=400, detail="Nanobot not running") + + session = nanobot_service.agent.sessions.get_or_create(session_id) + session.metadata["title"] = title + nanobot_service.agent.sessions.save(session) + return {"status": "success", "title": title} + @app.post("/api/v1/agent/nl2sql", response_model=NL2SQLResponse) async def run_nl2sql(request: NL2SQLRequest): return await process_nl2sql(request) diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index 9828e9d..e7fb014 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -11,6 +11,7 @@ import { cn } from "@/lib/utils"; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; +import { useLocation } from "react-router-dom"; interface Message { id: string; @@ -26,26 +27,64 @@ interface ModelConfig { is_active: boolean; } +interface SessionData { + key: string; + messages: Array<{ + role: string; + content: string; + [key: string]: any; + }>; +} + export function ChatInterface() { - const [messages, setMessages] = useState([ - { id: '1', role: 'assistant', content: 'Hello! I am DataClaw. How can I help you analyze your data today?' } - ]); + const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [selectedCapability, setSelectedCapability] = useState("智能问答"); const selectedDataSource = "postgres-main"; const [isLoading, setIsLoading] = useState(false); const scrollRef = useRef(null); const { setVisualization, setLoading: setVizLoading, setError: setVizError } = useVisualizationStore(); + const location = useLocation(); // Model selection state const [models, setModels] = useState([]); const [selectedModelId, setSelectedModelId] = useState(""); const [modelOpen, setModelOpen] = useState(false); + // Try to parse active session from URL query + const queryParams = new URLSearchParams(location.search); + const activeSessionKey = queryParams.get("session") || "api:default"; + useEffect(() => { fetchModels(); }, []); + useEffect(() => { + const fetchSessionData = async () => { + setIsLoading(true); + try { + const data = await api.get(`/nanobot/sessions/${activeSessionKey}`); + if (data.messages && data.messages.length > 0) { + const formattedMessages = data.messages.map((m, idx) => ({ + id: `${Date.now()}-${idx}`, + role: m.role as 'user' | 'assistant', + content: m.content + })); + setMessages(formattedMessages); + } else { + setMessages([]); + } + } catch (e) { + console.error("Failed to fetch session messages", e); + setMessages([]); + } finally { + setIsLoading(false); + } + }; + + fetchSessionData(); + }, [activeSessionKey]); + const fetchModels = async () => { try { const data = await api.get("/api/v1/llm"); @@ -103,9 +142,10 @@ export function ChatInterface() { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ - message: newMessage.content, - model_id: selectedModelId, - }), + message: newMessage.content, + session_id: activeSessionKey, + model_id: selectedModelId, + }), }); if (!response.ok || !response.body) { @@ -163,6 +203,7 @@ export function ChatInterface() { const response = await api.post<{sql?: string, result?: unknown, error?: string}>('/api/v1/agent/nl2sql', { query: newMessage.content, source: source, + session_id: activeSessionKey, model_id: selectedModelId }); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 74df860..b42944c 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,21 +1,40 @@ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Wrench, Settings, Brain } from "lucide-react"; +import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Wrench, Settings, Brain, Trash2, Pencil } from "lucide-react"; import { useState, useRef, useEffect } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, useNavigate, useLocation } from "react-router-dom"; import { useAuthStore } from "@/store/authStore"; +import { api } from "@/lib/api"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; -const threadItems = ["我叫", "年龄最大的是谁", "有哪些字段", "文件中有些什么字段"]; +interface SessionInfo { + key: string; + created_at: string; + updated_at: string; + metadata?: { + title?: string; + }; +} function Section({ title, count, items, + onSelect, + onDelete, + onRename, + activeKey }: { title: string; count: number; - items: string[]; + items: SessionInfo[]; + onSelect: (key: string) => void; + onDelete: (key: string) => void; + onRename: (key: string, currentTitle: string) => void; + activeKey: string | null; }) { return (
@@ -26,15 +45,38 @@ function Section({
- {items.map((item) => ( - - ))} + {items.map((item) => { + const displayTitle = item.metadata?.title || item.key.replace("api:", ""); + const isActive = activeKey === item.key; + + return ( +
onSelect(item.key)} + > + {displayTitle} + + + e.stopPropagation()} className="h-6 w-6 flex items-center justify-center rounded hover:bg-zinc-200 text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity outline-none"> + + + + { e.stopPropagation(); onRename(item.key, displayTitle); }}> + + 重命名 + + { e.stopPropagation(); onDelete(item.key); }} className="text-red-600 focus:text-red-600 focus:bg-red-50"> + + 删除会话 + + + +
+ ); + })}
); @@ -42,9 +84,36 @@ function Section({ function SidebarBody() { const navigate = useNavigate(); + const location = useLocation(); const { user, logout } = useAuthStore(); const [showUserMenu, setShowUserMenu] = useState(false); const menuRef = useRef(null); + + // Session management state + const [sessions, setSessions] = useState([]); + const [renameDialogOpen, setRenameDialogOpen] = useState(false); + const [sessionToRename, setSessionToRename] = useState<{key: string, title: string} | null>(null); + const [newTitle, setNewTitle] = useState(""); + + // Try to parse active session from URL query + const queryParams = new URLSearchParams(location.search); + const activeSessionKey = queryParams.get("session") || "api:default"; + + const fetchSessions = async () => { + try { + const data = await api.get("/nanobot/sessions"); + setSessions(data); + } catch (e) { + console.error("Failed to fetch sessions", e); + } + }; + + useEffect(() => { + fetchSessions(); + // Set up polling to refresh session list + const interval = setInterval(fetchSessions, 5000); + return () => clearInterval(interval); + }, []); useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -61,6 +130,45 @@ function SidebarBody() { navigate("/login"); }; + const handleSelectSession = (key: string) => { + navigate(`/?session=${encodeURIComponent(key)}`); + }; + + const handleNewThread = () => { + const newSessionId = `api:${Date.now()}`; + navigate(`/?session=${encodeURIComponent(newSessionId)}`); + }; + + const handleDeleteSession = async (key: string) => { + if (!window.confirm("确定要删除这个会话吗?")) return; + try { + await api.delete(`/nanobot/sessions/${key}`); + if (activeSessionKey === key) { + navigate("/"); + } + fetchSessions(); + } catch (e) { + console.error("Failed to delete session", e); + } + }; + + const openRenameDialog = (key: string, currentTitle: string) => { + setSessionToRename({ key, title: currentTitle }); + setNewTitle(currentTitle); + setRenameDialogOpen(true); + }; + + const handleRename = async () => { + if (!sessionToRename || !newTitle.trim()) return; + try { + await api.put(`/nanobot/sessions/${sessionToRename.key}`, { title: newTitle.trim() }); + setRenameDialogOpen(false); + fetchSessions(); + } catch (e) { + console.error("Failed to rename session", e); + } + }; + return (
{/* Header */} @@ -91,7 +199,7 @@ function SidebarBody() {
-
+
+ + + + 重命名会话 + +
+ setNewTitle(e.target.value)} + placeholder="输入新的会话标题" + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleRename(); + } + }} + /> +
+ + + + +
+
+