From ea56761a229831fd150512d83dd26d3108d0297f Mon Sep 17 00:00:00 2001 From: wuyang6 Date: Wed, 13 May 2026 14:42:22 +0800 Subject: [PATCH] Refine memory settings and admin usage stats --- backend/api/server.py | 20 ++ frontend/app/app/admin/page.tsx | 77 ++++++ .../app/app/api/claw/memory/events/route.ts | 20 ++ .../assistant-ui/threadlist-sidebar.tsx | 259 ++++++++++++------ 4 files changed, 299 insertions(+), 77 deletions(-) create mode 100644 frontend/app/app/api/claw/memory/events/route.ts diff --git a/backend/api/server.py b/backend/api/server.py index 0986451..5683218 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -1229,6 +1229,17 @@ def create_app(state: AgentState) -> FastAPI: except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc + @app.get('/api/memory/events') + async def get_memory_events(account_id: str) -> dict[str, Any]: + try: + safe_account = _safe_account_id(account_id) + return { + 'queue': state.memory_manager.queue_snapshot(safe_account), + 'events': state.memory_manager.list_recent_events(safe_account, limit=20), + } + 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: @@ -3561,6 +3572,7 @@ def _list_admin_accounts(state: AgentState) -> list[dict[str, Any]]: total_tokens = 0 input_tokens = 0 output_tokens = 0 + reasoning_tokens = 0 latest_mtime = 0.0 models: dict[str, int] = {} for path in session_files: @@ -3573,6 +3585,7 @@ def _list_admin_accounts(state: AgentState) -> list[dict[str, Any]]: 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) + reasoning_tokens += int(usage.get('reasoning_tokens') or 0) total_tokens += int( usage.get('total_tokens') or (usage.get('input_tokens') or 0) + (usage.get('output_tokens') or 0) @@ -3590,7 +3603,14 @@ def _list_admin_accounts(state: AgentState) -> list[dict[str, Any]]: 'tool_calls': tool_calls, 'input_tokens': input_tokens, 'output_tokens': output_tokens, + 'reasoning_tokens': reasoning_tokens, 'total_tokens': total_tokens, + 'average_tokens_per_session': ( + round(total_tokens / session_count, 1) if session_count else 0 + ), + 'average_tool_calls_per_session': ( + round(tool_calls / session_count, 1) if session_count else 0 + ), 'latest_session_at': latest_mtime, 'models': models, 'user_memory_lines': _count_optional_lines(memory_dir / USER_MEMORY_FILENAME), diff --git a/frontend/app/app/admin/page.tsx b/frontend/app/app/admin/page.tsx index 6f6a598..00696fb 100644 --- a/frontend/app/app/admin/page.tsx +++ b/frontend/app/app/admin/page.tsx @@ -30,7 +30,10 @@ type AccountRow = { tool_calls: number; input_tokens: number; output_tokens: number; + reasoning_tokens: number; total_tokens: number; + average_tokens_per_session: number; + average_tool_calls_per_session: number; latest_session_at: number; models: Record; user_memory_lines: number; @@ -331,6 +334,70 @@ function AdminDashboard({ /> +
+
+
用量详情
+
+ + + + + + + + +
+
+
+
模型分布
+
+ {Object.entries(selectedAccount?.models ?? {}).length ? ( + Object.entries(selectedAccount?.models ?? {}) + .sort((a, b) => b[1] - a[1]) + .map(([model, count]) => ( +
+ + {model} + + {count} 会话 +
+ )) + ) : ( +
暂无模型用量
+ )} +
+
+
+
@@ -463,3 +530,13 @@ function formatNumber(value: number) { maximumFractionDigits: 1, }).format(value); } + +function formatTime(value?: number) { + if (!value) return "暂无"; + return new Intl.DateTimeFormat("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(value * 1000)); +} diff --git a/frontend/app/app/api/claw/memory/events/route.ts b/frontend/app/app/api/claw/memory/events/route.ts new file mode 100644 index 0000000..c24faf1 --- /dev/null +++ b/frontend/app/app/api/claw/memory/events/route.ts @@ -0,0 +1,20 @@ +import { getCurrentAccount } from "@/lib/claw-auth"; + +const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765"; + +export async function GET() { + const account = await getCurrentAccount(); + if (!account) + return Response.json({ error: "unauthorized" }, { status: 401 }); + const url = new URL(`${CLAW_API_URL}/api/memory/events`); + url.searchParams.set("account_id", account.id); + const response = await fetch(url, { cache: "no-store" }); + const payload = await response.text(); + return new Response(payload, { + status: response.status, + headers: { + "content-type": + response.headers.get("content-type") ?? "application/json", + }, + }); +} diff --git a/frontend/app/components/assistant-ui/threadlist-sidebar.tsx b/frontend/app/components/assistant-ui/threadlist-sidebar.tsx index 0eaaa9f..8b15a2f 100644 --- a/frontend/app/components/assistant-ui/threadlist-sidebar.tsx +++ b/frontend/app/components/assistant-ui/threadlist-sidebar.tsx @@ -101,6 +101,29 @@ type SkillMemoryPayload = { updated_at?: string | null; }; +type MemoryQueuePayload = { + queue?: { + totals?: { + events?: number; + pending?: number; + processing?: number; + done?: number; + failed?: number; + }; + }; + events?: MemoryEventPayload[]; +}; + +type MemoryEventPayload = { + id: string; + session_id: string; + status: string; + priority: number; + signals?: string[]; + error?: string | null; + updated_at?: string; +}; + type ReplaySessionPayload = Parameters[0]; type SidebarSession = ClawSessionSummary & { @@ -439,9 +462,12 @@ function AccountMenu({ null, ); const [modelUsageOpen, setModelUsageOpen] = useState(false); - const [memoryOpen, setMemoryOpen] = useState(false); + const [memoryDialogOpen, setMemoryDialogOpen] = useState(false); const [userMemory, setUserMemory] = useState(null); const [skillMemories, setSkillMemories] = useState([]); + const [memoryQueue, setMemoryQueue] = useState( + null, + ); const [memoryDraft, setMemoryDraft] = useState(""); const [selectedMemorySkill, setSelectedMemorySkill] = useState(""); const [memorySaving, setMemorySaving] = useState(false); @@ -484,7 +510,7 @@ function AccountMenu({ }, [open]); useEffect(() => { - if (!memoryOpen) return; + if (!open && !memoryDialogOpen) return; Promise.all([ fetch("/api/claw/memory/user", { cache: "no-store" }).then((res) => res.ok ? res.json() : null, @@ -492,18 +518,23 @@ function AccountMenu({ fetch("/api/claw/memory/skills", { cache: "no-store" }).then((res) => res.ok ? res.json() : [], ), - ]).then(([userPayload, skillPayload]) => { + fetch("/api/claw/memory/events", { cache: "no-store" }).then((res) => + res.ok ? res.json() : null, + ), + ]).then(([userPayload, skillPayload, queuePayload]) => { const nextUser = userPayload as UserMemoryPayload | null; setUserMemory(nextUser); - setMemoryDraft(nextUser?.content ?? ""); - setSelectedMemorySkill(""); setSkillMemories( Array.isArray(skillPayload) ? (skillPayload as SkillMemoryPayload[]) : [], ); + setMemoryQueue(queuePayload as MemoryQueuePayload | null); + if (memoryDialogOpen && !selectedMemorySkill) { + setMemoryDraft(nextUser?.content ?? ""); + } }); - }, [memoryOpen]); + }, [open, memoryDialogOpen, selectedMemorySkill]); async function logout() { await fetch("/api/claw/auth/logout", { method: "POST" }); @@ -528,9 +559,11 @@ function AccountMenu({ if (selectedMemorySkill) { const payload = (await response.json()) as SkillMemoryPayload; setSkillMemories((current) => - current.map((item) => - item.skill === payload.skill ? payload : item, - ), + current.some((item) => item.skill === payload.skill) + ? current.map((item) => + item.skill === payload.skill ? payload : item, + ) + : [...current, payload], ); setMemoryDraft(payload.content); } else { @@ -685,78 +718,32 @@ function AccountMenu({ ) : null}
- - {memoryOpen ? ( -
-
- - {skillMemories.map((item) => ( - - ))} -
-