feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1

Merged
Blizzard merged 181 commits from feat/wails3 into main 2026-07-17 01:12:32 +00:00
4 changed files with 34 additions and 30 deletions
Showing only changes of commit 8f1aefe13e - Show all commits
+15 -11
View File
@@ -10,12 +10,20 @@ import { cn } from "../ui";
const DRAG = { "--wails-draggable": "drag" } as CSSProperties;
const NODRAG = { "--wails-draggable": "no-drag" } as CSSProperties;
function Light({ on, label }: { on: boolean; label: string }) {
const dot = on ? "bg-success shadow-[0_0_8px_rgba(52,211,153,0.7)]" : "bg-danger";
// 系统状态:把 5 项基建健康收成一个整体状态点(客户不看数据库名,异常时 tooltip 提示项数)。
function SystemStatus({ h }: { h: ReturnType<typeof useHealth> }) {
const down = [h.gateway, h.db, h.nats, h.milvus, h.neo4j].filter((v) => !v).length;
const ok = down === 0;
return (
<span className="flex items-center gap-1.5 text-[11px] text-slate-500" title={`${label} ${on ? "在线" : "离线"}`}>
<span className={cn("h-1.5 w-1.5 rounded-full", dot)} />
{label}
<span
title={ok ? "所有服务运行正常" : `${down} 项服务暂不可用`}
className={cn(
"flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px]",
ok ? "border-line text-slate-500" : "border-warn/40 text-warn",
)}
>
<span className={cn("h-1.5 w-1.5 rounded-full", ok ? "bg-success" : "bg-warn")} />
{ok ? "系统正常" : "部分异常"}
</span>
);
}
@@ -140,12 +148,8 @@ export function TopBar({ user, tenant, tenants = [], onSwitchTenant, space, spac
<kbd className="rounded border border-line px-1 font-mono text-[10px] text-slate-500">K</kbd>
</button>
<div className="ml-2 flex items-center gap-3">
<Light on={h.gateway} label="Gateway" />
<Light on={h.db} label="DB" />
<Light on={h.nats} label="NATS" />
<Light on={h.milvus} label="Milvus" />
<Light on={h.neo4j} label="Neo4j" />
<div className="ml-2">
<SystemStatus h={h} />
</div>
<div className="ml-auto flex items-center gap-2" style={NODRAG}>
<TenantSwitcher tenants={tenants} activeId={tenant?.tenant?.id} onSwitch={onSwitchTenant} />
@@ -45,10 +45,10 @@ export const NODE_KINDS: Record<string, NodeKind> = {
},
retriever: {
kind: "retriever",
label: "检索 (RAG)",
label: "知识检索",
accent: "border-l-sky-500",
badge: "bg-sky-100 text-sky-700",
desc: "绑定知识库做混合检索",
desc: "知识库检索相关内容",
fields: [
{ key: "kb", label: "知识库", type: "select", options: ["(未建库)", "合同库", "判例库"], required: true },
{ key: "topK", label: "TopK", type: "number" },
+14 -14
View File
@@ -77,15 +77,16 @@ interface FileJob {
}
type KbTab = "ingest" | "vault" | "search" | "graph";
// key 对齐后端事件阶段(勿改);label 用「结果语言」,不暴露 Milvus/Bleve/Neo4j 等实现名。
const STAGE: Record<string, { icon: LucideIcon; label: string }> = {
: { icon: Upload, label: "解析文件" },
: { icon: FileText, label: "解析完成" },
: { icon: Scissors, label: "切块" },
: { icon: Sparkles, label: "向量化" },
Milvus: { icon: Database, label: "写入向量库" },
Bleve: { icon: Search, label: "写入全文索引" },
: { icon: Network, label: "取知识" },
Neo4j: { icon: Share2, label: "写入图谱" },
: { icon: Scissors, label: "拆分段落" },
: { icon: Sparkles, label: "理解内容" },
Milvus: { icon: Database, label: "建立语义索引" },
Bleve: { icon: Search, label: "建立全文索引" },
: { icon: Network, label: "取知识" },
Neo4j: { icon: Share2, label: "构建知识关系" },
: { icon: CheckCircle2, label: "完成" },
: { icon: XCircle, label: "失败" },
};
@@ -395,23 +396,22 @@ export function KbView({ identity, spaceId = "", spaceReadOnly = false }: { iden
{tab === "search" && (
<div className="h-full overflow-y-auto p-4">
<h3 className="mb-2 text-xs font-semibold text-slate-400"> · {kb} + rerank</h3>
<h3 className="mb-2 text-xs font-semibold text-slate-400">{kb}</h3>
<div className="flex max-w-3xl gap-2">
<Input className="flex-1" value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onSearch()} placeholder="输入查询,语义召回相关片段…" />
<Input className="flex-1" value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onSearch()} placeholder="输入问题或关键词,查找相关内容…" />
<Input type="number" className="w-16" value={topK} min={1} max={20} onChange={(e) => setTopK(Number(e.target.value))} title="TopK" />
<Button variant="primary" size="md" icon={Search} onClick={onSearch} disabled={searching || !q.trim()}>
{searching ? "检索中…" : "检索"}
</Button>
</div>
<ul className="mt-3 max-w-3xl space-y-2">
{hits === null && <li className="text-xs text-slate-600"></li>}
{hits !== null && hits.length === 0 && <li className="text-xs text-slate-600"> RAG </li>}
{hits === null && <li className="text-xs text-slate-600"></li>}
{hits !== null && hits.length === 0 && <li className="text-xs text-slate-600"></li>}
{hits?.map((h, i) => (
<li key={i} className="rounded-lg border border-line bg-ink-850 p-2">
<div className="mb-1 flex items-center gap-2 text-[10px]">
<Badge tone="accent"></Badge>
<span className="text-slate-600">#{i + 1}</span>
<span className="ml-auto font-mono text-brand-400">{h.score.toFixed(3)}</span>
<span className="ml-auto font-mono text-slate-500" title="相关度">{(h.score * 100).toFixed(0)}%</span>
</div>
<div className="text-xs text-slate-300">{h.text}</div>
</li>
@@ -423,7 +423,7 @@ export function KbView({ identity, spaceId = "", spaceReadOnly = false }: { iden
{tab === "graph" && (
<div className="h-full overflow-y-auto p-4">
<div className="mb-2 flex max-w-3xl items-center justify-between">
<h3 className="text-xs font-semibold text-slate-400"> · {kb}Neo4j · </h3>
<h3 className="text-xs font-semibold text-slate-400">{kb}</h3>
<Button size="sm" icon={Network} onClick={onGraph}>
</Button>
@@ -613,7 +613,7 @@ function VaultPanel({ identity, kb, spaceReadOnly = false }: { identity: Identit
</Button>
</>
)}
<span className="ml-auto text-[10px] text-slate-600"> [[]]</span>
<span className="ml-auto text-[10px] text-slate-600"> [[]] </span>
</div>
{mode === "graph" ? (
@@ -79,7 +79,7 @@ export function RunsView({ run }: { run: RunState }) {
const statusCls =
cur.phase === "streaming" ? "text-accent-400" : cur.phase === "done" ? "text-success" : cur.phase === "error" ? "text-danger" : "text-slate-500";
const statusText =
cur.phase === "streaming" ? "流式中…" : cur.phase === "done" ? "完成 ✓" : cur.phase === "error" ? `${cur.error ?? "出错"}` : cur.phase === "submitting" ? "提交中…" : "就绪";
cur.phase === "streaming" ? "生成中…" : cur.phase === "done" ? "完成 ✓" : cur.phase === "error" ? `${cur.error ?? "出错"}` : cur.phase === "submitting" ? "提交中…" : "就绪";
return (
<div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden p-6">
@@ -145,7 +145,7 @@ export function RunsView({ run }: { run: RunState }) {
{cur.output ? (
<OutputView output={cur.output} />
) : (
<EmptyState icon={Activity} title="尚无输出" desc="选择一次运行查看其模型输出;或发起新运行,token 在此流式呈现。" />
<EmptyState icon={Activity} title="尚无输出" desc="选择一次运行查看结果;或发起新运行,回复会在这里逐字呈现。" />
)}
</Panel>
</div>
@@ -208,7 +208,7 @@ function EvalView({ ev }: { ev: EvalResult | null }) {
<Badge tone={LEVEL_TONE[ev.level] ?? "neutral"}>{ev.level} · {ev.overall.toFixed(2)}</Badge>
{ev.corrected && <Badge tone="accent"></Badge>}
<span className="text-slate-500"> {ev.rule.toFixed(2)} · {ev.llm.toFixed(2)}</span>
{ev.sources > 0 && <span className="text-slate-500"> {ev.faithful.toFixed(2)} · {ev.sources} </span>}
{ev.sources > 0 && <span className="text-slate-500"> {ev.faithful.toFixed(2)} · {ev.sources} </span>}
</div>
{ev.reason && <p className="leading-relaxed text-slate-400">{ev.reason}</p>}
{ev.flags?.length > 0 && <p className="text-warn">{ev.flags.join("")}</p>}