Improve memory manager UI and auth persistence

This commit is contained in:
wuyang6
2026-05-13 15:13:41 +08:00
parent ea56761a22
commit b5e8b2f218
2 changed files with 115 additions and 30 deletions
@@ -586,6 +586,9 @@ function AccountMenu({
(sum, item) => sum + item.totalTokens,
0,
);
const selectedSkillMemory = selectedMemorySkill
? skillMemories.find((item) => item.skill === selectedMemorySkill)
: null;
return (
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
@@ -757,43 +760,90 @@ function AccountMenu({
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
<Dialog open={memoryDialogOpen} onOpenChange={setMemoryDialogOpen}>
<DialogContent className="max-h-[86vh] overflow-hidden sm:max-w-3xl">
<DialogContent className="max-h-[88vh] overflow-hidden sm:max-w-5xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
Skill Skill
Skill
</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">
<div className="grid min-h-0 gap-4 md:grid-cols-[220px_1fr_240px]">
<div className="min-h-0 rounded-md border bg-muted/20">
<div className="border-b px-3 py-2 font-medium text-xs">
</div>
<div className="max-h-[520px] overflow-y-auto p-2">
<Button
size="xs"
className="mb-2 h-auto w-full justify-start px-2 py-2 text-left"
size="sm"
variant={selectedMemorySkill ? "outline" : "secondary"}
onClick={() => {
setSelectedMemorySkill("");
setMemoryDraft(userMemory?.content ?? "");
}}
>
<div className="min-w-0">
<div className="truncate font-medium"></div>
<div className="text-muted-foreground text-xs">
{userMemory?.line_count ?? 0}
</div>
</div>
</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 className="mb-1 px-1 text-muted-foreground text-xs">
Skill
</div>
{skillMemories.length ? (
<div className="grid gap-1">
{skillMemories.map((item) => (
<Button
key={item.skill}
className="h-auto w-full justify-start px-2 py-2 text-left"
size="sm"
variant={
selectedMemorySkill === item.skill
? "secondary"
: "ghost"
}
onClick={() => {
setSelectedMemorySkill(item.skill);
setMemoryDraft(item.content);
}}
>
<div className="min-w-0">
<div className="truncate font-medium">
{item.skill}
</div>
<div className="text-muted-foreground text-xs">
{item.line_count ?? 0}
{item.updated_at
? ` · ${formatShortTime(item.updated_at)}`
: ""}
</div>
</div>
</Button>
))}
</div>
) : (
<div className="rounded-md border border-dashed p-3 text-muted-foreground text-xs">
Skill 使 Skill
Skill
</div>
)}
</div>
</div>
<div className="grid min-h-0 content-start gap-2">
<div className="rounded-md border bg-muted/20 px-3 py-2">
<div className="font-medium text-sm">
{selectedMemorySkill
? `${selectedMemorySkill} 使用记忆`
: "用户记忆"}
</div>
<div className="mt-1 text-muted-foreground text-xs">
{selectedMemorySkill
? "仅在该 Skill 启用并进入模型可见列表时注入。"
: "作为当前账号的长期偏好和通用经验注入。"}
</div>
</div>
<textarea
className="min-h-[420px] resize-none rounded-md border bg-background p-3 font-mono text-xs outline-none"
@@ -802,10 +852,11 @@ function AccountMenu({
/>
<div className="flex items-center justify-between gap-2">
<span className="truncate text-muted-foreground text-xs">
{selectedMemorySkill
? `${selectedMemorySkill} 使用记忆`
: "用户记忆"}
{selectedMemorySkill && selectedSkillMemory?.updated_at
? `最近更新 ${formatShortTime(selectedSkillMemory.updated_at)}`
: userMemory?.updated_at
? `最近更新 ${formatShortTime(userMemory.updated_at)}`
: "暂无更新时间"}
</span>
<Button
size="sm"
@@ -925,6 +976,17 @@ function formatCompactNumber(value: number) {
}).format(value);
}
function formatShortTime(value: string) {
const timestamp = Date.parse(value);
if (Number.isNaN(timestamp)) return value;
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(timestamp));
}
function AccountStat({
label,
value,
+25 -2
View File
@@ -10,6 +10,8 @@ const AUTH_ROOT = path.join(
const USERS_PATH = path.join(AUTH_ROOT, "users.json");
const SESSIONS_PATH = path.join(AUTH_ROOT, "auth_sessions.json");
const COOKIE_NAME = "claw_account_session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
const SESSION_REFRESH_INTERVAL_MS = 60 * 60 * 12 * 1000;
type UserRecord = {
id: string;
@@ -23,7 +25,10 @@ type UsersFile = {
};
type SessionsFile = {
sessions: Record<string, { accountId: string; createdAt: string }>;
sessions: Record<
string,
{ accountId: string; createdAt: string; updatedAt?: string }
>;
};
export type AccountSession = {
@@ -87,6 +92,11 @@ export async function getCurrentAccount(): Promise<AccountSession | null> {
const usersFile = await readUsers();
const account = usersFile.users.find((user) => user.id === session.accountId);
if (!account) return null;
if (shouldRefreshSession(session.updatedAt ?? session.createdAt)) {
session.updatedAt = new Date().toISOString();
await writeSessions(sessionsFile);
await setSessionCookie(token);
}
return { id: account.id, username: account.username };
}
@@ -132,16 +142,22 @@ async function createSession(account: UserRecord) {
sessionsFile.sessions[token] = {
accountId: account.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await writeSessions(sessionsFile);
await setSessionCookie(token);
return { id: account.id, username: account.username };
}
async function setSessionCookie(token: string) {
const cookieStore = await cookies();
cookieStore.set(COOKIE_NAME, token, {
httpOnly: true,
sameSite: "lax",
path: "/",
maxAge: SESSION_MAX_AGE_SECONDS,
});
return { id: account.id, username: account.username };
}
async function createAccountDirectories(accountId: string) {
@@ -207,3 +223,10 @@ function verifyPassword(password: string, passwordHash: string) {
const expected = Buffer.from(expectedHash);
return actual.length === expected.length && timingSafeEqual(actual, expected);
}
function shouldRefreshSession(value?: string) {
if (!value) return true;
const timestamp = Date.parse(value);
if (Number.isNaN(timestamp)) return true;
return Date.now() - timestamp > SESSION_REFRESH_INTERVAL_MS;
}