From e2104efc64ff40e96c6adaa130ae658663a6fb73 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Thu, 2 Jul 2026 08:59:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(gateway):=20admin/overview=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=BA=A7=E8=81=9A=E5=90=88=E6=8E=A5=E5=8F=A3=20+=20Ti?= =?UTF-8?q?er4=20=E5=90=8E=E7=AB=AF=E8=B7=AF=E7=BA=BF=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- DEPTH_ROADMAP.md | 68 +++++++++++++++++++++- sundynix-gateway/internal/handler/admin.go | 62 ++++++++++++++++++++ sundynix-gateway/internal/router/router.go | 3 +- sundynix-gateway/internal/store/pgsql.go | 13 +++++ 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/DEPTH_ROADMAP.md b/DEPTH_ROADMAP.md index 014110e..e93f820 100644 --- a/DEPTH_ROADMAP.md +++ b/DEPTH_ROADMAP.md @@ -55,6 +55,20 @@ - [ ] 补前端测试(自评 🔴 缺失):关键流程(run/审批/多智能体)的组件/逻辑测试 - 验收:后端已有能力在 UI 都可见可操作;前端测试覆盖核心流程。 +### [~] T1.3 管理端做实(admin 控制台从 mock → 真控制面) +管理端定位 = **系统控制塔**(统筹全系统:全局吞吐/配置态/健康/安全/成本),区别于桌面端个人运行页。 +RBAC 未做,暂以单管理员账号代理;概览口径必须是**系统级**而非个人作用域。 +- [x] 接 vitest(此前零测试)+ 41 单测(api/diff/路由派生) +- [x] 新增「提示词」控制面页(接 `/api/v1/prompts`:建版本/激活热下发/对比 diff/撤销,live 验证) +- [x] 概览页重做为「系统控制塔」:全局任务吞吐 + 模型路由/Fallback 态 + 提示词控制面态 + 系统健康拓扑 + (去掉个人作用域的 我的KB/我的token/我的运行feed,与桌面端运行页不再重复) +- [ ] 🔴 **下一步:admin 专属聚合接口 `GET /api/v1/admin/overview`(RequireAdmin)** + —— 概览现借桌面端个人接口 `stats/overview`(其任务/评测碰巧全局因 Task/Eval 无 owner),不干净。 + 新接口一处聚合**系统治理口径**:全平台用户总数 / 租户数 / 全局任务(今日/累计/趋势/终态) / + 全局评测均分 / 模型配置态 / 提示词覆盖数 / 服务健康。前端概览改吃此接口,补上「用户/租户总数」缺口。 +- [ ] 其余 mock 页按需做实:Datasources / Evals / Tenants / Guardrails(多为本地 mock,无后端数据源) +- 验收:概览/关键页吃真实**系统级**数据;admin 有独立聚合接口不再蹭个人接口。 + --- ## 🥉 Tier 2 — 把 AI 核心做深(差异化卖点) @@ -140,13 +154,63 @@ --- +## 🧱 Tier 4 — 后端做实(2026-06-30 三路 Explore 审计产出) + +> 来源:三个 Explore agent 全量扫 gateway / dispatcher+shared / mcp 后端,找出"未实现 + 可优化"。 +> 工作量:S=1–2 天 · M=3–5 天 · L=1–2 周。一项项做,完成勾选 + 注明 live 结果。 +> 推荐起步:**T4.B(审计可溯源 + admin/overview)** —— 内聚、全 M、直接喂管理端、不依赖大重构。 + +### [ ] T4.A 多租户 / RBAC / 用户治理(结构性地基,体量大) +- [ ] 真正的 RBAC:User 加 role 字段 + 角色/权限表,`RequireAdmin`(middleware/auth.go:46) 从白名单升级为角色校验 | M +- [ ] 用户管理接口:列举 / 禁用 / 改角色(现仅注册/登录/查我,handler/auth.go)| M +- [ ] 多租户:Task/Eval/KB/Agent 加 tenant_id,owner_id 之上补租户隔离边界 + 租户表/关联表(store 全表,单租户假设)| L + +### [ ] 🔴 T4.B 审计 / 可溯源 + 管理端聚合(推荐先做,内聚且喂管理端) +- [ ] `audit_log` 表 + 中间件:改模型/删模型/改密钥/激活 prompt 等敏感操作落库(handler/admin.go 全无留痕)| M +- [ ] 护栏拦截事件落库 `guardrail_event`(middleware/guardrail.go:29 现只打日志,无法复盘"谁触发多少次")| M +- [ ] HITL 审批决定落库(task_handler.go:172 现只发 NATS,无审批历史)| S +- [ ] `GET /api/v1/admin/overview`(RequireAdmin) 系统级聚合:用户/租户/全局任务/评测/模型态/prompt 覆盖(替代概览现借的个人 stats/overview,见 [T1.3])| M + +### [ ] T4.C 真实成本计费 +- [ ] token×单价 落库 `token_usage`(现仅 Redis 按天计数 TTL48h,task_handler.go:532)| L +- [ ] 消费聚合 + 账单生成,Pricing 表与真实消费挂钩(现表建了未挂)| —(并入上条) + +### [ ] T4.D AI 核心深化 +- [ ] 记忆 Consolidate 层:盲目 upsert→记忆腐烂,补 Mem0 式 ADD/UPDATE/DELETE 对账(mcp-go/internal/memory/store.go,见 [[memory-upgrade-plan]])| M +- [ ] prompt 灰度% A/B:激活某版按小流量验证再全量(shared/prompts,现激活即全量,见 [T2.3])| M +- [ ] 外部接入做实:`mcp-go/internal/external/apis.go:13` Call 空壳 → provider 路由 + 统一鉴权/重试/限流/审计 | M +- [ ] mcp-py 算法层去桩:文档解析接 PaddleOCR/magic-pdf(mineru.py:11 返回空块)| M +- [ ] 报告原生 PDF(现仅 Word,office/unioffice.go)| M + +### [ ] T4.E 编排引擎边角 +- [ ] Map 节点错误传播 + 汇总(eino/graph.go:65 单子项失败无感知)| M +- [ ] 熔断器接回 failover(harness/circuitbreaker.go 与 llm/failover.go 未接合,可能长卡备用)| M +- [ ] Branch else 兜底 + coordinator 专家超时(专家卡死拖垮全局,coordinator.go:109)| M +- [ ] DSL 拓扑/节点-工具映射校验(dsl/parser.go:23 TODO,现仅 JSON 格式校验)| M + +### [ ] T4.F 健壮性 / 安全 / 性能收口(多为 S,可穿插着做) +- [ ] 关键 DB 写失败上浮 5xx(现 best-effort 返 200,前端无感,task_handler.go:69 等)| M +- [ ] KB 级联删事务化(kb.go:172 三库删一半失败仍删 PG → 不一致)| M +- [ ] 预算硬顶兜底(budget.go ≤0 即无限,单任务可无限烧)| M +- [ ] 审批 checkpoint 落盘失败重试(compose_compiler.go:323 现只 log → 任务永卡 waiting)| M +- [ ] 安全:CORS 缺省去 `*`、限流按登录用户(非纯 IP)、`X-Session-ID` 防伪、开发 JWT 密钥外置、输入校验补强 | S×N +- [ ] 性能:ListModels 去 O(N²)(admin.go:48)、KB/Agent 列表分页、报告分章检索缓存、Milvus 批量 ensure | S~M +- [ ] 可观测:panic 进 trace span + 回写通知、TTFT/token-s/各路检索耗时指标 | M +- [ ] 配置化:切块大小 / history 轮数 / 各并发度 收口为统一可配 | S +- [ ] 删死代码 `mcp-go/internal/search/hybrid.go`(空壳,真实 RAG 在 rag.go)| S + +--- + ## 进度回顾 | Tier | 完成 / 总 | |---|---| | T0 激活 | 2 / 2 ✅ | -| T1 收口 | 1 / 2 | +| T1 收口 | T1.1 ✅ / T1.2 进行中 / T1.3 进行中 | | T2 深化 | 4 / 4 ✅ | | T3 硬化 ⏸ | 0 / 5 | +| **T4 后端做实** | 0 / 6 组(A–F) | -> 推进顺序:T0.1 → T0.2 → T1.1 → T1.2 → T2.1 → T2.4(三段✅) → **T2.4 P0 Bleve 落盘** → … → (T3 按需)。 +> 推进顺序:T0/T1.1/T2 已清。当前在 T1.2/T1.3(前端深化 + 管理端做实)。 +> **后端下一步:T4.B(审计可溯源 + admin/overview)** —— 内聚、全 M、喂管理端、不依赖大重构。 +> 之后按价值挑:T4.D(AI 核心) / T4.E(编排边角) / T4.F(健壮性收口,可穿插) / T4.A(RBAC 大地基) / T4.C(计费)。 diff --git a/sundynix-gateway/internal/handler/admin.go b/sundynix-gateway/internal/handler/admin.go index fdc98da..dedd675 100644 --- a/sundynix-gateway/internal/handler/admin.go +++ b/sundynix-gateway/internal/handler/admin.go @@ -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 = "••••" diff --git a/sundynix-gateway/internal/router/router.go b/sundynix-gateway/internal/router/router.go index dfc2e3d..682876a 100644 --- a/sundynix-gateway/internal/router/router.go +++ b/sundynix-gateway/internal/router/router.go @@ -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 diff --git a/sundynix-gateway/internal/store/pgsql.go b/sundynix-gateway/internal/store/pgsql.go index 7ae94f0..0230d1c 100644 --- a/sundynix-gateway/internal/store/pgsql.go +++ b/sundynix-gateway/internal/store/pgsql.go @@ -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 {