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:
@@ -553,6 +553,48 @@ func (h *Handler) KbSearch(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"hits": hits})
|
||||
}
|
||||
|
||||
// AdminKbSearch: POST /api/v1/admin/kb/search —— 管理端「检索试验台」:按**完整作用域键**
|
||||
// 检索任意租户的知识库(kb 形如 "<space_id>/<name>",由 /admin/datasources 清单给出)。
|
||||
//
|
||||
// 与用户侧 KbSearch 的关键差别:这里**不做 scopedKB 改写** —— 用户侧会把库名套上调用者
|
||||
// 自己的 space_id,那样 admin 只能搜到自己空间的库,试验台就废了。跨租户是本端点的目的
|
||||
// (运维排障:线上召回不准时,定位是 embedding、切块还是融合的问题)。
|
||||
//
|
||||
// mode: 空=生产混合(含 rerank);vector/fulltext/graph/hybrid=单路/纯融合(不 rerank),
|
||||
// 用于逐路对比看是哪一路没召回。
|
||||
func (h *Handler) AdminKbSearch(c *gin.Context) {
|
||||
var body struct {
|
||||
KB string `json:"kb"` // 完整作用域键 "<space_id>/<name>"
|
||||
Q string `json:"q"`
|
||||
TopK int `json:"topK"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.Q == "" || body.KB == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "kb 与 q 必填"})
|
||||
return
|
||||
}
|
||||
args := map[string]any{"kb": body.KB, "q": body.Q}
|
||||
if body.TopK > 0 {
|
||||
args["topK"] = body.TopK
|
||||
}
|
||||
if body.Mode != "" {
|
||||
args["mode"] = body.Mode
|
||||
}
|
||||
res, err := h.bus.CallTool(c.Request.Context(), contract.ToolSubjectGo("kb_search"),
|
||||
&contract.ToolCall{Tool: "kb_search", Args: args})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !res.OK {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": res.Error})
|
||||
return
|
||||
}
|
||||
var hits []map[string]any
|
||||
_ = json.Unmarshal([]byte(res.Content), &hits)
|
||||
c.JSON(http.StatusOK, gin.H{"hits": hits})
|
||||
}
|
||||
|
||||
// KbGraph: GET /api/v1/kb/graph?kb= —— 某知识库的图谱三元组(→ mcp-go kb_graph,Neo4j)。
|
||||
func (h *Handler) KbGraph(c *gin.Context) {
|
||||
res, err := h.bus.CallTool(c.Request.Context(), contract.ToolSubjectGo("kb_graph"),
|
||||
|
||||
@@ -166,6 +166,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
admin.GET("/usage", h.AdminUsage) // 用量/积分/成本:全平台按天趋势 + 租户排行 / 单租户余额
|
||||
admin.GET("/evals", h.AdminEvals) // 自动评测观测:质量趋势 + 计数 + 错题本(真数据)
|
||||
admin.GET("/datasources", h.AdminDatasources) // 数据源清单:全平台知识库 + 文档数(真数据)
|
||||
admin.POST("/kb/search", h.AdminKbSearch) // 检索试验台:按完整作用域键跨租户检索(支持单路 mode 对比)
|
||||
admin.POST("/migrate-kb-storage", h.MigrateKBStorage) // 增量3:存量 KB 三库 owner/kb→space/kb 重灌(一次性)
|
||||
admin.GET("/audit", h.AuditList) // 敏感操作审计流(倒序,翻页)
|
||||
admin.GET("/guardrail-events", h.GuardrailEvents) // 护栏命中安全事件流(倒序,翻页)
|
||||
|
||||
@@ -7,8 +7,11 @@ import "context"
|
||||
|
||||
// DatasourceKB 是一条知识库的清单行(含文档数与总字数、租户名)。
|
||||
type DatasourceKB struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
// SpaceID 是检索作用域键的前半段:向量/全文/图谱三库里的库名是 "<space_id>/<name>"
|
||||
// (见 handler.scopedKB)。admin 检索试验台要按这个完整键定位库,故必须带出来。
|
||||
SpaceID string `json:"space_id"`
|
||||
Kind string `json:"kind"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
@@ -24,12 +27,12 @@ func (p *Postgres) AllDatasources(ctx context.Context) []DatasourceKB {
|
||||
}
|
||||
var out []DatasourceKB
|
||||
p.db.WithContext(WithoutTenant(ctx)).Table("sundynix_kb k").
|
||||
Select("k.id, k.name, k.kind, k.tenant_id, coalesce(t.name,'') as tenant_name, k.owner, "+
|
||||
Select("k.id, k.name, k.space_id, k.kind, k.tenant_id, coalesce(t.name,'') as tenant_name, k.owner, "+
|
||||
"count(d.id) as doc_count, coalesce(sum(d.size),0) as total_words").
|
||||
Joins("left join sundynix_doc d on d.kb = k.name and d.space_id = k.space_id and d.deleted_at is null").
|
||||
Joins("left join sundynix_tenant t on t.id = k.tenant_id").
|
||||
Where("k.deleted_at is null").
|
||||
Group("k.id, k.name, k.kind, k.tenant_id, t.name, k.owner").
|
||||
Group("k.id, k.name, k.space_id, k.kind, k.tenant_id, t.name, k.owner").
|
||||
Order("doc_count desc, k.created_at desc").
|
||||
Scan(&out)
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user