Refine memory settings and admin usage stats
This commit is contained in:
@@ -1229,6 +1229,17 @@ def create_app(state: AgentState) -> FastAPI:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from 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}')
|
@app.get('/api/memory/skills/{skill_name}')
|
||||||
async def get_skill_memory(skill_name: str, account_id: str) -> dict[str, Any]:
|
async def get_skill_memory(skill_name: str, account_id: str) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
@@ -3561,6 +3572,7 @@ def _list_admin_accounts(state: AgentState) -> list[dict[str, Any]]:
|
|||||||
total_tokens = 0
|
total_tokens = 0
|
||||||
input_tokens = 0
|
input_tokens = 0
|
||||||
output_tokens = 0
|
output_tokens = 0
|
||||||
|
reasoning_tokens = 0
|
||||||
latest_mtime = 0.0
|
latest_mtime = 0.0
|
||||||
models: dict[str, int] = {}
|
models: dict[str, int] = {}
|
||||||
for path in session_files:
|
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 {}
|
usage = data.get('usage') if isinstance(data.get('usage'), dict) else {}
|
||||||
input_tokens += int(usage.get('input_tokens') or 0)
|
input_tokens += int(usage.get('input_tokens') or 0)
|
||||||
output_tokens += int(usage.get('output_tokens') or 0)
|
output_tokens += int(usage.get('output_tokens') or 0)
|
||||||
|
reasoning_tokens += int(usage.get('reasoning_tokens') or 0)
|
||||||
total_tokens += int(
|
total_tokens += int(
|
||||||
usage.get('total_tokens')
|
usage.get('total_tokens')
|
||||||
or (usage.get('input_tokens') or 0) + (usage.get('output_tokens') or 0)
|
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,
|
'tool_calls': tool_calls,
|
||||||
'input_tokens': input_tokens,
|
'input_tokens': input_tokens,
|
||||||
'output_tokens': output_tokens,
|
'output_tokens': output_tokens,
|
||||||
|
'reasoning_tokens': reasoning_tokens,
|
||||||
'total_tokens': total_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,
|
'latest_session_at': latest_mtime,
|
||||||
'models': models,
|
'models': models,
|
||||||
'user_memory_lines': _count_optional_lines(memory_dir / USER_MEMORY_FILENAME),
|
'user_memory_lines': _count_optional_lines(memory_dir / USER_MEMORY_FILENAME),
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ type AccountRow = {
|
|||||||
tool_calls: number;
|
tool_calls: number;
|
||||||
input_tokens: number;
|
input_tokens: number;
|
||||||
output_tokens: number;
|
output_tokens: number;
|
||||||
|
reasoning_tokens: number;
|
||||||
total_tokens: number;
|
total_tokens: number;
|
||||||
|
average_tokens_per_session: number;
|
||||||
|
average_tool_calls_per_session: number;
|
||||||
latest_session_at: number;
|
latest_session_at: number;
|
||||||
models: Record<string, number>;
|
models: Record<string, number>;
|
||||||
user_memory_lines: number;
|
user_memory_lines: number;
|
||||||
@@ -331,6 +334,70 @@ function AdminDashboard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
|
||||||
|
<div className="rounded-lg border bg-card">
|
||||||
|
<div className="border-b p-3 font-medium">用量详情</div>
|
||||||
|
<div className="grid gap-3 p-3 md:grid-cols-4">
|
||||||
|
<SmallStat
|
||||||
|
label="输入 token"
|
||||||
|
value={formatNumber(selectedAccount?.input_tokens ?? 0)}
|
||||||
|
/>
|
||||||
|
<SmallStat
|
||||||
|
label="输出 token"
|
||||||
|
value={formatNumber(selectedAccount?.output_tokens ?? 0)}
|
||||||
|
/>
|
||||||
|
<SmallStat
|
||||||
|
label="推理 token"
|
||||||
|
value={formatNumber(selectedAccount?.reasoning_tokens ?? 0)}
|
||||||
|
/>
|
||||||
|
<SmallStat
|
||||||
|
label="平均 token/会话"
|
||||||
|
value={formatNumber(
|
||||||
|
selectedAccount?.average_tokens_per_session ?? 0,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<SmallStat
|
||||||
|
label="平均工具/会话"
|
||||||
|
value={selectedAccount?.average_tool_calls_per_session ?? 0}
|
||||||
|
/>
|
||||||
|
<SmallStat
|
||||||
|
label="注册状态"
|
||||||
|
value={selectedAccount?.registered ? "已注册" : "仅目录"}
|
||||||
|
/>
|
||||||
|
<SmallStat
|
||||||
|
label="最近活跃"
|
||||||
|
value={formatTime(selectedAccount?.latest_session_at)}
|
||||||
|
/>
|
||||||
|
<SmallStat
|
||||||
|
label="模型数"
|
||||||
|
value={Object.keys(selectedAccount?.models ?? {}).length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-card">
|
||||||
|
<div className="border-b p-3 font-medium">模型分布</div>
|
||||||
|
<div className="max-h-40 overflow-y-auto p-3 text-sm">
|
||||||
|
{Object.entries(selectedAccount?.models ?? {}).length ? (
|
||||||
|
Object.entries(selectedAccount?.models ?? {})
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([model, count]) => (
|
||||||
|
<div
|
||||||
|
key={model}
|
||||||
|
className="mb-2 grid grid-cols-[1fr_auto] gap-3"
|
||||||
|
>
|
||||||
|
<span className="truncate text-muted-foreground">
|
||||||
|
{model}
|
||||||
|
</span>
|
||||||
|
<span>{count} 会话</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground">暂无模型用量</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 xl:grid-cols-[360px_1fr]">
|
<div className="grid gap-4 xl:grid-cols-[360px_1fr]">
|
||||||
<div className="grid gap-4 content-start">
|
<div className="grid gap-4 content-start">
|
||||||
<div className="rounded-lg border bg-card">
|
<div className="rounded-lg border bg-card">
|
||||||
@@ -463,3 +530,13 @@ function formatNumber(value: number) {
|
|||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
}).format(value);
|
}).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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -101,6 +101,29 @@ type SkillMemoryPayload = {
|
|||||||
updated_at?: string | null;
|
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 ReplaySessionPayload = Parameters<typeof toReplayRepository>[0];
|
||||||
|
|
||||||
type SidebarSession = ClawSessionSummary & {
|
type SidebarSession = ClawSessionSummary & {
|
||||||
@@ -439,9 +462,12 @@ function AccountMenu({
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [modelUsageOpen, setModelUsageOpen] = useState(false);
|
const [modelUsageOpen, setModelUsageOpen] = useState(false);
|
||||||
const [memoryOpen, setMemoryOpen] = useState(false);
|
const [memoryDialogOpen, setMemoryDialogOpen] = useState(false);
|
||||||
const [userMemory, setUserMemory] = useState<UserMemoryPayload | null>(null);
|
const [userMemory, setUserMemory] = useState<UserMemoryPayload | null>(null);
|
||||||
const [skillMemories, setSkillMemories] = useState<SkillMemoryPayload[]>([]);
|
const [skillMemories, setSkillMemories] = useState<SkillMemoryPayload[]>([]);
|
||||||
|
const [memoryQueue, setMemoryQueue] = useState<MemoryQueuePayload | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [memoryDraft, setMemoryDraft] = useState("");
|
const [memoryDraft, setMemoryDraft] = useState("");
|
||||||
const [selectedMemorySkill, setSelectedMemorySkill] = useState("");
|
const [selectedMemorySkill, setSelectedMemorySkill] = useState("");
|
||||||
const [memorySaving, setMemorySaving] = useState(false);
|
const [memorySaving, setMemorySaving] = useState(false);
|
||||||
@@ -484,7 +510,7 @@ function AccountMenu({
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!memoryOpen) return;
|
if (!open && !memoryDialogOpen) return;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch("/api/claw/memory/user", { cache: "no-store" }).then((res) =>
|
fetch("/api/claw/memory/user", { cache: "no-store" }).then((res) =>
|
||||||
res.ok ? res.json() : null,
|
res.ok ? res.json() : null,
|
||||||
@@ -492,18 +518,23 @@ function AccountMenu({
|
|||||||
fetch("/api/claw/memory/skills", { cache: "no-store" }).then((res) =>
|
fetch("/api/claw/memory/skills", { cache: "no-store" }).then((res) =>
|
||||||
res.ok ? res.json() : [],
|
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;
|
const nextUser = userPayload as UserMemoryPayload | null;
|
||||||
setUserMemory(nextUser);
|
setUserMemory(nextUser);
|
||||||
setMemoryDraft(nextUser?.content ?? "");
|
|
||||||
setSelectedMemorySkill("");
|
|
||||||
setSkillMemories(
|
setSkillMemories(
|
||||||
Array.isArray(skillPayload)
|
Array.isArray(skillPayload)
|
||||||
? (skillPayload as SkillMemoryPayload[])
|
? (skillPayload as SkillMemoryPayload[])
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
setMemoryQueue(queuePayload as MemoryQueuePayload | null);
|
||||||
|
if (memoryDialogOpen && !selectedMemorySkill) {
|
||||||
|
setMemoryDraft(nextUser?.content ?? "");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [memoryOpen]);
|
}, [open, memoryDialogOpen, selectedMemorySkill]);
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await fetch("/api/claw/auth/logout", { method: "POST" });
|
await fetch("/api/claw/auth/logout", { method: "POST" });
|
||||||
@@ -528,9 +559,11 @@ function AccountMenu({
|
|||||||
if (selectedMemorySkill) {
|
if (selectedMemorySkill) {
|
||||||
const payload = (await response.json()) as SkillMemoryPayload;
|
const payload = (await response.json()) as SkillMemoryPayload;
|
||||||
setSkillMemories((current) =>
|
setSkillMemories((current) =>
|
||||||
current.map((item) =>
|
current.some((item) => item.skill === payload.skill)
|
||||||
item.skill === payload.skill ? payload : item,
|
? current.map((item) =>
|
||||||
),
|
item.skill === payload.skill ? payload : item,
|
||||||
|
)
|
||||||
|
: [...current, payload],
|
||||||
);
|
);
|
||||||
setMemoryDraft(payload.content);
|
setMemoryDraft(payload.content);
|
||||||
} else {
|
} else {
|
||||||
@@ -685,78 +718,32 @@ function AccountMenu({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 rounded-md border bg-muted/30 p-2 text-xs">
|
<div className="mt-3 rounded-md border bg-muted/30 p-2 text-xs">
|
||||||
<button
|
<div className="flex items-center justify-between gap-2">
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center justify-between gap-2 text-left"
|
|
||||||
onClick={() => setMemoryOpen((value) => !value)}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-1.5 font-medium">
|
<span className="flex items-center gap-1.5 font-medium">
|
||||||
<BrainIcon className="size-3.5" />
|
<BrainIcon className="size-3.5" />
|
||||||
记忆
|
记忆
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1 text-muted-foreground">
|
<Button
|
||||||
用户 {userMemory?.line_count ?? 0} 行 · Skill{" "}
|
size="xs"
|
||||||
{skillMemories.length}
|
variant="outline"
|
||||||
<ChevronDownIcon
|
onClick={() => {
|
||||||
className={`size-3.5 transition-transform ${
|
setSelectedMemorySkill("");
|
||||||
memoryOpen ? "rotate-180" : ""
|
setMemoryDraft(userMemory?.content ?? "");
|
||||||
}`}
|
setOpen(false);
|
||||||
/>
|
setMemoryDialogOpen(true);
|
||||||
</span>
|
}}
|
||||||
</button>
|
>
|
||||||
{memoryOpen ? (
|
管理
|
||||||
<div className="mt-2 grid gap-2">
|
</Button>
|
||||||
<div className="flex gap-1 overflow-x-auto">
|
</div>
|
||||||
<Button
|
<div className="mt-2 grid grid-cols-3 gap-2">
|
||||||
size="xs"
|
<AccountStat label="用户行" value={userMemory?.line_count ?? 0} />
|
||||||
variant={selectedMemorySkill ? "outline" : "secondary"}
|
<AccountStat label="Skill" value={skillMemories.length} />
|
||||||
onClick={() => {
|
<AccountStat
|
||||||
setSelectedMemorySkill("");
|
label="待处理"
|
||||||
setMemoryDraft(userMemory?.content ?? "");
|
value={memoryQueue?.queue?.totals?.pending ?? 0}
|
||||||
}}
|
/>
|
||||||
>
|
</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>
|
|
||||||
<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}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex items-center justify-between gap-2 border-t pt-3">
|
<div className="mt-3 flex items-center justify-between gap-2 border-t pt-3">
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
@@ -769,6 +756,124 @@ function AccountMenu({
|
|||||||
</div>
|
</div>
|
||||||
</PopoverPrimitive.Content>
|
</PopoverPrimitive.Content>
|
||||||
</PopoverPrimitive.Portal>
|
</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>
|
</PopoverPrimitive.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user