From a325098d1212d3695fbe319e83fb9d9c5ffbe179 Mon Sep 17 00:00:00 2001 From: wuyang6 Date: Wed, 13 May 2026 14:04:42 +0800 Subject: [PATCH] Add personal memory and admin dashboard --- backend/api/server.py | 441 +++++++++++ frontend/app/app/admin/page.tsx | 583 ++++++++++++++ frontend/app/app/api/admin/[...path]/route.ts | 52 ++ .../claw/memory/skills/[skillName]/route.ts | 55 ++ .../app/app/api/claw/memory/skills/route.ts | 19 + .../app/app/api/claw/memory/user/route.ts | 44 ++ .../assistant-ui/threadlist-sidebar.tsx | 150 ++++ src/personal_memory.py | 737 ++++++++++++++++++ 8 files changed, 2081 insertions(+) create mode 100644 frontend/app/app/admin/page.tsx create mode 100644 frontend/app/app/api/admin/[...path]/route.ts create mode 100644 frontend/app/app/api/claw/memory/skills/[skillName]/route.ts create mode 100644 frontend/app/app/api/claw/memory/skills/route.ts create mode 100644 frontend/app/app/api/claw/memory/user/route.ts create mode 100644 src/personal_memory.py diff --git a/backend/api/server.py b/backend/api/server.py index cd76af3..f47c30d 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -8,6 +8,7 @@ from __future__ import annotations import asyncio import csv +import hashlib import json import os import queue @@ -48,6 +49,11 @@ from src.jupyter_runtime import ( ) from src.mcp_runtime import MCPRuntime, MCPServerProfile from src.openai_compat import OpenAICompatClient, OpenAICompatError +from src.personal_memory import ( + SKILL_MEMORY_DIRNAME, + USER_MEMORY_FILENAME, + PersonalMemoryManager, +) from src.session_store import ( DEFAULT_AGENT_SESSION_DIR, StoredAgentSession, @@ -514,6 +520,11 @@ class AgentState: allow_shell=allow_shell, allow_write=allow_write, ) + self.memory_manager = PersonalMemoryManager( + self.session_directory.parent / 'accounts', + self.config_for, + ) + self.memory_manager.start() @property def cwd(self) -> Path: @@ -582,6 +593,7 @@ class AgentState: 'uploads': self.session_directory, 'outputs': self.session_directory, 'python_env': base / 'python' / '.venv', + 'memory': base / 'memory', } base = self._account_base(account_id) return { @@ -591,6 +603,7 @@ class AgentState: 'uploads': base / 'sessions', 'outputs': base / 'sessions', 'python_env': base / 'python' / '.venv', + 'memory': base / 'memory', } def _skill_settings_path(self, account_id: str | None) -> Path: @@ -955,6 +968,21 @@ class FeishuOnlineDocRequest(BaseModel): account_id: str | None = None +class AdminLoginRequest(BaseModel): + username: str = Field(min_length=1) + password: str = Field(min_length=1) + + +class AdminAccountCreateRequest(BaseModel): + account_id: str = Field(min_length=2) + + +class MemoryUpdateRequest(BaseModel): + account_id: str = Field(min_length=1) + content: str + skill: str | None = None + + def _append_runtime_context(current: str | None, addition: str) -> str: parts = [part.strip() for part in (current, addition) if part and part.strip()] return '\n\n'.join(parts) @@ -1163,6 +1191,158 @@ def create_app(state: AgentState) -> FastAPI: ) return result + # ------------- memory ---------------------------------------------------- + @app.get('/api/memory/user') + async def get_user_memory(account_id: str) -> dict[str, Any]: + try: + return state.memory_manager.list_user_memory(_safe_account_id(account_id)) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.put('/api/memory/user') + async def put_user_memory(payload: MemoryUpdateRequest) -> dict[str, Any]: + try: + return state.memory_manager.update_user_memory( + _safe_account_id(payload.account_id), + payload.content, + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get('/api/memory/skills') + async def get_skill_memories(account_id: str) -> list[dict[str, Any]]: + try: + return state.memory_manager.list_skill_memories(_safe_account_id(account_id)) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get('/api/memory/skills/{skill_name}') + async def get_skill_memory(skill_name: str, account_id: str) -> dict[str, Any]: + try: + return state.memory_manager.read_skill_memory( + _safe_account_id(account_id), + skill_name, + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.put('/api/memory/skills/{skill_name}') + async def put_skill_memory( + skill_name: str, + payload: MemoryUpdateRequest, + ) -> dict[str, Any]: + try: + return state.memory_manager.update_skill_memory( + _safe_account_id(payload.account_id), + skill_name, + payload.content, + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.delete('/api/memory/skills/{skill_name}') + async def delete_skill_memory(skill_name: str, account_id: str) -> dict[str, Any]: + try: + return state.memory_manager.delete_skill_memory( + _safe_account_id(account_id), + skill_name, + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get('/api/memory/queue') + async def get_memory_queue(account_id: str | None = None) -> dict[str, Any]: + return state.memory_manager.queue_snapshot( + _safe_account_id(account_id) if account_id else None + ) + + @app.get('/api/memory/events') + async def get_memory_events( + account_id: str, + limit: int = 50, + ) -> list[dict[str, Any]]: + return state.memory_manager.list_recent_events( + _safe_account_id(account_id), + limit=limit, + ) + + # ------------- admin ----------------------------------------------------- + @app.post('/api/admin/login') + async def admin_login(payload: AdminLoginRequest) -> dict[str, Any]: + if payload.username == 'admin' and payload.password == 'admin': + return {'token': _admin_token(), 'username': 'admin'} + raise HTTPException(status_code=401, detail='管理员账号或密码错误') + + @app.get('/api/admin/summary') + async def admin_summary(token: str) -> dict[str, Any]: + _require_admin_token(token) + return _build_admin_summary(state) + + @app.get('/api/admin/accounts') + async def admin_accounts(token: str) -> list[dict[str, Any]]: + _require_admin_token(token) + return _list_admin_accounts(state) + + @app.post('/api/admin/accounts') + async def admin_create_account( + token: str, + payload: AdminAccountCreateRequest, + ) -> dict[str, Any]: + _require_admin_token(token) + return _admin_create_account(state, payload.account_id) + + @app.delete('/api/admin/accounts/{account_id}') + async def admin_delete_account(account_id: str, token: str) -> dict[str, Any]: + _require_admin_token(token) + return _admin_delete_account(state, account_id) + + @app.get('/api/admin/memory') + async def admin_memory(token: str, account_id: str | None = None) -> dict[str, Any]: + _require_admin_token(token) + safe_account = _safe_account_id(account_id) if account_id else None + return { + 'queue': state.memory_manager.queue_snapshot(safe_account), + 'events': ( + state.memory_manager.list_recent_events(safe_account, limit=100) + if safe_account + else [] + ), + } + + @app.get('/api/admin/memory/user') + async def admin_user_memory(token: str, account_id: str) -> dict[str, Any]: + _require_admin_token(token) + return state.memory_manager.list_user_memory(_safe_account_id(account_id)) + + @app.put('/api/admin/memory/user') + async def admin_update_user_memory( + token: str, + payload: MemoryUpdateRequest, + ) -> dict[str, Any]: + _require_admin_token(token) + return state.memory_manager.update_user_memory( + _safe_account_id(payload.account_id), + payload.content, + ) + + @app.get('/api/admin/memory/skills') + async def admin_skill_memories(token: str, account_id: str) -> list[dict[str, Any]]: + _require_admin_token(token) + return state.memory_manager.list_skill_memories(_safe_account_id(account_id)) + + @app.put('/api/admin/memory/skills/{skill_name}') + async def admin_update_skill_memory( + skill_name: str, + token: str, + payload: MemoryUpdateRequest, + ) -> dict[str, Any]: + _require_admin_token(token) + return state.memory_manager.update_skill_memory( + _safe_account_id(payload.account_id), + skill_name, + payload.content, + ) + @app.get('/api/models') async def list_models_get(account_id: str | None = None) -> dict[str, Any]: config = state.config_for(account_id) @@ -1450,6 +1630,12 @@ def create_app(state: AgentState) -> FastAPI: requested_session_id, ) runtime_context = request.runtime_context + memory_context = state.memory_manager.render_injection( + request.account_id, + state.enabled_skill_names(request.account_id), + ) + if memory_context: + runtime_context = _append_runtime_context(runtime_context, memory_context) if jupyter_runtime is not None: runtime_context = _append_runtime_context( runtime_context, @@ -1575,6 +1761,9 @@ def create_app(state: AgentState) -> FastAPI: elapsed_ms = max(0, int((time.perf_counter() - started_at) * 1000)) payload = _serialize_run_result(result) payload['elapsed_ms'] = elapsed_ms + used_skills = _extract_used_skill_names(payload.get('transcript') or ()) + if not used_skills: + used_skills = tuple(state.enabled_skill_names(request.account_id)) if result.session_id: _annotate_last_assistant_elapsed( session_directory, @@ -1591,6 +1780,17 @@ def create_app(state: AgentState) -> FastAPI: fallback_title=fallback_title, ) _annotate_transcript_elapsed(payload, elapsed_ms) + try: + state.memory_manager.enqueue_interaction( + account_id=request.account_id, + session_id=result.session_id, + user_prompt=prompt, + assistant_output=result.final_output, + skills=used_skills, + model=config.model, + ) + except Exception: + pass state.run_manager.finish( run_record.run_id, 'cancelled' if run_record.cancel_event.is_set() else 'completed', @@ -3288,6 +3488,213 @@ def _safe_account_id(account_id: str | None) -> str: return normalized[:80] or 'default' +def _admin_token() -> str: + return os.environ.get('ZK_ADMIN_TOKEN') or 'admin' + + +def _require_admin_token(token: str) -> None: + if token != _admin_token(): + raise HTTPException(status_code=401, detail='Admin unauthorized') + + +def _accounts_root(state: AgentState) -> Path: + return state.session_directory.parent / 'accounts' + + +def _users_json_path(state: AgentState) -> Path: + return _accounts_root(state) / 'users.json' + + +def _auth_sessions_json_path(state: AgentState) -> Path: + return _accounts_root(state) / 'auth_sessions.json' + + +def _load_users_file(state: AgentState) -> dict[str, Any]: + path = _users_json_path(state) + try: + payload = json.loads(path.read_text(encoding='utf-8')) + except (OSError, json.JSONDecodeError): + payload = {'users': []} + if not isinstance(payload, dict) or not isinstance(payload.get('users'), list): + return {'users': []} + return payload + + +def _save_users_file(state: AgentState, payload: dict[str, Any]) -> None: + path = _users_json_path(state) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') + + +def _hash_admin_created_password(password: str = '123456') -> str: + salt = os.urandom(16).hex() + digest = hashlib.scrypt( + password.encode('utf-8'), + salt=salt.encode('utf-8'), + n=16384, + r=8, + p=1, + dklen=64, + ).hex() + return f'{salt}:{digest}' + + +def _build_admin_summary(state: AgentState) -> dict[str, Any]: + accounts = _list_admin_accounts(state) + totals = { + 'accounts': len(accounts), + 'sessions': sum(item['session_count'] for item in accounts), + 'tool_calls': sum(item['tool_calls'] for item in accounts), + 'tokens': sum(item['total_tokens'] for item in accounts), + } + return { + 'totals': totals, + 'memory_queue': state.memory_manager.queue_snapshot(), + 'accounts': accounts[:20], + } + + +def _list_admin_accounts(state: AgentState) -> list[dict[str, Any]]: + users_payload = _load_users_file(state) + known_users = { + str(item.get('id') or item.get('username') or '').strip() + for item in users_payload.get('users', []) + if isinstance(item, dict) + } + accounts_root = _accounts_root(state) + directory_users = ( + { + path.name + for path in accounts_root.iterdir() + if path.is_dir() and path.name not in {'__pycache__'} + } + if accounts_root.exists() + else set() + ) + account_ids = sorted({item for item in known_users | directory_users if item}) + result: list[dict[str, Any]] = [] + for account_id in account_ids: + base = accounts_root / account_id + sessions_dir = base / 'sessions' + session_files = _iter_session_files(sessions_dir) + session_count = 0 + tool_calls = 0 + total_tokens = 0 + input_tokens = 0 + output_tokens = 0 + latest_mtime = 0.0 + models: dict[str, int] = {} + for path in session_files: + try: + data = json.loads(path.read_text(encoding='utf-8')) + except (OSError, json.JSONDecodeError): + continue + session_count += 1 + tool_calls += int(data.get('tool_calls') or 0) + usage = data.get('usage') if isinstance(data.get('usage'), dict) else {} + input_tokens += int(usage.get('input_tokens') or 0) + output_tokens += int(usage.get('output_tokens') or 0) + total_tokens += int( + usage.get('total_tokens') + or (usage.get('input_tokens') or 0) + (usage.get('output_tokens') or 0) + ) + model_config = data.get('model_config') if isinstance(data.get('model_config'), dict) else {} + model = str(model_config.get('model') or data.get('model') or 'unknown') + models[model] = models.get(model, 0) + 1 + latest_mtime = max(latest_mtime, _session_mtime(path)) + memory_dir = base / 'memory' + result.append( + { + 'account_id': account_id, + 'registered': account_id in known_users, + 'session_count': session_count, + 'tool_calls': tool_calls, + 'input_tokens': input_tokens, + 'output_tokens': output_tokens, + 'total_tokens': total_tokens, + 'latest_session_at': latest_mtime, + 'models': models, + 'user_memory_lines': _count_optional_lines(memory_dir / USER_MEMORY_FILENAME), + 'skill_memory_count': len(list((memory_dir / SKILL_MEMORY_DIRNAME).glob('*.md'))) + if (memory_dir / SKILL_MEMORY_DIRNAME).exists() + else 0, + } + ) + return sorted(result, key=lambda item: item['latest_session_at'], reverse=True) + + +def _admin_create_account(state: AgentState, account_id: str) -> dict[str, Any]: + safe_id = _safe_account_id(account_id) + if safe_id == 'default': + raise HTTPException(status_code=400, detail='账号名不合法') + users = _load_users_file(state) + user_rows = users.setdefault('users', []) + if any( + isinstance(item, dict) + and str(item.get('id') or item.get('username') or '').strip() == safe_id + for item in user_rows + ): + raise HTTPException(status_code=400, detail='账号已存在') + user_rows.append( + { + 'id': safe_id, + 'username': safe_id, + 'passwordHash': _hash_admin_created_password(), + 'createdAt': datetime_utc_iso(), + } + ) + _save_users_file(state, users) + base = _accounts_root(state) / safe_id + (base / 'sessions').mkdir(parents=True, exist_ok=True) + state.memory_manager.ensure_account(safe_id) + return {'account_id': safe_id, 'created': True, 'initial_password': '123456'} + + +def _admin_delete_account(state: AgentState, account_id: str) -> dict[str, Any]: + safe_id = _safe_account_id(account_id) + users = _load_users_file(state) + users['users'] = [ + item + for item in users.get('users', []) + if not ( + isinstance(item, dict) + and str(item.get('id') or item.get('username') or '').strip() == safe_id + ) + ] + _save_users_file(state, users) + sessions_path = _auth_sessions_json_path(state) + try: + auth_sessions = json.loads(sessions_path.read_text(encoding='utf-8')) + except (OSError, json.JSONDecodeError): + auth_sessions = {'sessions': {}} + if isinstance(auth_sessions.get('sessions'), dict): + auth_sessions['sessions'] = { + token: session + for token, session in auth_sessions['sessions'].items() + if not (isinstance(session, dict) and session.get('accountId') == safe_id) + } + sessions_path.write_text( + json.dumps(auth_sessions, ensure_ascii=False, indent=2) + '\n', + encoding='utf-8', + ) + base = _accounts_root(state) / safe_id + if base.exists(): + shutil.rmtree(base) + state._clear_agents_for_account(safe_id) + return {'account_id': safe_id, 'deleted': True} + + +def _count_optional_lines(path: Path) -> int: + try: + return len(path.read_text(encoding='utf-8').splitlines()) + except OSError: + return 0 + + +def datetime_utc_iso() -> str: + return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) + + def _is_internal_message(content: str) -> bool: return content.lstrip().startswith('') @@ -3302,6 +3709,40 @@ def _strip_session_context(content: str) -> str: return content.strip() +def _extract_used_skill_names(transcript: Any) -> tuple[str, ...]: + """从 transcript 中尽量提取实际调用过的 Skill 名称。""" + names: list[str] = [] + if not isinstance(transcript, (list, tuple)): + return () + for entry in transcript: + if not isinstance(entry, dict): + continue + tool_calls = entry.get('tool_calls') + if not isinstance(tool_calls, list): + continue + for call in tool_calls: + if not isinstance(call, dict): + continue + function = call.get('function') if isinstance(call.get('function'), dict) else {} + tool_name = call.get('name') or function.get('name') + if str(tool_name).lower() != 'skill': + continue + arguments = call.get('arguments') or function.get('arguments') + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError: + arguments = {} + if not isinstance(arguments, dict): + continue + for key in ('skill', 'skill_name', 'name'): + value = arguments.get(key) + if isinstance(value, str) and value.strip(): + names.append(value.strip()) + break + return tuple(dict.fromkeys(names)) + + def _iter_session_files(directory: Path) -> list[Path]: if not directory.exists(): return [] diff --git a/frontend/app/app/admin/page.tsx b/frontend/app/app/admin/page.tsx new file mode 100644 index 0000000..f772f9a --- /dev/null +++ b/frontend/app/app/admin/page.tsx @@ -0,0 +1,583 @@ +"use client"; + +import { + ActivityIcon, + DatabaseIcon, + RefreshCwIcon, + SaveIcon, + Trash2Icon, + UserPlusIcon, + UsersIcon, +} from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +type AdminSummary = { + totals?: { + accounts: number; + sessions: number; + tool_calls: number; + tokens: number; + }; + memory_queue?: MemoryQueue; + accounts?: AccountRow[]; +}; + +type AccountRow = { + account_id: string; + registered: boolean; + session_count: number; + tool_calls: number; + input_tokens: number; + output_tokens: number; + total_tokens: number; + latest_session_at: number; + models: Record; + user_memory_lines: number; + skill_memory_count: number; +}; + +type MemoryQueue = { + totals: { + events: number; + pending: number; + processing: number; + done: number; + failed: number; + }; + accounts: Array<{ + account_id: string; + events: number; + pending: number; + processing: number; + done: number; + failed: number; + }>; +}; + +type MemoryEvent = { + id: string; + session_id: string; + skills: string[]; + signals: string[]; + priority: number; + status: string; + error?: string | null; + created_at: string; + updated_at: string; +}; + +type UserMemory = { + content: string; + line_count: number; + updated_at?: string | null; +}; + +type SkillMemory = { + skill: string; + content: string; + line_count: number; + updated_at?: string | null; +}; + +const TOKEN_KEY = "zk-admin-token"; + +export default function AdminPage() { + const [token, setToken] = useState(""); + const [username, setUsername] = useState("admin"); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + setToken(localStorage.getItem(TOKEN_KEY) ?? ""); + }, []); + + async function login() { + setError(""); + setIsLoading(true); + try { + const response = await fetch("/api/admin/login", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + const payload = (await response.json()) as { + token?: string; + detail?: string; + }; + if (!response.ok || !payload.token) { + throw new Error(payload.detail ?? "登录失败"); + } + localStorage.setItem(TOKEN_KEY, payload.token); + setToken(payload.token); + } catch (err) { + setError(err instanceof Error ? err.message : "登录失败"); + } finally { + setIsLoading(false); + } + } + + if (!token) { + return ( +
+
+
+

管理后台

+

+ 查看用户、用量、会话、工具调用和记忆队列。 +

+
+
+ setUsername(event.target.value)} + placeholder="管理员账号" + /> + setPassword(event.target.value)} + placeholder="管理员密码" + onKeyDown={(event) => { + if (event.key === "Enter") login(); + }} + /> + {error ?

{error}

: null} + +
+
+
+ ); + } + + return setToken("")} />; +} + +function AdminDashboard({ + token, + onLogout, +}: { + token: string; + onLogout: () => void; +}) { + const [summary, setSummary] = useState(null); + const [accounts, setAccounts] = useState([]); + const [selectedAccountId, setSelectedAccountId] = useState(""); + const [events, setEvents] = useState([]); + const [userMemory, setUserMemory] = useState(null); + const [skillMemories, setSkillMemories] = useState([]); + const [selectedSkill, setSelectedSkill] = useState(""); + const [memoryDraft, setMemoryDraft] = useState(""); + const [newAccountId, setNewAccountId] = useState(""); + const [notice, setNotice] = useState(""); + + const selectedAccount = useMemo( + () => accounts.find((item) => item.account_id === selectedAccountId), + [accounts, selectedAccountId], + ); + const selectedSkillMemory = useMemo( + () => skillMemories.find((item) => item.skill === selectedSkill), + [skillMemories, selectedSkill], + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: 首次进入后台时加载一次即可,后续由刷新按钮触发。 + useEffect(() => { + refresh(); + }, []); + + // biome-ignore lint/correctness/useExhaustiveDependencies: 账号切换时加载对应记忆;函数体依赖当前 token。 + useEffect(() => { + if (!selectedAccountId) return; + loadAccountMemory(selectedAccountId); + }, [selectedAccountId]); + + useEffect(() => { + if (selectedSkillMemory) { + setMemoryDraft(selectedSkillMemory.content); + } else if (userMemory) { + setMemoryDraft(userMemory.content); + } + }, [selectedSkillMemory, userMemory]); + + async function refresh() { + const [summaryPayload, accountPayload] = await Promise.all([ + adminFetch(token, "/summary"), + adminFetch(token, "/accounts"), + ]); + setSummary(summaryPayload); + setAccounts(accountPayload); + const firstAccount = + selectedAccountId || accountPayload[0]?.account_id || ""; + setSelectedAccountId(firstAccount); + if (firstAccount) await loadAccountMemory(firstAccount); + } + + async function loadAccountMemory(accountId: string) { + const [memoryPayload, userPayload, skillPayload] = await Promise.all([ + adminFetch<{ events: MemoryEvent[] }>( + token, + `/memory?account_id=${encodeURIComponent(accountId)}`, + ), + adminFetch( + token, + `/memory/user?account_id=${encodeURIComponent(accountId)}`, + ), + adminFetch( + token, + `/memory/skills?account_id=${encodeURIComponent(accountId)}`, + ), + ]); + setEvents(memoryPayload.events ?? []); + setUserMemory(userPayload); + setSkillMemories(skillPayload); + setSelectedSkill(""); + setMemoryDraft(userPayload.content); + } + + async function createAccount() { + if (!newAccountId.trim()) return; + const payload = await adminFetch<{ + account_id: string; + initial_password: string; + }>(token, "/accounts", { + method: "POST", + body: JSON.stringify({ account_id: newAccountId.trim() }), + }); + setNotice( + `已创建 ${payload.account_id},初始密码 ${payload.initial_password}`, + ); + setNewAccountId(""); + await refresh(); + } + + async function deleteAccount(accountId: string) { + if (!confirm(`确定删除账号 ${accountId} 及其本地数据吗?`)) return; + await adminFetch(token, `/accounts/${encodeURIComponent(accountId)}`, { + method: "DELETE", + }); + setNotice(`已删除 ${accountId}`); + setSelectedAccountId(""); + await refresh(); + } + + async function saveMemory() { + if (!selectedAccountId) return; + if (selectedSkill) { + await adminFetch( + token, + `/memory/skills/${encodeURIComponent(selectedSkill)}`, + { + method: "PUT", + body: JSON.stringify({ + account_id: selectedAccountId, + content: memoryDraft, + }), + }, + ); + setNotice(`已保存 ${selectedSkill} 记忆`); + } else { + await adminFetch(token, "/memory/user", { + method: "PUT", + body: JSON.stringify({ + account_id: selectedAccountId, + content: memoryDraft, + }), + }); + setNotice("已保存用户记忆"); + } + await loadAccountMemory(selectedAccountId); + } + + function logout() { + localStorage.removeItem(TOKEN_KEY); + onLogout(); + } + + return ( +
+
+
+
+ +
+
+

ZK Data Agent 管理后台

+

+ 用户、会话、工具用量和个性化记忆队列。 +

+
+ + +
+
+ +
+ {notice ? ( +
+ {notice} +
+ ) : null} + +
+ + + + +
+ +
+
+
+
+ + 用户 +
+
+ setNewAccountId(event.target.value)} + /> + +
+
+
+ {accounts.map((account) => ( + + ))} +
+
+ +
+
+ + + + item.account_id === selectedAccountId, + )?.pending ?? 0 + } + /> +
+ +
+
+
+
+
记忆编辑
+
+ 默认编辑用户记忆;选择 Skill 后编辑该 Skill 记忆。 +
+
+ +
+
+ +
+ {skillMemories.map((item) => ( + + ))} +
+
+