Files
xk-ai-agent/internal/handler/observe_handler.go
2026-08-14 21:50:48 +08:00

195 lines
6.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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})
}