195 lines
6.4 KiB
Go
195 lines
6.4 KiB
Go
package handler
|
||
|
||
// ========================================================================
|
||
// ObserveHandler —— /agent/view 面板观测与调试接口
|
||
// ========================================================================
|
||
// 端点清单(全部走全局 Auth,无放行):
|
||
// GET /api/v1/agent/logs 增量日志(内存环形缓冲)
|
||
// GET /api/v1/agent/system 进程运行时状态
|
||
// GET /api/v1/agent/config Agent 配置只读视图(含守卫词表)
|
||
// POST /api/v1/agent/guard-test 医疗守卫测试台
|
||
// POST /api/v1/agent/kb-test 知识库检索测试
|
||
// POST /api/v1/models/test 模型连通性测试(注册在 modelGroup)
|
||
//
|
||
// 安全边界:
|
||
// - 全部只读或无持久副作用(ModelTest 消耗少量 token)
|
||
// - 日志可能含 PHI(debug 开关打开时),logs 接口必须有 Auth
|
||
// - config 视图不返回任何密钥(EmbeddingAPIKey 等一律不出)
|
||
// ========================================================================
|
||
|
||
import (
|
||
"runtime"
|
||
"strconv"
|
||
"time"
|
||
|
||
"tcm-agent/internal/agentcfg"
|
||
"tcm-agent/internal/service"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// ObserveHandler 观测接口处理器
|
||
type ObserveHandler struct {
|
||
enhancer *service.EnhancerService // 复用 enhance 同款检索/模型解析逻辑
|
||
startedAt time.Time // 进程启动时间(算 uptime)
|
||
}
|
||
|
||
// NewObserveHandler 构造函数(router.Setup 启动时创建一次)
|
||
func NewObserveHandler(enhancer *service.EnhancerService) *ObserveHandler {
|
||
return &ObserveHandler{
|
||
enhancer: enhancer,
|
||
startedAt: time.Now(),
|
||
}
|
||
}
|
||
|
||
// Logs 增量拉取内存日志
|
||
//
|
||
// GET /api/v1/agent/logs?since_id=0&limit=200
|
||
// 面板首次加载 since_id=0 全量拉,之后带上一次返回的最大 ID 增量拉
|
||
func (h *ObserveHandler) Logs(c *gin.Context) {
|
||
sinceID, _ := strconv.ParseInt(c.DefaultQuery("since_id", "0"), 10, 64)
|
||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "200"))
|
||
entries := service.MemLog.List(sinceID, limit)
|
||
// 返回最大 ID 作为下次轮询游标(无新日志时沿用请求值)
|
||
lastID := sinceID
|
||
if len(entries) > 0 {
|
||
lastID = entries[len(entries)-1].ID
|
||
}
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"data": gin.H{
|
||
"entries": entries,
|
||
"last_id": lastID,
|
||
},
|
||
})
|
||
}
|
||
|
||
// System 进程运行时状态
|
||
//
|
||
// GET /api/v1/agent/system
|
||
// 面板「系统状态」Tab 5s 轮询,全部从 runtime 取,零外部依赖
|
||
func (h *ObserveHandler) System(c *gin.Context) {
|
||
var ms runtime.MemStats
|
||
runtime.ReadMemStats(&ms)
|
||
|
||
concUsed, concCap := service.GetConcurrency()
|
||
runUsed, runCap := service.RunLogUsage()
|
||
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"data": gin.H{
|
||
"started_at": h.startedAt.Unix(),
|
||
"uptime_seconds": int64(time.Since(h.startedAt).Seconds()),
|
||
"go_version": runtime.Version(),
|
||
"goroutines": runtime.NumGoroutine(),
|
||
// 内存三个关键指标:堆占用 / 累计分配 / 从 OS 拿到的总量
|
||
"heap_alloc_mb": float64(ms.HeapAlloc) / 1024 / 1024,
|
||
"sys_mb": float64(ms.Sys) / 1024 / 1024,
|
||
"num_gc": ms.NumGC,
|
||
// enhance 并发状态(信号量探针)
|
||
"enhance_concurrency": gin.H{"used": concUsed, "capacity": concCap},
|
||
// RunLog 缓冲占用
|
||
"runlog_usage": gin.H{"used": runUsed, "capacity": runCap},
|
||
},
|
||
})
|
||
}
|
||
|
||
// Config Agent 配置只读视图
|
||
//
|
||
// GET /api/v1/agent/config
|
||
// 数据来自 agentcfg.Get()(xk_system_config 实时加载,30s 缓存)+ 守卫词表。
|
||
// 面板只展示不修改——开关的写入口统一在 PHP 后台,避免双写
|
||
func (h *ObserveHandler) Config(c *gin.Context) {
|
||
cfg := agentcfg.Get()
|
||
white, black := service.MedicalGuardKeywords()
|
||
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"data": gin.H{
|
||
"react": gin.H{
|
||
"enabled": cfg.ReAct.Enabled,
|
||
"max_iterations": cfg.ReAct.MaxIterations,
|
||
"planning_enabled": cfg.ReAct.PlanningEnabled,
|
||
"reflection_enabled": cfg.ReAct.ReflectionEnabled,
|
||
"json_repair_enabled": cfg.ReAct.JSONRepairEnabled,
|
||
},
|
||
"token_budget": gin.H{
|
||
"enabled": cfg.TokenBudget.Enabled,
|
||
"per_request": cfg.TokenBudget.PerRequest,
|
||
"max_tokens_per_call": cfg.TokenBudget.MaxTokensPerCall,
|
||
},
|
||
"kb": gin.H{
|
||
"source": cfg.KB.Source,
|
||
"embedding_provider": cfg.KB.EmbeddingProvider,
|
||
"top_k": cfg.KB.TopK,
|
||
"search_mode": cfg.KB.SearchMode,
|
||
},
|
||
"medical_guard": gin.H{
|
||
"enabled": cfg.MedicalGuard.Enabled,
|
||
"whitelist": white,
|
||
"blacklist": black,
|
||
},
|
||
"debug": gin.H{
|
||
"log_request_body": cfg.Debug.LogRequestBody,
|
||
},
|
||
},
|
||
})
|
||
}
|
||
|
||
// GuardTest 医疗守卫测试台
|
||
//
|
||
// POST /api/v1/agent/guard-test body: {"text": "..."}
|
||
// 用与 Enhance 入口完全相同的守卫逻辑跑一遍给定文本
|
||
func (h *ObserveHandler) GuardTest(c *gin.Context) {
|
||
var req struct {
|
||
Text string `json:"text" binding:"required"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(400, gin.H{"code": 400, "message": "text 必填"})
|
||
return
|
||
}
|
||
result := service.GuardTest(req.Text)
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"data": gin.H{
|
||
"passed": result.Passed,
|
||
"reason": result.Reason,
|
||
"hit_whitelist": result.HitWhitelist,
|
||
"hit_blacklist": result.HitBlacklist,
|
||
},
|
||
})
|
||
}
|
||
|
||
// KBTest 知识库检索测试
|
||
//
|
||
// POST /api/v1/agent/kb-test body: {"query": "...", "top_k": 5}
|
||
// 走 enhance 主流程同款检索路径(含 ai_kb_source 分流),
|
||
// 回答「Agent 实际会检索到什么」
|
||
func (h *ObserveHandler) KBTest(c *gin.Context) {
|
||
var req struct {
|
||
Query string `json:"query" binding:"required"`
|
||
TopK int `json:"top_k"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(400, gin.H{"code": 400, "message": "query 必填"})
|
||
return
|
||
}
|
||
result := h.enhancer.KBTest(c.Request.Context(), req.Query, req.TopK)
|
||
c.JSON(200, gin.H{"code": 200, "data": result})
|
||
}
|
||
|
||
// ModelTest 模型连通性测试
|
||
//
|
||
// POST /api/v1/models/test body: {"provider": "", "message": ""}
|
||
// provider 留空 = 测当前生效配置;会真实调一次 LLM(max_tokens 64)
|
||
func (h *ObserveHandler) ModelTest(c *gin.Context) {
|
||
var req struct {
|
||
Provider string `json:"provider"`
|
||
Message string `json:"message"`
|
||
}
|
||
// body 可以整个为空(全默认),解析失败也不阻断
|
||
_ = c.ShouldBindJSON(&req)
|
||
result := h.enhancer.ModelTest(c.Request.Context(), req.Provider, req.Message)
|
||
c.JSON(200, gin.H{"code": 200, "data": result})
|
||
}
|