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 = "••••"
+2 -1
View File
@@ -94,7 +94,8 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
admin.POST("/models/test", h.TestModel)
admin.GET("/pricing", h.ListPricing) // 各模型计价(token↔真钱)
admin.PUT("/pricing", h.SavePricing) // 设置某模型输入/输出单价
admin.GET("/status", h.AdminStatus) // 服务状态:基建/服务探活 + MCP 工具注册
admin.GET("/status", h.AdminStatus) // 服务状态:基建/服务探活 + MCP 工具注册
admin.GET("/overview", h.AdminOverview) // 系统级聚合:全平台用户/任务/评测/模型态/提示词态/健康
}
}
return r
+13
View File
@@ -303,6 +303,19 @@ func (p *Postgres) RecentRuns(ctx context.Context, n int) []RunRow {
return out
}
// SystemCounts 返回系统级计数(管理端 overview 用:全平台口径,非 owner 隔离)。
// 软删行由 GORM DeletedAt 作用域自动排除。降级模式返回零值。
func (p *Postgres) SystemCounts(ctx context.Context) (users, kbs, docs int64) {
if p.db == nil {
return 0, 0, 0
}
db := p.db.WithContext(ctx)
db.Model(&User{}).Count(&users)
db.Model(&KB{}).Count(&kbs)
db.Model(&Doc{}).Count(&docs)
return
}
// Close 释放底层连接。
func (p *Postgres) Close() {
if p.db == nil {