import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { RetrievalBench } from "../components/RetrievalBench"; import { adminDatasources, listModels, migrateKbStorage, type DatasourceKB, type MigrateKbResult } from "../api"; // 数据源 & RAG:真数据(全平台知识库清单,来自 sundynix_kb/sundynix_doc)。 const KIND_LABEL: Record = { general: "通用", folder: "文件夹", project: "项目", case: "案例" }; const fmtWords = (n: number) => (n >= 1e4 ? `${(n / 1e4).toFixed(1)}万` : `${n}`); export function DatasourcesPage() { const [counts, setCounts] = useState({ users: 0, kbs: 0, docs: 0 }); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(""); // 运行时的 active embedding 模型展示 const [activeEmbedding, setActiveEmbedding] = useState("加载中…"); // 运维工具折叠状态 const [opsOpen, setOpsOpen] = useState(false); // 迁移 Modal 状态 const [migrationModal, setMigrationModal] = useState<"none" | "confirm" | "running" | "done">("none"); const [migrationResult, setMigrationResult] = useState(null); const [migrationErr, setMigrationErr] = useState(""); useEffect(() => { adminDatasources() .then((r) => { setCounts(r.counts); setRows(r.datasources); setErr(""); }) .catch((e) => setErr((e as Error).message)) .finally(() => setLoading(false)); // 获取激活的 embedding 模型 listModels("embedding") .then((models) => { const active = models.find((m) => m.active); if (active) { setActiveEmbedding(`${active.model} (${active.provider})`); } else { setActiveEmbedding("未配置/未激活 (将回退代码默认)"); } }) .catch(() => setActiveEmbedding("获取失败")); }, []); const startMigration = async () => { setMigrationModal("running"); setMigrationErr(""); try { const res = await migrateKbStorage(); setMigrationResult(res); setMigrationModal("done"); } catch (e) { setMigrationErr((e as Error).message); setMigrationModal("confirm"); // 退回到确认态展示错误 } }; const totalWords = rows.reduce((a, r) => a + r.total_words, 0); return (
{/* 顶部激活模型只读展示 & 引导 */}

当前激活向量化模型 (Embedding)

{activeEmbedding}
前往「模型配置」修改 →
{/* 运维工具 (RAG 迁移) */}
{opsOpen && (
知识库存储目录路径迁移 建议在低峰期操作

将平台全量知识库底层的存储目录规范,由早期版本的 owner/kb_id/doc 升级为新版本统一的 space_id/kb_id/doc 规范。此操作将遍历所有文件,并在数据库与对象存储中更新映射关系,迁移期间可能产生轻微检索延迟。

)}
{/* 迁移 Modal */} {migrationModal !== "none" && (

{migrationModal === "confirm" && "确认开始存储迁移?"} {migrationModal === "running" && "正在迁移中…"} {migrationModal === "done" && "迁移完成 🎉"}

一键升级平台 KB 作用域至 space_id

{migrationModal !== "running" && ( )}
{migrationModal === "confirm" && ( <>

您将触发全平台的数据目录热迁移。这会将旧版未挂载空间的独立知识库映射,统一调整至 Space 空间映射下。

⚠️ 运维安全警示:
· 这是一个高危数据库与存储热变更操作
· 建议执行前对 sundynix_doc 表进行备份
· 确认执行后不可中途停止
{migrationErr && (
操作失败:{migrationErr}
)} )} {migrationModal === "running" && (
正在扫描文档并重新入队,请勿关闭窗口…
)} {migrationModal === "done" && migrationResult && (
数据升级指令已成功执行,各文档已重新入队执行 RRF 索引重灌。
已扫描文档总数 {migrationResult.total}
入队重灌文档数 {migrationResult.enqueued}
跳过(已是新作用域) {migrationResult.skipped}
)}
{migrationModal === "confirm" && ( <> )} {migrationModal === "done" && ( )}
)} {/* 检索管线说明(诚实:三路 + RRF 等权融合,非可调权重) */}

混合检索管线

三路并行召回 → RRF 倒排互惠融合(Reciprocal Rank Fusion,平滑常数 k=60)→ rerank 重排。 融合按各路排名等权,无「每路占几成」的可调权重;单路可经 SearchByMode 指定。检索参数定义在 mcp-go 代码中。

{/* 检索试验台:紧跟管线说明 —— 讲完原理就能当场验证,不用只看文字 */} {/* 平台数据源计数 */}
{/* 知识库清单(真数据) */}

知识库清单(全平台)

{loading ? (
加载中…
) : err ? (
加载失败:{err}
) : rows.length === 0 ? (
还没有知识库
) : (
{rows.map((r) => ( ))}
知识库 类型 租户 文档数 总字数
{r.name} {KIND_LABEL[r.kind] ?? r.kind} {r.tenant_name || r.tenant_id || "—"} {r.doc_count} {fmtWords(r.total_words)}
)}
); } const ROUTE_TONE: Record = { violet: { dot: "bg-violet-500", text: "text-violet-600" }, cyan: { dot: "bg-cyan-500", text: "text-cyan-600" }, emerald: { dot: "bg-emerald-500", text: "text-emerald-600" }, }; function Route({ color, name, impl, desc }: { color: string; name: string; impl: string; desc: string }) { const t = ROUTE_TONE[color]; return (
{name} {impl}

{desc}

); } const STAT_TONE: Record = { violet: "text-violet-600", cyan: "text-cyan-600", emerald: "text-emerald-600" }; function Stat({ label, value, sub, tone }: { label: string; value: string; sub?: string; tone: string }) { return (
{label}
{value}
{sub &&
{sub}
}
); }