Files
xk-ai-agent/internal/handler/observe_handler.go

195 lines
6.4 KiB
Go
Raw Permalink Normal View History

2026-08-14 21:50:48 +08:00
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
// - 日志可能含 PHIdebug 开关打开时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 留空 = 测当前生效配置;会真实调一次 LLMmax_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})
}