feat: Agent CLI 与终端审计回放全面优化

- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏
- 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复
- 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示
- 审计历史页只读(移除删除),移除回放弹框录制搜索
This commit is contained in:
Logan committed 2026-07-01 12:13:53 +08:00
1 parent 9a3dd304f7
commit 1acb0c49af
81 files changed
+3916 -1655

No files matched your search

+60 -2
View File
@@ -4,8 +4,10 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"ezops/middleware"
"ezops/model"
"github.com/gin-gonic/gin"
@@ -48,6 +50,26 @@ func (h *TerminalSessionHandler) ListTerminalSessions(c *gin.Context) {
if req.Status != "" {
query = query.Where("status = ?", req.Status)
}
if kw := strings.TrimSpace(req.Keyword); kw != "" {
like := "%" + kw + "%"
query = query.Where(
"username LIKE ? OR server_name LIKE ? OR server_ip LIKE ? OR client_ip LIKE ?",
like, like, like, like,
)
}
if req.StartDate != "" {
if t, err := time.ParseInLocation("2006-01-02", req.StartDate, time.Local); err == nil {
query = query.Where("start_time >= ?", t)
}
}
if req.EndDate != "" {
if t, err := time.ParseInLocation("2006-01-02", req.EndDate, time.Local); err == nil {
query = query.Where("start_time < ?", t.Add(24*time.Hour))
}
}
if req.HasRecord == "1" || strings.EqualFold(req.HasRecord, "true") {
query = query.Where("record_size > 0")
}
var total int64
if err := query.Count(&total).Error; err != nil {
@@ -173,11 +195,17 @@ func (h *TerminalSessionHandler) KillSession(c *gin.Context) {
return
}
// 强制终止 WebShell 连接
// 强制终止 WebShell 连接;在线连接由 webshell finalize 落盘录制,勿抢先 UPDATE 覆盖 // Logan.Li
killed := false
if h.killFunc != nil {
h.killFunc(session.ID)
killed = h.killFunc(session.ID)
}
if killed {
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已终止"})
return
}
// 僵尸会话(连接已断开、服务重启等):无录制可落盘,仅关闭记录
now := time.Now()
duration := int(now.Sub(session.StartTime).Seconds())
if err := h.db.Model(&session).Updates(map[string]interface{}{
@@ -222,6 +250,36 @@ func (h *TerminalSessionHandler) DownloadSession(c *gin.Context) {
c.String(http.StatusOK, session.RecordData)
}
// LogReplayView 记录终端回放查看审计(二次审计留痕) // Logan.Li
// POST /api/v1/terminal-sessions/:id/replay-view
func (h *TerminalSessionHandler) LogReplayView(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"})
return
}
var session model.TerminalSession
if err := h.db.Select("id", "username", "server_name", "server_ip").First(&session, id).Error; err != nil {
c.JSON(http.StatusOK, model.Response{Code: 404, Message: "会话不存在"})
return
}
uid := middleware.GetUserID(c)
log := model.AuditLog{
UserID: &uid,
Username: middleware.GetUsername(c),
IP: c.ClientIP(),
UserAgent: c.GetHeader("User-Agent"),
Module: "terminal_audit",
Action: "replay_view",
Resource: "terminal_session",
ResourceID: strconv.FormatUint(id, 10),
Request: fmt.Sprintf("回放终端会话 #%d(%s → %s/%s)", id, session.Username, session.ServerName, session.ServerIP),
Status: 200,
}
_ = h.db.Create(&log).Error // 落库失败不阻断回放
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "ok"})
}
// StreamSession 终端会话实时流(WebSocket 暂未实现)
// GET /api/v1/terminal-sessions/:id/stream
func (h *TerminalSessionHandler) StreamSession(c *gin.Context) {