Refine memory settings and admin usage stats

This commit is contained in:
wuyang6
2026-05-13 14:42:22 +08:00
parent 7282a18116
commit ea56761a22
4 changed files with 299 additions and 77 deletions
@@ -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<typeof toReplayRepository>[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<UserMemoryPayload | null>(null);
const [skillMemories, setSkillMemories] = useState<SkillMemoryPayload[]>([]);
const [memoryQueue, setMemoryQueue] = useState<MemoryQueuePayload | null>(
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}
</div>
<div className="mt-3 rounded-md border bg-muted/30 p-2 text-xs">
<button
type="button"
className="flex w-full items-center justify-between gap-2 text-left"
onClick={() => setMemoryOpen((value) => !value)}
>
<div className="flex items-center justify-between gap-2">
<span className="flex items-center gap-1.5 font-medium">
<BrainIcon className="size-3.5" />
</span>
<span className="flex items-center gap-1 text-muted-foreground">
{userMemory?.line_count ?? 0} · Skill{" "}
{skillMemories.length}
<ChevronDownIcon
className={`size-3.5 transition-transform ${
memoryOpen ? "rotate-180" : ""
}`}
/>
</span>
</button>
{memoryOpen ? (
<div className="mt-2 grid gap-2">
<div className="flex gap-1 overflow-x-auto">
<Button
size="xs"
variant={selectedMemorySkill ? "outline" : "secondary"}
onClick={() => {
setSelectedMemorySkill("");
setMemoryDraft(userMemory?.content ?? "");
}}
>
</Button>
{skillMemories.map((item) => (
<Button
key={item.skill}
size="xs"
variant={
selectedMemorySkill === item.skill
? "secondary"
: "outline"
}
onClick={() => {
setSelectedMemorySkill(item.skill);
setMemoryDraft(item.content);
}}
>
{item.skill}
</Button>
))}
</div>
<textarea
className="min-h-32 w-full resize-y rounded-md border bg-background p-2 font-mono text-xs outline-none"
value={memoryDraft}
onChange={(event) => setMemoryDraft(event.target.value)}
/>
<div className="flex items-center justify-between gap-2">
<span className="truncate text-muted-foreground">
Skill
{skillMemories.map((item) => item.skill).join("、") ||
"暂无"}
</span>
<Button
size="xs"
onClick={saveUserMemory}
disabled={memorySaving}
>
<SaveIcon className="size-3" />
</Button>
</div>
</div>
) : null}
<Button
size="xs"
variant="outline"
onClick={() => {
setSelectedMemorySkill("");
setMemoryDraft(userMemory?.content ?? "");
setOpen(false);
setMemoryDialogOpen(true);
}}
>
</Button>
</div>
<div className="mt-2 grid grid-cols-3 gap-2">
<AccountStat label="用户行" value={userMemory?.line_count ?? 0} />
<AccountStat label="Skill" value={skillMemories.length} />
<AccountStat
label="待处理"
value={memoryQueue?.queue?.totals?.pending ?? 0}
/>
</div>
</div>
<div className="mt-3 flex items-center justify-between gap-2 border-t pt-3">
<span className="text-muted-foreground text-xs">
@@ -769,6 +756,124 @@ function AccountMenu({
</div>
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
<Dialog open={memoryDialogOpen} onOpenChange={setMemoryDialogOpen}>
<DialogContent className="max-h-[86vh] overflow-hidden sm:max-w-3xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="grid min-h-0 gap-4 md:grid-cols-[1fr_240px]">
<div className="grid min-h-0 gap-2">
<div className="flex gap-1 overflow-x-auto pb-1">
<Button
size="xs"
variant={selectedMemorySkill ? "outline" : "secondary"}
onClick={() => {
setSelectedMemorySkill("");
setMemoryDraft(userMemory?.content ?? "");
}}
>
</Button>
{skillMemories.map((item) => (
<Button
key={item.skill}
size="xs"
variant={
selectedMemorySkill === item.skill
? "secondary"
: "outline"
}
onClick={() => {
setSelectedMemorySkill(item.skill);
setMemoryDraft(item.content);
}}
>
{item.skill}
</Button>
))}
</div>
<textarea
className="min-h-[420px] resize-none rounded-md border bg-background p-3 font-mono text-xs outline-none"
value={memoryDraft}
onChange={(event) => setMemoryDraft(event.target.value)}
/>
<div className="flex items-center justify-between gap-2">
<span className="truncate text-muted-foreground text-xs">
{selectedMemorySkill
? `${selectedMemorySkill} 使用记忆`
: "用户记忆"}
</span>
<Button
size="sm"
onClick={saveUserMemory}
disabled={memorySaving}
>
<SaveIcon className="size-3.5" />
{memorySaving ? "保存中..." : "保存"}
</Button>
</div>
</div>
<div className="grid content-start gap-3">
<div className="grid grid-cols-2 gap-2">
<AccountStat
label="用户记忆行"
value={userMemory?.line_count ?? 0}
/>
<AccountStat label="Skill 记忆" value={skillMemories.length} />
<AccountStat
label="待处理"
value={memoryQueue?.queue?.totals?.pending ?? 0}
/>
<AccountStat
label="失败"
value={memoryQueue?.queue?.totals?.failed ?? 0}
/>
</div>
<div className="rounded-md border bg-muted/30">
<div className="border-b px-2 py-2 font-medium text-xs">
</div>
<div className="max-h-[320px] overflow-y-auto p-2">
{memoryQueue?.events?.length ? (
memoryQueue.events.map((event) => (
<div
key={event.id}
className="mb-2 rounded-md border bg-background p-2 text-xs"
>
<div className="flex items-center justify-between gap-2">
<span className="font-medium">{event.status}</span>
<span className="text-muted-foreground">
P{event.priority}
</span>
</div>
<div className="mt-1 truncate text-muted-foreground">
{event.session_id}
</div>
<div className="mt-1 text-muted-foreground">
{event.signals?.join(" / ") || "无信号"}
</div>
{event.error ? (
<div className="mt-1 text-destructive">
{event.error}
</div>
) : null}
</div>
))
) : (
<div className="py-6 text-center text-muted-foreground text-xs">
</div>
)}
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</PopoverPrimitive.Root>
);
}