feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
@@ -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" },
|
||||
|
||||
@@ -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>}
|
||||
|
||||
Reference in New Issue
Block a user