Refine memory settings and admin usage stats
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user