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
@@ -11,6 +11,7 @@ package agentcli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"ezops/pkg/agentcli/core"
|
||||
"ezops/pkg/agentcli/evolver"
|
||||
@@ -54,9 +55,13 @@ func NewEngine(cfg EngineConfig) (*Engine, error) {
|
||||
// LLM 客户端
|
||||
client := llm.NewClient(cfg.LLM)
|
||||
|
||||
// 工具注册中心:ssh_execute(注入目标主机凭据)+ analyze_output
|
||||
// 工具注册中心:ssh_execute(SSH 长连接)+ analyze_output
|
||||
registry := tools.NewRegistry()
|
||||
registry.Register(tools.NewSSHExecuteTool(cfg.SSH, coreCfg.ToolTimeout))
|
||||
sshTool, err := tools.NewSSHExecuteTool(cfg.SSH, coreCfg.ToolTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh_execute 初始化失败: %w", err)
|
||||
}
|
||||
registry.Register(sshTool)
|
||||
registry.Register(tools.NewAnalyzeOutputTool())
|
||||
|
||||
// 安全控制器
|
||||
@@ -81,16 +86,9 @@ func (e *Engine) SetConfirmFn(fn safety.ConfirmFn) { e.safety.SetConfirmFn(fn) }
|
||||
// SetAutoApprove 测试用:自动批准(生产禁用) // Logan.Li
|
||||
func (e *Engine) SetAutoApprove(b bool) { e.safety.SetAutoApprove(b) }
|
||||
|
||||
// Run 执行任务(ReAct 循环),serverInfo 为目标主机摘要(注入 system prompt) // Logan.Li
|
||||
func (e *Engine) Run(ctx context.Context, query, serverInfo string) (string, error) {
|
||||
res, err := e.agent.Run(ctx, query, serverInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if res != nil {
|
||||
return res.Output, nil
|
||||
}
|
||||
return "", nil
|
||||
// Run 执行任务(ReAct 循环);prior 为上一轮对话历史(不含 system),priorTokens 为累计 token // Logan.Li
|
||||
func (e *Engine) Run(ctx context.Context, query, serverInfo string, prior []llm.Message, priorTokens int) (*core.RunResult, error) {
|
||||
return e.agent.Run(ctx, query, serverInfo, prior, priorTokens)
|
||||
}
|
||||
|
||||
// EvolveStats 进化统计(供审计展示) // Logan.Li
|
||||
@@ -101,8 +99,11 @@ func (e *Engine) EvolveStats() map[string]interface{} {
|
||||
return e.evolver.GetStats()
|
||||
}
|
||||
|
||||
// Close 释放资源 // Logan.Li
|
||||
// Close 释放资源(含 SSH 长连接) // Logan.Li
|
||||
func (e *Engine) Close() {
|
||||
if e.tools != nil {
|
||||
e.tools.Close()
|
||||
}
|
||||
if e.agent != nil {
|
||||
e.agent.Close()
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ import (
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventThought EventType = "thought" // AI 思考/推理文本
|
||||
EventToolCall EventType = "tool_call" // 即将执行的工具(名/参数/风险)
|
||||
EventThought EventType = "thought" // AI 思考/推理文本(流式结束后的完整内容)
|
||||
EventThoughtDelta EventType = "thought_delta" // AI 思考流式增量 token
|
||||
EventToolCall EventType = "tool_call" // 即将执行的工具(名/参数/风险)
|
||||
EventToolResult EventType = "tool_result" // 工具执行结果(输出/成败/耗时)
|
||||
EventProgress EventType = "progress" // 轮次进度 + token
|
||||
EventDone EventType = "done" // 任务完成(最终答案)
|
||||
@@ -50,6 +51,14 @@ type Event struct {
|
||||
// EventCallback 事件回调(controller 注入) // Logan.Li
|
||||
type EventCallback func(Event)
|
||||
|
||||
// RunResult ReAct 执行结果(含对话历史供多轮追问) // Logan.Li
|
||||
type RunResult struct {
|
||||
Success bool
|
||||
Output string
|
||||
History []llm.Message // system 之后的对话历史
|
||||
TotalTokens int
|
||||
}
|
||||
|
||||
// Config Agent 运行配置 // Logan.Li
|
||||
type Config struct {
|
||||
MaxIterations int
|
||||
@@ -60,6 +69,7 @@ type Config struct {
|
||||
OutputMaxLength int
|
||||
MaxContextTokens int
|
||||
MemoryDir string // Evolver 记忆目录
|
||||
RoundOffset int // 轮次偏移(多轮追问时接续上一轮编号) // Logan.Li
|
||||
}
|
||||
|
||||
// DefaultConfig 默认配置 // Logan.Li
|
||||
@@ -112,9 +122,9 @@ func (a *Agent) emit(e Event) {
|
||||
}
|
||||
}
|
||||
|
||||
// Run 执行单次任务(ReAct 循环 + Evolver 集成) // Logan.Li
|
||||
func (a *Agent) Run(ctx context.Context, query, serverInfo string) (*tools.Result, error) {
|
||||
a.totalTokens = 0
|
||||
// Run 执行单次任务(ReAct 循环 + Evolver 集成);prior 为不含 system 的历史对话 // Logan.Li
|
||||
func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.Message, priorTokens int) (*RunResult, error) {
|
||||
a.totalTokens = priorTokens
|
||||
start := time.Now()
|
||||
|
||||
// Evolver PREDICT:获取历史经验注入 system prompt
|
||||
@@ -127,10 +137,11 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string) (*tools.Resul
|
||||
toolDescs := a.buildToolDescriptions()
|
||||
systemPrompt := prompt.BuildSystemPrompt(serverInfo, toolDescs, evolveCtx)
|
||||
|
||||
messages := []llm.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: query},
|
||||
messages := []llm.Message{{Role: "system", Content: systemPrompt}}
|
||||
if len(prior) > 0 {
|
||||
messages = append(messages, prior...)
|
||||
}
|
||||
messages = append(messages, llm.Message{Role: "user", Content: query})
|
||||
llmTools := a.registry.ToLLMTools()
|
||||
|
||||
toolCallHistory := make(map[string]int) // 循环检测
|
||||
@@ -144,7 +155,7 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string) (*tools.Resul
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.emit(Event{Type: EventProgress, Round: iter + 1})
|
||||
a.emit(Event{Type: EventProgress, Round: a.roundNum(iter)})
|
||||
|
||||
// 自适应检查点
|
||||
if hint := a.checkAdaptiveCheckpoint(iter, toolCallHistory, consecutiveFailures, totalToolCalls); hint != "" {
|
||||
@@ -154,12 +165,17 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string) (*tools.Resul
|
||||
// 裁剪历史
|
||||
trimmed := a.trimMessages(messages)
|
||||
|
||||
// 调用 LLM
|
||||
resp, err := a.llm.Complete(ctx, &llm.CompletionRequest{
|
||||
// 调用 LLM(流式推送思考增量,降低长推理等待感) // Logan.Li
|
||||
round := a.roundNum(iter)
|
||||
resp, err := a.llm.Stream(ctx, &llm.CompletionRequest{
|
||||
Messages: trimmed,
|
||||
Tools: llmTools,
|
||||
Temperature: a.config.Temperature,
|
||||
MaxTokens: a.config.MaxTokens,
|
||||
}, func(delta string) {
|
||||
if strings.TrimSpace(delta) != "" {
|
||||
a.emit(Event{Type: EventThoughtDelta, Round: round, Thought: delta})
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
a.emit(Event{Type: EventError, Message: "LLM 调用失败: " + err.Error()})
|
||||
@@ -169,9 +185,9 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string) (*tools.Resul
|
||||
a.totalTokens += resp.Usage.TotalTokens
|
||||
a.emit(Event{Type: EventProgress, Tokens: a.totalTokens})
|
||||
|
||||
// 推送思考文本
|
||||
// 推送完整思考文本(供持久化与回放) // Logan.Li
|
||||
if strings.TrimSpace(resp.Message.Content) != "" {
|
||||
a.emit(Event{Type: EventThought, Round: iter + 1, Thought: resp.Message.Content})
|
||||
a.emit(Event{Type: EventThought, Round: round, Thought: resp.Message.Content})
|
||||
}
|
||||
messages = append(messages, resp.Message)
|
||||
|
||||
@@ -180,11 +196,14 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string) (*tools.Resul
|
||||
answer := resp.Message.Content
|
||||
a.emit(Event{Type: EventDone, Answer: answer, Tokens: a.totalTokens})
|
||||
a.triggerEvolve(ctx, query, serverInfo, answer, toolRecords, true, start)
|
||||
return &tools.Result{Success: true, Output: answer}, nil
|
||||
return &RunResult{
|
||||
Success: true, Output: answer,
|
||||
History: a.extractHistory(messages), TotalTokens: a.totalTokens,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 处理工具调用
|
||||
observations, records, failInc := a.processToolCalls(ctx, resp.Message.ToolCalls, toolCallHistory, iter+1)
|
||||
observations, records, failInc := a.processToolCalls(ctx, resp.Message.ToolCalls, toolCallHistory, round)
|
||||
toolRecords = append(toolRecords, records...)
|
||||
messages = append(messages, observations...)
|
||||
totalToolCalls += len(resp.Message.ToolCalls)
|
||||
@@ -195,7 +214,9 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string) (*tools.Resul
|
||||
msg := fmt.Sprintf("达到最大迭代次数(%d),任务未能完成。可能任务过于复杂或需要更多步骤。", a.config.MaxIterations)
|
||||
a.emit(Event{Type: EventError, Message: msg, Tokens: a.totalTokens})
|
||||
a.triggerEvolve(ctx, query, serverInfo, "", toolRecords, false, start)
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
return &RunResult{
|
||||
Success: false, History: a.extractHistory(messages), TotalTokens: a.totalTokens,
|
||||
}, fmt.Errorf("%s", msg)
|
||||
}
|
||||
|
||||
// processToolCalls 处理工具调用,返回 observation 消息 + 记录 // Logan.Li
|
||||
@@ -264,6 +285,13 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
||||
success = false
|
||||
}
|
||||
|
||||
// 超长 ssh 输出自动分析,避免 token 爆炸 // Logan.Li
|
||||
if tc.Function.Name == "ssh_execute" && success && len(output) > a.autoAnalyzeThreshold() {
|
||||
if analyzed := a.autoAnalyzeOutput(ctx, output); analyzed != "" {
|
||||
output = analyzed
|
||||
}
|
||||
}
|
||||
|
||||
// 输出截断
|
||||
if len(output) > a.config.OutputMaxLength {
|
||||
output = output[:a.config.OutputMaxLength] +
|
||||
@@ -313,6 +341,43 @@ func (a *Agent) triggerEvolve(ctx context.Context, query, serverHost, answer str
|
||||
}()
|
||||
}
|
||||
|
||||
// roundNum 计算对外展示的 ReAct 轮次(含多轮追问偏移) // Logan.Li
|
||||
func (a *Agent) roundNum(iter int) int {
|
||||
return a.config.RoundOffset + iter + 1
|
||||
}
|
||||
|
||||
// extractHistory 提取 system 之后的对话历史(供多轮追问) // Logan.Li
|
||||
func (a *Agent) extractHistory(messages []llm.Message) []llm.Message {
|
||||
if len(messages) <= 1 {
|
||||
return nil
|
||||
}
|
||||
out := make([]llm.Message, len(messages)-1)
|
||||
copy(out, messages[1:])
|
||||
return out
|
||||
}
|
||||
|
||||
// autoAnalyzeThreshold 触发自动 analyze_output 的输出长度阈值 // Logan.Li
|
||||
func (a *Agent) autoAnalyzeThreshold() int {
|
||||
t := a.config.OutputMaxLength / 2
|
||||
if t < 4000 {
|
||||
return 4000
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// autoAnalyzeOutput 对超长命令输出自动调用 analyze_output // Logan.Li
|
||||
func (a *Agent) autoAnalyzeOutput(ctx context.Context, output string) string {
|
||||
tool, err := a.registry.Get("analyze_output")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
r, err := tool.Execute(ctx, map[string]interface{}{"output": output})
|
||||
if err != nil || r == nil || !r.Success || strings.TrimSpace(r.Output) == "" {
|
||||
return ""
|
||||
}
|
||||
return r.Output + "\n[系统自动分析超长输出]"
|
||||
}
|
||||
|
||||
// buildToolDescriptions 工具描述文本 // Logan.Li
|
||||
func (a *Agent) buildToolDescriptions() string {
|
||||
var sb strings.Builder
|
||||
@@ -428,9 +493,5 @@ func estimateTokens(s string) int {
|
||||
return len([]rune(s))/3 + 1
|
||||
}
|
||||
|
||||
// Close 释放资源 // Logan.Li
|
||||
func (a *Agent) Close() {
|
||||
if a.registry != nil {
|
||||
// Registry 无 Closer 实现(Web 工具无连接池),保留接口位
|
||||
}
|
||||
}
|
||||
// Close 释放资源(SSH 长连接由 Engine.Close 统一释放) // Logan.Li
|
||||
func (a *Agent) Close() {}
|
||||
@@ -42,6 +42,7 @@ type EvolveResult struct {
|
||||
LearnedHint string `json:"learned_hint"`
|
||||
ToolSequence []string `json:"tool_sequence"`
|
||||
ExperienceAdded bool `json:"experience_added"`
|
||||
SkillAdded bool `json:"skill_added"`
|
||||
Consolidated bool `json:"consolidated"`
|
||||
}
|
||||
|
||||
@@ -107,6 +108,21 @@ func (e *EvolverEngine) Evolve(ctx context.Context, exec *TaskExecution) *Evolve
|
||||
go e.memorizeFact(ctx, exec) // 异步,不阻塞进化主流程
|
||||
}
|
||||
|
||||
// Step 6: PROCEDURAL —— 成功任务沉淀程序记忆(工具命令序列) // Logan.Li
|
||||
if exec.Success {
|
||||
if steps := extractSSHSteps(exec.ToolCalls); len(steps) >= 2 {
|
||||
skill := SkillEntry{
|
||||
Name: result.TaskType + "标准流程",
|
||||
When: result.TaskType,
|
||||
Steps: steps,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
if err := e.mem.AddSkill(skill); err == nil {
|
||||
result.SkillAdded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -194,6 +210,12 @@ func (e *EvolverEngine) GetContextForPrompt(query string) string {
|
||||
if hosts := e.mem.Hosts(); len(hosts) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" 【已知主机】%s\n", strings.Join(hosts, ", ")))
|
||||
}
|
||||
if skills := e.mem.FindSkills(taskType, 2); len(skills) > 0 {
|
||||
sb.WriteString(" 【推荐流程】\n")
|
||||
for _, sk := range skills {
|
||||
sb.WriteString(fmt.Sprintf(" - %s: %s\n", sk.Name, strings.Join(sk.Steps, " → ")))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -258,6 +280,20 @@ func resultToolNames(calls []ToolCallRecord) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// extractSSHSteps 从工具调用记录提取 ssh_execute 命令序列 // Logan.Li
|
||||
func extractSSHSteps(calls []ToolCallRecord) []string {
|
||||
out := make([]string, 0, len(calls))
|
||||
for _, c := range calls {
|
||||
if c.ToolName != "ssh_execute" {
|
||||
continue
|
||||
}
|
||||
if cmd, ok := c.Args["command"].(string); ok && strings.TrimSpace(cmd) != "" {
|
||||
out = append(out, strings.TrimSpace(cmd))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func uniqueStrings(in []string) []string {
|
||||
seen := make(map[string]bool)
|
||||
out := make([]string, 0, len(in))
|
||||
|
||||
@@ -235,6 +235,21 @@ func (m *Memories) Hosts() []string {
|
||||
return readJSON(m.envPath, KnownHosts{}).Hosts
|
||||
}
|
||||
|
||||
// FindSkills 按任务类型查找程序记忆(运维流程,最多 n 条) // Logan.Li
|
||||
func (m *Memories) FindSkills(taskType string, n int) []SkillEntry {
|
||||
all := readJSONL[SkillEntry](m.sklPath)
|
||||
var matched []SkillEntry
|
||||
for _, s := range all {
|
||||
if taskType == "" || s.When == taskType {
|
||||
matched = append(matched, s)
|
||||
}
|
||||
}
|
||||
if len(matched) > n {
|
||||
matched = matched[len(matched)-n:]
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
// CountStats 统计各层记忆条目数 // Logan.Li
|
||||
func (m *Memories) CountStats() map[string]int {
|
||||
return map[string]int{
|
||||
|
||||
@@ -42,6 +42,12 @@ type anthropicMessage struct {
|
||||
Content json.RawMessage `json:"content"`
|
||||
}
|
||||
|
||||
// anthropicThinking 启用 Claude thinking 模式(Claude 3.7 Sonnet 支持) // Logan.Li
|
||||
type anthropicThinking struct {
|
||||
Type string `json:"type"`
|
||||
BudgetTokens int `json:"budget_tokens"`
|
||||
}
|
||||
|
||||
// anthropicRequest Anthropic /v1/messages 请求体 // Logan.Li
|
||||
type anthropicRequest struct {
|
||||
Model string `json:"model"`
|
||||
@@ -51,6 +57,7 @@ type anthropicRequest struct {
|
||||
Tools []anthropicTool `json:"tools,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Thinking *anthropicThinking `json:"thinking,omitempty"` // Claude 3.7 Sonnet 推理模式 // Logan.Li
|
||||
}
|
||||
|
||||
// anthropicTool Anthropic 工具定义(input_schema,对应 OpenAI 的 parameters) // Logan.Li
|
||||
@@ -157,6 +164,12 @@ func buildTools(tools []Tool) []anthropicTool {
|
||||
return out
|
||||
}
|
||||
|
||||
// isAnthropicThinkingModel 判断是否为支持 thinking 的 Claude 模型 // Logan.Li
|
||||
func isAnthropicThinkingModel(model string) bool {
|
||||
m := strings.ToLower(model)
|
||||
return strings.Contains(m, "claude-3-7") || strings.Contains(m, "claude-3.7")
|
||||
}
|
||||
|
||||
// buildRequest 构造 Anthropic 请求体(Complete 与 Stream 共用) // Logan.Li
|
||||
func (c *AnthropicClient) buildRequest(req *CompletionRequest, stream bool) ([]byte, error) {
|
||||
system, msgs := c.buildMessages(req)
|
||||
@@ -168,6 +181,19 @@ func (c *AnthropicClient) buildRequest(req *CompletionRequest, stream bool) ([]b
|
||||
if payload.MaxTokens <= 0 {
|
||||
payload.MaxTokens = 4096
|
||||
}
|
||||
// 启用 Claude thinking 模式时,temperature 必须固定为 1,max_tokens 需大于 budget_tokens // Logan.Li
|
||||
enabled := req.ReasoningEnabled || c.cfg.ReasoningEnabled
|
||||
if enabled && isAnthropicThinkingModel(payload.Model) {
|
||||
payload.Temperature = 1
|
||||
budget := payload.MaxTokens / 2
|
||||
if budget < 1024 {
|
||||
budget = 1024
|
||||
}
|
||||
if payload.MaxTokens <= budget {
|
||||
payload.MaxTokens = budget + 1024
|
||||
}
|
||||
payload.Thinking = &anthropicThinking{Type: "enabled", BudgetTokens: budget}
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||
|
||||
@@ -17,13 +17,14 @@ import (
|
||||
|
||||
// Config LLM 客户端配置(endpoint 已由上层解析为完整 base url) // Logan.Li
|
||||
type Config struct {
|
||||
Protocol string // "openai" | "anthropic",由 client.go 工厂决定具体实现
|
||||
Endpoint string // 完整 base url,如 https://api.deepseek.com/v1
|
||||
APIKey string // Bearer token
|
||||
Model string // 模型名,如 deepseek-chat / gpt-4o-mini
|
||||
Temperature float64 // 采样温度
|
||||
MaxTokens int // 单次最大输出 token
|
||||
Timeout time.Duration // HTTP 超时(0 表示使用默认 180s)
|
||||
Protocol string // "openai" | "anthropic",由 client.go 工厂决定具体实现
|
||||
Endpoint string // 完整 base url,如 https://api.deepseek.com/v1
|
||||
APIKey string // Bearer token
|
||||
Model string // 模型名,如 deepseek-chat / gpt-4o-mini
|
||||
Temperature float64 // 采样温度
|
||||
MaxTokens int // 单次最大输出 token
|
||||
ReasoningEnabled bool // 是否启用推理/Plan 模式(OpenAI o-series / DeepSeek R1 等) // Logan.Li
|
||||
Timeout time.Duration // HTTP 超时(0 表示使用默认 180s)
|
||||
}
|
||||
|
||||
// OpenAIClient OpenAI 兼容客户端 // Logan.Li
|
||||
@@ -48,15 +49,45 @@ func NewOpenAIClient(cfg Config) Client {
|
||||
// Name 返回客户端名称 // Logan.Li
|
||||
func (c *OpenAIClient) Name() string { return "openai-compatible(" + c.cfg.Model + ")" }
|
||||
|
||||
// isOpenAIReasoningModel 判断是否为需要 reasoning_effort 的 OpenAI o-series 模型 // Logan.Li
|
||||
func isOpenAIReasoningModel(model string) bool {
|
||||
m := strings.ToLower(model)
|
||||
return strings.Contains(m, "o1") || strings.Contains(m, "o3")
|
||||
}
|
||||
|
||||
// isGenericReasoningModel 判断是否为通用推理开关模型(DeepSeek R1 等) // Logan.Li
|
||||
func isGenericReasoningModel(model string) bool {
|
||||
m := strings.ToLower(model)
|
||||
return strings.Contains(m, "reasoning") || strings.Contains(m, "r1") || strings.Contains(m, "deepseek-reasoner")
|
||||
}
|
||||
|
||||
// applyReasoningParams 根据模型类型应用 reasoning 参数 // Logan.Li
|
||||
func applyReasoningParams(payload *openaiRequest, enabled bool) {
|
||||
if !enabled {
|
||||
return
|
||||
}
|
||||
if isOpenAIReasoningModel(payload.Model) {
|
||||
// OpenAI o-series 使用 reasoning_effort,temperature 必须固定为 1(不传) // Logan.Li
|
||||
payload.ReasoningEffort = "medium"
|
||||
payload.Temperature = 0
|
||||
return
|
||||
}
|
||||
if isGenericReasoningModel(payload.Model) {
|
||||
payload.Reasoning = true
|
||||
}
|
||||
}
|
||||
|
||||
// openaiRequest OpenAI chat/completions 请求体 // Logan.Li
|
||||
type openaiRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
ToolChoice string `json:"tool_choice,omitempty"` // "auto" 让模型自主决定
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"` // 流式补全(Stream 方法置 true)
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
ToolChoice string `json:"tool_choice,omitempty"` // "auto" 让模型自主决定
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"` // 流式补全(Stream 方法置 true)
|
||||
ReasoningEffort string `json:"reasoning_effort,omitempty"` // OpenAI o-series 推理强度 // Logan.Li
|
||||
Reasoning bool `json:"reasoning,omitempty"` // DeepSeek 等部分平台推理开关 // Logan.Li
|
||||
}
|
||||
|
||||
// openaiResponse OpenAI chat/completions 响应体 // Logan.Li
|
||||
@@ -89,6 +120,7 @@ func (c *OpenAIClient) Complete(ctx context.Context, req *CompletionRequest) (*C
|
||||
if payload.MaxTokens <= 0 {
|
||||
payload.MaxTokens = 4096
|
||||
}
|
||||
applyReasoningParams(&payload, req.ReasoningEnabled || c.cfg.ReasoningEnabled)
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||
@@ -151,6 +183,7 @@ func (c *OpenAIClient) Stream(ctx context.Context, req *CompletionRequest, onDel
|
||||
if payload.MaxTokens <= 0 {
|
||||
payload.MaxTokens = 4096
|
||||
}
|
||||
applyReasoningParams(&payload, req.ReasoningEnabled || c.cfg.ReasoningEnabled)
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||
|
||||
@@ -43,10 +43,11 @@ type ToolFunction struct {
|
||||
|
||||
// CompletionRequest 补全请求参数 // Logan.Li
|
||||
type CompletionRequest struct {
|
||||
Messages []Message `json:"messages"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Messages []Message `json:"messages"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
ReasoningEnabled bool `json:"reasoning_enabled,omitempty"` // 是否启用推理/Plan 模式(协议相关) // Logan.Li
|
||||
}
|
||||
|
||||
// CompletionResponse 补全响应(取首个 choice) // Logan.Li
|
||||
|
||||
@@ -153,6 +153,17 @@ func StringParam(args map[string]interface{}, key string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Close 关闭所有实现了 Close() 的工具(如 ssh_execute 长连接) // Logan.Li
|
||||
func (r *Registry) Close() {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
for _, t := range r.tools {
|
||||
if c, ok := t.(interface{ Close() error }); ok {
|
||||
_ = c.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IntParam 取整型参数(兼容 int/float64/int64) // Logan.Li
|
||||
func IntParam(args map[string]interface{}, key string) int {
|
||||
if v, ok := args[key]; ok {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// ssh_execute.go - 远程命令执行工具 // Logan.Li
|
||||
// 复用 aiops 既有 pkg/remoteexec(Go 原生 SSH),凭据由 controller 从 model.Server 构建后注入。
|
||||
// 任务级复用 PersistentClient 长连接,避免每条命令重建 SSH 握手。
|
||||
// 实现 DynamicRiskTool:依据具体命令动态评估风险,决定是否触发用户确认。
|
||||
package tools
|
||||
|
||||
@@ -14,16 +15,28 @@ import (
|
||||
|
||||
// SSHExecuteTool 远程命令执行工具 // Logan.Li
|
||||
type SSHExecuteTool struct {
|
||||
cfg remoteexec.SSHConfig // 目标主机连接配置(含凭据)
|
||||
timeout time.Duration // 默认命令超时
|
||||
client *remoteexec.PersistentClient // 任务级 SSH 长连接
|
||||
timeout time.Duration // 默认命令超时
|
||||
}
|
||||
|
||||
// NewSSHExecuteTool 创建远程执行工具 // Logan.Li
|
||||
func NewSSHExecuteTool(cfg remoteexec.SSHConfig, timeout time.Duration) *SSHExecuteTool {
|
||||
// NewSSHExecuteTool 创建远程执行工具(内部建立 SSH 长连接) // Logan.Li
|
||||
func NewSSHExecuteTool(cfg remoteexec.SSHConfig, timeout time.Duration) (*SSHExecuteTool, error) {
|
||||
if timeout <= 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
return &SSHExecuteTool{cfg: cfg, timeout: timeout}
|
||||
client, err := remoteexec.NewPersistentClient(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SSH 长连接建立失败: %w", err)
|
||||
}
|
||||
return &SSHExecuteTool{client: client, timeout: timeout}, nil
|
||||
}
|
||||
|
||||
// Close 释放 SSH 长连接 // Logan.Li
|
||||
func (t *SSHExecuteTool) Close() error {
|
||||
if t.client == nil {
|
||||
return nil
|
||||
}
|
||||
return t.client.Close()
|
||||
}
|
||||
|
||||
// Name 工具名 // Logan.Li
|
||||
@@ -31,14 +44,9 @@ func (t *SSHExecuteTool) Name() string { return "ssh_execute" }
|
||||
|
||||
// Description 给 LLM 的工具说明 // Logan.Li
|
||||
func (t *SSHExecuteTool) Description() string {
|
||||
host := t.cfg.Host
|
||||
if host == "" {
|
||||
host = "目标主机"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"在远程服务器 %s(用户 %s)上执行 shell 命令并返回合并的 stdout+stderr 输出与退出码。"+
|
||||
"用于排障(查看日志/进程/磁盘/网络)、安装与配置软件、重启服务等。"+
|
||||
"高危命令(rm -rf /、mkfs、reboot 等)需用户确认。", host, t.cfg.User)
|
||||
return "在远程目标服务器上执行 shell 命令并返回合并的 stdout+stderr 输出与退出码。" +
|
||||
"用于排障(查看日志/进程/磁盘/网络)、安装与配置软件、重启服务等。" +
|
||||
"高危命令(rm -rf /、mkfs、reboot 等)需用户确认。"
|
||||
}
|
||||
|
||||
// Parameters JSON Schema 参数定义 // Logan.Li
|
||||
@@ -79,13 +87,11 @@ func (t *SSHExecuteTool) Execute(ctx context.Context, args map[string]interface{
|
||||
timeout = time.Duration(sec) * time.Second
|
||||
}
|
||||
|
||||
res, err := remoteexec.Exec(t.cfg, command, timeout)
|
||||
res, err := t.client.Exec(command, timeout)
|
||||
if err != nil {
|
||||
// 连接级失败(无法 SSH)
|
||||
return &Result{Success: false, Error: fmt.Sprintf("SSH 连接失败: %v", err)}, nil
|
||||
}
|
||||
|
||||
// 附带退出码,便于 agent 判断命令是否成功(排障关键信号)
|
||||
output := res.Output
|
||||
if !strings.HasSuffix(output, "\n") && output != "" {
|
||||
output += "\n"
|
||||
@@ -93,8 +99,8 @@ func (t *SSHExecuteTool) Execute(ctx context.Context, args map[string]interface{
|
||||
output += fmt.Sprintf("[exit_code=%d]", res.ExitCode)
|
||||
|
||||
return &Result{
|
||||
Success: res.Success, // exit_code==0
|
||||
Success: res.Success,
|
||||
Output: output,
|
||||
Error: res.Error,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user