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
@@ -0,0 +1,144 @@
|
||||
// persistent_client.go - 任务级 SSH 长连接,多次 Exec 复用同一 client // Logan.Li
|
||||
package remoteexec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// PersistentClient 可复用的 SSH 长连接(任务生命周期内持有,Close 释放) // Logan.Li
|
||||
type PersistentClient struct {
|
||||
cfg SSHConfig
|
||||
client *ssh.Client
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewPersistentClient 建立 SSH 长连接 // Logan.Li
|
||||
func NewPersistentClient(cfg SSHConfig) (*PersistentClient, error) {
|
||||
client, err := connect(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PersistentClient{cfg: cfg, client: client}, nil
|
||||
}
|
||||
|
||||
// Exec 在长连接上执行命令;连接断开时自动重连一次 // Logan.Li
|
||||
func (p *PersistentClient) Exec(command string, timeout time.Duration) (*ExecResult, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
result, err := p.execLocked(command, timeout)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
// 连接级失败:重连后重试一次 // Logan.Li
|
||||
if p.client != nil {
|
||||
_ = p.client.Close()
|
||||
p.client = nil
|
||||
}
|
||||
client, connErr := connect(p.cfg)
|
||||
if connErr != nil {
|
||||
return result, err
|
||||
}
|
||||
p.client = client
|
||||
return p.execLocked(command, timeout)
|
||||
}
|
||||
|
||||
// execLocked 假定已持锁,在当前 client 上执行 // Logan.Li
|
||||
func (p *PersistentClient) execLocked(command string, timeout time.Duration) (*ExecResult, error) {
|
||||
if p.client == nil {
|
||||
client, err := connect(p.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.client = client
|
||||
}
|
||||
return execWithClient(p.client, p.cfg, command, timeout)
|
||||
}
|
||||
|
||||
// Close 关闭长连接 // Logan.Li
|
||||
func (p *PersistentClient) Close() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.client == nil {
|
||||
return nil
|
||||
}
|
||||
err := p.client.Close()
|
||||
p.client = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// execWithClient 在已有 SSH client 上执行命令(供 Exec / PersistentClient 共用) // Logan.Li
|
||||
func execWithClient(client *ssh.Client, cfg SSHConfig, command string, timeout time.Duration) (*ExecResult, error) {
|
||||
start := time.Now()
|
||||
result := &ExecResult{Host: cfg.Host, Port: cfg.Port}
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
result.Duration = time.Since(start).Milliseconds()
|
||||
result.Error = fmt.Sprintf("创建SSH会话失败: %v", err)
|
||||
return result, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
if timeout <= 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
type execOut struct {
|
||||
output []byte
|
||||
err error
|
||||
}
|
||||
ch := make(chan execOut, 1)
|
||||
|
||||
go func() {
|
||||
var stdout, stderr bytes.Buffer
|
||||
session.Stdout = &stdout
|
||||
session.Stderr = &stderr
|
||||
runErr := session.Run(command)
|
||||
|
||||
output := stdout.Bytes()
|
||||
if stderr.Len() > 0 {
|
||||
if len(output) > 0 {
|
||||
output = append(output, '\n')
|
||||
}
|
||||
output = append(output, stderr.Bytes()...)
|
||||
}
|
||||
ch <- execOut{output: output, err: runErr}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
result.Duration = time.Since(start).Milliseconds()
|
||||
result.Error = fmt.Sprintf("执行超时(%v)", timeout)
|
||||
result.ExitCode = -1
|
||||
return result, fmt.Errorf("exec timeout after %v", timeout)
|
||||
case out := <-ch:
|
||||
result.Duration = time.Since(start).Milliseconds()
|
||||
result.Output = string(out.output)
|
||||
|
||||
if out.err != nil {
|
||||
if exitErr, ok := out.err.(*ssh.ExitError); ok {
|
||||
result.ExitCode = exitErr.ExitStatus()
|
||||
result.Success = false
|
||||
} else {
|
||||
result.ExitCode = -1
|
||||
result.Error = out.err.Error()
|
||||
result.Success = false
|
||||
return result, out.err
|
||||
}
|
||||
} else {
|
||||
result.ExitCode = 0
|
||||
result.Success = true
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in new issue
Block a user