feat(gateway): admin/overview 系统级聚合接口 + Tier4 后端路线图

- GET /api/v1/admin/overview (RequireAdmin):全平台口径聚合——
  用户/KB/文档总数 + 全局任务(今日/累计/趋势/终态) + 全局评测均分
  + 模型配置态(主/备/各kind数) + 提示词覆盖态 + 服务健康
- store.SystemCounts:全局 users/kb/doc 计数(区别 owner 隔离的个人口径)
- 区别桌面端个人 stats/overview:管理端概览改吃此接口(系统控制塔)
- DEPTH_ROADMAP 新增 Tier4 后端做实(A–F 六组,三路 Explore 审计产出)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-02 08:59:28 +08:00
parent cb7dd86121
commit e2104efc64
4 changed files with 143 additions and 3 deletions
@@ -12,9 +12,71 @@ import (
"github.com/sundynix/sundynix-gateway/internal/store"
"github.com/sundynix/sundynix-shared/contract"
"github.com/sundynix/sundynix-shared/prompts"
"github.com/sundynix/sundynix-shared/secrets"
)
// AdminOverview: GET /api/v1/admin/overview —— 管理端系统级聚合(控制塔口径)。
// 区别于 stats/overview(桌面端个人工作台):这里一律系统级——全平台用户/任务/评测/
// 模型配置态/提示词控制面态/服务健康。Task/Eval 表无 owner 即全量;用户/KB/Doc 走全局计数。
func (h *Handler) AdminOverview(c *gin.Context) {
ctx := c.Request.Context()
ov := h.db.StatsOverview(ctx, "") // owner="" → 跳过个人 KB 口径,仅取全局任务/评测
users, kbs, docs := h.db.SystemCounts(ctx)
// 模型配置态:主模型 + 备用链数 + 各 kind 数量。
chat, _ := h.db.ListModels(ctx, "chat")
emb, _ := h.db.ListModels(ctx, "embedding")
activeChat, activeEmb, fallbacks := "", "", 0
for _, m := range chat {
if m.Active {
activeChat = m.Provider + "/" + m.Model
} else {
fallbacks++
}
}
for _, m := range emb {
if m.Active {
activeEmb = m.Provider + "/" + m.Model
}
}
// 提示词控制面态:受管 key 总数(代码内置)vs 已激活热覆盖数。
promptRows, _ := h.db.ListPrompts(ctx)
overrides := map[string]struct{}{}
for _, pr := range promptRows {
if pr.Active {
overrides[pr.Key] = struct{}{}
}
}
// 服务健康(与 Health / StatsOverview 同口径:本地可判 + milvus/neo4j 经 mcp-go)。
services := gin.H{"gateway": true, "nats": true, "db": h.db.Enabled(), "redis": h.cache.Enabled(), "milvus": false, "neo4j": false}
cctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
if res, err := h.bus.CallTool(cctx, contract.ToolSubjectGo("health"), &contract.ToolCall{Tool: "health"}); err == nil && res != nil && res.OK {
var sub map[string]bool
if json.Unmarshal([]byte(res.Content), &sub) == nil {
services["milvus"], services["neo4j"] = sub["milvus"], sub["neo4j"]
}
}
c.JSON(http.StatusOK, gin.H{
"users": users, "kb_count": kbs, "kb_docs": docs,
"tasks_today": ov.TasksToday, "tasks_total": ov.TasksTotal,
"status_count": ov.StatusCount, "task_trend": ov.TaskTrend,
"eval_avg": ov.EvalAvg, "faithful_avg": ov.FaithfulAvg, "eval_count": ov.EvalCount,
"models": gin.H{
"chat_count": len(chat), "embedding_count": len(emb),
"active_chat": activeChat, "active_embedding": activeEmb, "fallbacks": fallbacks,
},
"prompts": gin.H{"managed": len(prompts.Known), "overrides": len(overrides)},
"services": services,
"checked_at": time.Now().Format(time.RFC3339),
})
}
// maskPrefix 是脱敏展示用的占位前缀;前端把列表里的脱敏 key 原样回传即视为「未改动」。
const maskPrefix = "••••"