feat: Agent CLI 与终端审计回放全面优化
- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏 - 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复 - 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示 - 审计历史页只读(移除删除),移除回放弹框录制搜索
This commit is contained in:
1 parent
9a3dd304f7
commit
1acb0c49af
81 files changed
+3916
-1655
No files matched your search
@@ -190,23 +190,50 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
recorder := newAsciicastRecorder()
|
||||
const maxRecordSize = 1024 * 1024
|
||||
var cmdBuf strings.Builder
|
||||
sessionStatus := "closed"
|
||||
var finalizeOnce sync.Once
|
||||
|
||||
finalizeSession := func(status string) {
|
||||
endTime := time.Now()
|
||||
duration := int(endTime.Sub(termSession.StartTime).Seconds())
|
||||
recData := recorder.String()
|
||||
recSize := recorder.Len()
|
||||
log.Printf("[WebShell] Finalize session=%d status=%s duration=%ds rec=%d bytes", termSession.ID, status, duration, recSize)
|
||||
// Raw SQL with error logging
|
||||
result := h.db.Exec("UPDATE sys_terminal_sessions SET status=?, duration=?, end_time=?, record_data=?, record_size=? WHERE id=?",
|
||||
status, duration, endTime, recData, recSize, termSession.ID)
|
||||
if result.Error != nil {
|
||||
log.Printf("[WebShell] ERROR finalize session=%d: %v", termSession.ID, result.Error)
|
||||
} else {
|
||||
log.Printf("[WebShell] Finalize OK session=%d rows_affected=%d", termSession.ID, result.RowsAffected)
|
||||
// 命令输出字节统计:每条命令对应「下一条命令前」的 stdout 累计量 // Logan.Li
|
||||
var cmdTrack struct {
|
||||
mu sync.Mutex
|
||||
lastCmdID uint
|
||||
stdoutBytes int
|
||||
}
|
||||
flushCmdOutput := func() {
|
||||
cmdTrack.mu.Lock()
|
||||
prevID := cmdTrack.lastCmdID
|
||||
bytes := cmdTrack.stdoutBytes
|
||||
cmdTrack.stdoutBytes = 0
|
||||
cmdTrack.mu.Unlock()
|
||||
if prevID > 0 {
|
||||
_ = h.db.Model(&model.SessionCommand{}).Where("id = ?", prevID).
|
||||
Update("output_size", bytes).Error
|
||||
}
|
||||
}
|
||||
|
||||
// finalizeSession 会话结束落盘(sync.Once 防重复写入) // Logan.Li
|
||||
finalizeSession := func(status string) {
|
||||
sessionStatus = status
|
||||
finalizeOnce.Do(func() {
|
||||
endTime := time.Now()
|
||||
duration := int(endTime.Sub(termSession.StartTime).Seconds())
|
||||
recData := recorder.String()
|
||||
recSize := recorder.Len()
|
||||
log.Printf("[WebShell] Finalize session=%d status=%s duration=%ds rec=%d bytes", termSession.ID, status, duration, recSize)
|
||||
result := h.db.Exec("UPDATE sys_terminal_sessions SET status=?, duration=?, end_time=?, record_data=?, record_size=? WHERE id=?",
|
||||
status, duration, endTime, recData, recSize, termSession.ID)
|
||||
if result.Error != nil {
|
||||
log.Printf("[WebShell] ERROR finalize session=%d: %v", termSession.ID, result.Error)
|
||||
} else {
|
||||
log.Printf("[WebShell] Finalize OK session=%d rows_affected=%d", termSession.ID, result.RowsAffected)
|
||||
}
|
||||
})
|
||||
}
|
||||
defer func() {
|
||||
flushCmdOutput()
|
||||
finalizeSession(sessionStatus)
|
||||
}()
|
||||
|
||||
connectingMsg := "\r\n\x1b[36mConnecting to " + server.Hostname + " (" + server.IP + ":" + strconv.Itoa(server.Port) + ")...\x1b[0m\r\n"
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: connectingMsg})
|
||||
|
||||
@@ -335,6 +362,9 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
if recorder.Len() < maxRecordSize {
|
||||
recorder.WriteRow(data)
|
||||
}
|
||||
cmdTrack.mu.Lock()
|
||||
cmdTrack.stdoutBytes += n
|
||||
cmdTrack.mu.Unlock()
|
||||
if h.auditHub != nil {
|
||||
h.auditHub.Broadcast(termSession.ID, data)
|
||||
}
|
||||
@@ -366,13 +396,21 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
case 0:
|
||||
if msg.Data != "" {
|
||||
stdinPipe.Write([]byte(msg.Data))
|
||||
// 录入用户输入,回放可见键入内容 // Logan.Li
|
||||
if recorder.Len() < maxRecordSize {
|
||||
recorder.WriteRow([]byte(msg.Data))
|
||||
}
|
||||
|
||||
// Command extraction from stdin
|
||||
cmdBuf.WriteString(msg.Data)
|
||||
if strings.Contains(msg.Data, "\r") || strings.Contains(msg.Data, "\n") {
|
||||
cmd := extractCommand(cmdBuf.String())
|
||||
if cmd != "" {
|
||||
recordCommand(h.db, termSession.ID, termSession.ServerID, usernameStr, cmd)
|
||||
flushCmdOutput()
|
||||
newID := recordCommand(h.db, termSession.ID, termSession.ServerID, termSession.ServerName, usernameStr, cmd)
|
||||
cmdTrack.mu.Lock()
|
||||
cmdTrack.lastCmdID = newID
|
||||
cmdTrack.mu.Unlock()
|
||||
}
|
||||
cmdBuf.Reset()
|
||||
}
|
||||
@@ -392,10 +430,16 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
sshClient.Close()
|
||||
<-done
|
||||
stdinPipe.Close()
|
||||
finalizeSession("closed")
|
||||
sessionStatus = "closed"
|
||||
log.Printf("[WebShell] Disconnected %s (%s)", server.Hostname, addr)
|
||||
}
|
||||
|
||||
// shortShellCmds 常见 2 字符 shell 命令白名单 // Logan.Li
|
||||
var shortShellCmds = map[string]bool{
|
||||
"ls": true, "cd": true, "ps": true, "df": true, "id": true, "wc": true,
|
||||
"mv": true, "cp": true, "vi": true, "sh": true, "ll": true, "pwd": true,
|
||||
}
|
||||
|
||||
// extractCommand extracts a clean command from raw terminal input
|
||||
func extractCommand(input string) string {
|
||||
cleaned := strings.Map(func(r rune) rune {
|
||||
@@ -411,25 +455,31 @@ func extractCommand(input string) string {
|
||||
if len(cleaned) < 2 || len(cleaned) > 500 {
|
||||
return ""
|
||||
}
|
||||
// 过滤输入中途误触回车产生的 2 字符残片(如 up/uo),保留 ls/cd 等 // Logan.Li
|
||||
if len(cleaned) == 2 && !shortShellCmds[cleaned] {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(cleaned, "\x1b[") {
|
||||
return ""
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// recordCommand records a terminal command to sys_session_commands
|
||||
func recordCommand(db *gorm.DB, sessionID, serverID uint, username, command string) {
|
||||
// recordCommand 写入命令审计并返回记录 ID // Logan.Li
|
||||
func recordCommand(db *gorm.DB, sessionID, serverID uint, serverName, username, command string) uint {
|
||||
now := time.Now()
|
||||
cmd := model.SessionCommand{
|
||||
SessionID: sessionID,
|
||||
ServerID: serverID,
|
||||
Username: username,
|
||||
Command: command,
|
||||
Timestamp: now,
|
||||
SessionID: sessionID,
|
||||
ServerID: serverID,
|
||||
ServerName: serverName,
|
||||
Username: username,
|
||||
Command: command,
|
||||
Timestamp: now,
|
||||
}
|
||||
if err := db.Create(&cmd).Error; err != nil {
|
||||
log.Printf("[WebShell] Command record failed: %v", err)
|
||||
} else {
|
||||
log.Printf("[WebShell] Command recorded session=%d cmd=%q", sessionID, command)
|
||||
return 0
|
||||
}
|
||||
log.Printf("[WebShell] Command recorded session=%d cmd=%q", sessionID, command)
|
||||
return cmd.ID
|
||||
}
|
||||
Reference in new issue
Block a user