feat(admin): 数据源页转 RAG 运维台 + 支付/模型菜单重组 + 概览升级为仪表盘

后端:
- 新增 POST /admin/kb/search:管理端跨租户检索,支持 mode 指定单路
  (vector/fulltext/graph/hybrid),不走 scopedKB(否则会被强制锁到调用者
  自己的 space,跨租户排障就没法做了)
- KB 清单补 space_id(检索键是 <space_id>/<name>,缺它前端拼不出 key)

前端:
- 数据源&RAG 页补「检索试验台」:同一 query 并排跑生产链路 + 四路诊断,
  召回不准时能直接定位是向量/分词/图谱哪一环挂了
- 支付拆成「配置 / 订单与对账」两个子页,挂到运维 > 支付 下;
  导航支持二级菜单(NavParent 命中子路由自动展开)
- SettingsPage → ModelConfigPage「模型配置」,模型参数与计费规则合一
- 概览 → 仪表盘:并入计费与用量(UsagePage → UsageSection),
  去掉系统健康拓扑(与服务状态页重复,同一份 /admin/status 数据)
- 全局隐藏滚动条(保留滚动)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-20 11:17:54 +08:00
parent ac4fd00e28
commit 79b110afd0
16 changed files with 577 additions and 237 deletions
+26
View File
@@ -434,6 +434,8 @@ export async function guardrailEvents(limit = 100): Promise<GuardrailEvent[]> {
export interface DatasourceKB {
id: string;
name: string;
/** 检索作用域键前半段:三库里的库名是 "<space_id>/<name>",检索试验台按此定位。 */
space_id: string;
kind: string;
tenant_id: string;
tenant_name: string;
@@ -442,6 +444,30 @@ export interface DatasourceKB {
total_words: number;
}
/** 检索命中:text=命中片段,score=该路的相似度/融合分(不同 mode 分数体系不同,勿跨路比较)。 */
export interface KbHit {
text: string;
score: number;
}
/** 检索模式:单路用于逐路定位是哪一路没召回;hybrid=纯 RRF 融合(不 rerank)""=生产链路(混合+rerank)。 */
export type SearchMode = "" | "vector" | "fulltext" | "graph" | "hybrid";
// adminKbSearch 检索试验台:按完整作用域键跨租户检索任意知识库。
// kb 传 `${space_id}/${name}`(来自 adminDatasources)。
export async function adminKbSearch(kb: string, q: string, topK = 5, mode: SearchMode = ""): Promise<KbHit[]> {
const res = guard(
await fetch(`${ADMIN}/kb/search`, {
method: "POST",
headers: authHeaders(true),
body: JSON.stringify({ kb, q, topK, mode }),
}),
);
const d = (await res.json().catch(() => ({}))) as { hits?: KbHit[]; error?: string };
if (!res.ok) throw new Error(d.error ?? `search failed: ${res.status}`);
return d.hits ?? [];
}
export async function adminDatasources(): Promise<{ counts: { users: number; kbs: number; docs: number }; datasources: DatasourceKB[] }> {
const res = guard(await fetch(`${ADMIN}/datasources`, { headers: authHeaders() }));
const d = (await res.json().catch(() => ({}))) as { counts?: { users: number; kbs: number; docs: number }; datasources?: DatasourceKB[]; error?: string };