agent-cli
This commit is contained in:
1 parent
e0d652d211
commit
0690285147
4 files changed
+159
-18
No files matched your search
@@ -37,6 +37,8 @@ func NewAgentCLIHandler(db *gorm.DB) *AgentCLIHandler { return &AgentCLIHandler{
|
|||||||
// wsEvent WebSocket 事件信封(后端→前端) // Logan.Li
|
// wsEvent WebSocket 事件信封(后端→前端) // Logan.Li
|
||||||
type wsEvent struct {
|
type wsEvent struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
Phase string `json:"phase,omitempty"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
Round int `json:"round,omitempty"`
|
Round int `json:"round,omitempty"`
|
||||||
SessionID uint `json:"session_id,omitempty"`
|
SessionID uint `json:"session_id,omitempty"`
|
||||||
Thought string `json:"thought,omitempty"`
|
Thought string `json:"thought,omitempty"`
|
||||||
@@ -46,6 +48,7 @@ type wsEvent struct {
|
|||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
Success bool `json:"success,omitempty"`
|
Success bool `json:"success,omitempty"`
|
||||||
Duration string `json:"duration,omitempty"`
|
Duration string `json:"duration,omitempty"`
|
||||||
|
OutputSize int `json:"output_size,omitempty"`
|
||||||
Tokens int `json:"tokens,omitempty"`
|
Tokens int `json:"tokens,omitempty"`
|
||||||
Answer string `json:"answer,omitempty"`
|
Answer string `json:"answer,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
@@ -456,9 +459,10 @@ func (ac *agentConn) onEvent(e core.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ev := wsEvent{
|
ev := wsEvent{
|
||||||
Type: string(e.Type), Round: e.Round, Thought: e.Thought,
|
Type: string(e.Type), Phase: e.Phase, Summary: e.Summary,
|
||||||
|
Round: e.Round, Thought: e.Thought,
|
||||||
ToolName: e.ToolName, ToolArgs: e.ToolArgs, Risk: e.Risk,
|
ToolName: e.ToolName, ToolArgs: e.ToolArgs, Risk: e.Risk,
|
||||||
Output: e.Output, Success: e.Success, Duration: e.Duration,
|
Output: e.Output, Success: e.Success, Duration: e.Duration, OutputSize: e.OutputSize,
|
||||||
Tokens: e.Tokens, Answer: e.Answer, Message: e.Message,
|
Tokens: e.Tokens, Answer: e.Answer, Message: e.Message,
|
||||||
}
|
}
|
||||||
if ac.session != nil {
|
if ac.session != nil {
|
||||||
@@ -472,6 +476,9 @@ func (ac *agentConn) onEvent(e core.Event) {
|
|||||||
}
|
}
|
||||||
sid := ac.session.ID
|
sid := ac.session.ID
|
||||||
switch e.Type {
|
switch e.Type {
|
||||||
|
case core.EventStatus:
|
||||||
|
// status 事件不改 DB schema:duration 字段在 type=status 时复用为 phase。
|
||||||
|
ac.persistMsg(sid, e.Round, "status", "system", e.Summary, e.ToolName, "", "", e.Risk, e.Phase != core.PhaseError && e.Phase != core.PhaseBlocked, e.Phase, e.Tokens)
|
||||||
case core.EventThought:
|
case core.EventThought:
|
||||||
ac.persistMsg(sid, e.Round, "thought", "assistant", e.Thought, "", "", "", "", false, "", 0)
|
ac.persistMsg(sid, e.Round, "thought", "assistant", e.Thought, "", "", "", "", false, "", 0)
|
||||||
case core.EventToolCall:
|
case core.EventToolCall:
|
||||||
@@ -503,7 +510,7 @@ func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, ris
|
|||||||
if cmd == "" && (toolName == "kubernetes_action" || toolName == "k8s_action") {
|
if cmd == "" && (toolName == "kubernetes_action" || toolName == "k8s_action") {
|
||||||
cmd = fmt.Sprintf("%v %v", args["action"], args["name"])
|
cmd = fmt.Sprintf("%v %v", args["action"], args["name"])
|
||||||
}
|
}
|
||||||
ac.write(wsEvent{Type: "confirm", ConfirmID: confirmID, ToolName: toolName, Command: cmd, Risk: risk.Display(), ToolArgs: args})
|
ac.write(wsEvent{Type: "confirm", Phase: core.PhaseApproval, Summary: "等待用户确认", ConfirmID: confirmID, ToolName: toolName, Command: cmd, Risk: risk.Display(), ToolArgs: args})
|
||||||
|
|
||||||
ch := make(chan bool, 1)
|
ch := make(chan bool, 1)
|
||||||
ac.cfMu.Lock()
|
ac.cfMu.Lock()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
type EventType string
|
type EventType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
EventStatus EventType = "status" // 生命周期状态(accepted/context/thinking/...)
|
||||||
EventThought EventType = "thought" // AI 思考/推理文本(流式结束后的完整内容)
|
EventThought EventType = "thought" // AI 思考/推理文本(流式结束后的完整内容)
|
||||||
EventThoughtDelta EventType = "thought_delta" // AI 思考流式增量 token
|
EventThoughtDelta EventType = "thought_delta" // AI 思考流式增量 token
|
||||||
EventToolCall EventType = "tool_call" // 即将执行的工具(名/参数/风险)
|
EventToolCall EventType = "tool_call" // 即将执行的工具(名/参数/风险)
|
||||||
@@ -32,9 +33,27 @@ const (
|
|||||||
EventError EventType = "error" // 错误(含达到最大轮次)
|
EventError EventType = "error" // 错误(含达到最大轮次)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PhaseAccepted = "accepted"
|
||||||
|
PhaseContext = "context"
|
||||||
|
PhaseThinking = "thinking"
|
||||||
|
PhasePlanning = "planning"
|
||||||
|
PhaseSafety = "safety_check"
|
||||||
|
PhaseApproval = "approval_required"
|
||||||
|
PhaseToolRunning = "tool_running"
|
||||||
|
PhaseObserving = "observing"
|
||||||
|
PhaseValidating = "validating"
|
||||||
|
PhaseSummarizing = "summarizing"
|
||||||
|
PhaseDone = "done"
|
||||||
|
PhaseBlocked = "blocked"
|
||||||
|
PhaseError = "error"
|
||||||
|
)
|
||||||
|
|
||||||
// Event 引擎产生的事件(controller 序列化后通过 WebSocket 推送) // Logan.Li
|
// Event 引擎产生的事件(controller 序列化后通过 WebSocket 推送) // Logan.Li
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Type EventType `json:"type"`
|
Type EventType `json:"type"`
|
||||||
|
Phase string `json:"phase,omitempty"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
Round int `json:"round,omitempty"`
|
Round int `json:"round,omitempty"`
|
||||||
Thought string `json:"thought,omitempty"`
|
Thought string `json:"thought,omitempty"`
|
||||||
ToolName string `json:"tool_name,omitempty"`
|
ToolName string `json:"tool_name,omitempty"`
|
||||||
@@ -43,6 +62,7 @@ type Event struct {
|
|||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
Success bool `json:"success,omitempty"`
|
Success bool `json:"success,omitempty"`
|
||||||
Duration string `json:"duration,omitempty"`
|
Duration string `json:"duration,omitempty"`
|
||||||
|
OutputSize int `json:"output_size,omitempty"`
|
||||||
Tokens int `json:"tokens,omitempty"`
|
Tokens int `json:"tokens,omitempty"`
|
||||||
Answer string `json:"answer,omitempty"`
|
Answer string `json:"answer,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
@@ -127,6 +147,7 @@ func (a *Agent) emit(e Event) {
|
|||||||
func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.Message, priorTokens int) (*RunResult, error) {
|
func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.Message, priorTokens int) (*RunResult, error) {
|
||||||
a.totalTokens = priorTokens
|
a.totalTokens = priorTokens
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
a.emit(Event{Type: EventStatus, Phase: PhaseAccepted, Summary: "任务已接收"})
|
||||||
|
|
||||||
// Evolver PREDICT:获取历史经验注入 system prompt
|
// Evolver PREDICT:获取历史经验注入 system prompt
|
||||||
evolveCtx := ""
|
evolveCtx := ""
|
||||||
@@ -137,6 +158,7 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.M
|
|||||||
// 工具描述
|
// 工具描述
|
||||||
toolDescs := a.buildToolDescriptions()
|
toolDescs := a.buildToolDescriptions()
|
||||||
systemPrompt := prompt.BuildSystemPrompt(a.config.Mode, serverInfo, toolDescs, evolveCtx)
|
systemPrompt := prompt.BuildSystemPrompt(a.config.Mode, serverInfo, toolDescs, evolveCtx)
|
||||||
|
a.emit(Event{Type: EventStatus, Phase: PhaseContext, Summary: fmt.Sprintf("mode=%s tools=%d prior_messages=%d", a.config.Mode, len(a.registry.List()), len(prior))})
|
||||||
|
|
||||||
messages := []llm.Message{{Role: "system", Content: systemPrompt}}
|
messages := []llm.Message{{Role: "system", Content: systemPrompt}}
|
||||||
if len(prior) > 0 {
|
if len(prior) > 0 {
|
||||||
@@ -168,6 +190,7 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.M
|
|||||||
|
|
||||||
// ReAct 须用 Complete:Stream 无法组装 tool_calls/tool_use(GLM/Claude 等会只回文字不执行) // Logan.Li
|
// ReAct 须用 Complete:Stream 无法组装 tool_calls/tool_use(GLM/Claude 等会只回文字不执行) // Logan.Li
|
||||||
round := a.roundNum(iter)
|
round := a.roundNum(iter)
|
||||||
|
a.emit(Event{Type: EventStatus, Phase: PhaseThinking, Round: round, Summary: "LLM 推理中"})
|
||||||
resp, err := a.llm.Complete(ctx, &llm.CompletionRequest{
|
resp, err := a.llm.Complete(ctx, &llm.CompletionRequest{
|
||||||
Messages: trimmed,
|
Messages: trimmed,
|
||||||
Tools: llmTools,
|
Tools: llmTools,
|
||||||
@@ -175,7 +198,7 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.M
|
|||||||
MaxTokens: a.config.MaxTokens,
|
MaxTokens: a.config.MaxTokens,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.emit(Event{Type: EventError, Message: "LLM 调用失败: " + err.Error()})
|
a.emit(Event{Type: EventError, Phase: PhaseError, Message: "LLM 调用失败: " + err.Error()})
|
||||||
return nil, fmt.Errorf("LLM调用失败: %w", err)
|
return nil, fmt.Errorf("LLM调用失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +231,8 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.M
|
|||||||
answer = "任务已完成。"
|
answer = "任务已完成。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.emit(Event{Type: EventDone, Answer: answer, Tokens: a.totalTokens})
|
a.emit(Event{Type: EventStatus, Phase: PhaseSummarizing, Round: round, Summary: "汇总最终答案"})
|
||||||
|
a.emit(Event{Type: EventDone, Phase: PhaseDone, Answer: answer, Tokens: a.totalTokens})
|
||||||
a.triggerEvolve(ctx, query, serverInfo, answer, toolRecords, true, start)
|
a.triggerEvolve(ctx, query, serverInfo, answer, toolRecords, true, start)
|
||||||
return &RunResult{
|
return &RunResult{
|
||||||
Success: true, Output: answer,
|
Success: true, Output: answer,
|
||||||
@@ -218,7 +242,7 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.M
|
|||||||
|
|
||||||
// 中间轮思考(后续还有 tool_calls,与终局 done 分开,避免重复输出) // Logan.Li
|
// 中间轮思考(后续还有 tool_calls,与终局 done 分开,避免重复输出) // Logan.Li
|
||||||
if thought := strings.TrimSpace(resp.Message.Content); thought != "" {
|
if thought := strings.TrimSpace(resp.Message.Content); thought != "" {
|
||||||
a.emit(Event{Type: EventThought, Round: round, Thought: thought})
|
a.emit(Event{Type: EventThought, Phase: PhasePlanning, Round: round, Thought: thought})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理工具调用
|
// 处理工具调用
|
||||||
@@ -231,7 +255,7 @@ func (a *Agent) Run(ctx context.Context, query, serverInfo string, prior []llm.M
|
|||||||
|
|
||||||
// 达到最大轮次
|
// 达到最大轮次
|
||||||
msg := fmt.Sprintf("达到最大迭代次数(%d),任务未能完成。可能任务过于复杂或需要更多步骤。", a.config.MaxIterations)
|
msg := fmt.Sprintf("达到最大迭代次数(%d),任务未能完成。可能任务过于复杂或需要更多步骤。", a.config.MaxIterations)
|
||||||
a.emit(Event{Type: EventError, Message: msg, Tokens: a.totalTokens})
|
a.emit(Event{Type: EventError, Phase: PhaseError, Message: msg, Tokens: a.totalTokens})
|
||||||
a.triggerEvolve(ctx, query, serverInfo, "", toolRecords, false, start)
|
a.triggerEvolve(ctx, query, serverInfo, "", toolRecords, false, start)
|
||||||
return &RunResult{
|
return &RunResult{
|
||||||
Success: false, History: a.extractHistory(messages), TotalTokens: a.totalTokens,
|
Success: false, History: a.extractHistory(messages), TotalTokens: a.totalTokens,
|
||||||
@@ -270,11 +294,12 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
|||||||
}
|
}
|
||||||
|
|
||||||
risk := a.toolRisk(tool, args)
|
risk := a.toolRisk(tool, args)
|
||||||
a.emit(Event{Type: EventToolCall, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display()})
|
a.emit(Event{Type: EventStatus, Phase: PhaseSafety, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "安全检查"})
|
||||||
|
|
||||||
// 安全检查(高危触发 confirmFn,由 controller 异步确认)
|
// 安全检查(高危触发 confirmFn,由 controller 异步确认)
|
||||||
approved, err := a.safetyCtl.Check(tool, args)
|
approved, err := a.safetyCtl.Check(tool, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
a.emit(Event{Type: EventStatus, Phase: PhaseError, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Message: err.Error()})
|
||||||
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
|
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
|
||||||
Content: "安全检查失败: " + err.Error()})
|
Content: "安全检查失败: " + err.Error()})
|
||||||
continue
|
continue
|
||||||
@@ -282,12 +307,14 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
|||||||
if !approved {
|
if !approved {
|
||||||
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
|
obs = append(obs, llm.Message{Role: "tool", ToolCallID: tc.ID, Name: tc.Function.Name,
|
||||||
Content: "操作已被用户拒绝执行"})
|
Content: "操作已被用户拒绝执行"})
|
||||||
|
a.emit(Event{Type: EventStatus, Phase: PhaseBlocked, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "用户拒绝执行"})
|
||||||
a.emit(Event{Type: EventToolResult, Round: round, ToolName: tc.Function.Name,
|
a.emit(Event{Type: EventToolResult, Round: round, ToolName: tc.Function.Name,
|
||||||
Output: "(用户拒绝执行)", Success: false})
|
Phase: PhaseObserving, Output: "(用户拒绝执行)", Success: false, OutputSize: len("(用户拒绝执行)")})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行
|
// 执行
|
||||||
|
a.emit(Event{Type: EventToolCall, Phase: PhaseToolRunning, Round: round, ToolName: tc.Function.Name, ToolArgs: args, Risk: risk.Display(), Summary: "工具执行中"})
|
||||||
toolStart := time.Now()
|
toolStart := time.Now()
|
||||||
toolCtx, cancel := context.WithTimeout(ctx, a.config.ToolTimeout)
|
toolCtx, cancel := context.WithTimeout(ctx, a.config.ToolTimeout)
|
||||||
result, execErr := tool.Execute(toolCtx, args)
|
result, execErr := tool.Execute(toolCtx, args)
|
||||||
@@ -317,8 +344,8 @@ func (a *Agent) processToolCalls(ctx context.Context, calls []llm.ToolCall, hist
|
|||||||
fmt.Sprintf("\n[输出已截断,原始 %d 字符]", len(output))
|
fmt.Sprintf("\n[输出已截断,原始 %d 字符]", len(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
a.emit(Event{Type: EventToolResult, Round: round, ToolName: tc.Function.Name,
|
a.emit(Event{Type: EventToolResult, Phase: PhaseObserving, Round: round, ToolName: tc.Function.Name,
|
||||||
Output: output, Success: success, Duration: dur.Truncate(time.Millisecond).String()})
|
Output: output, Success: success, Duration: dur.Truncate(time.Millisecond).String(), OutputSize: len(output)})
|
||||||
|
|
||||||
records = append(records, evolver.ToolCallRecord{
|
records = append(records, evolver.ToolCallRecord{
|
||||||
ToolName: tc.Function.Name, Args: args, Output: output,
|
ToolName: tc.Function.Name, Args: args, Output: output,
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import { PageShell } from '@/components/layout/PageShell';
|
|||||||
// WS 事件类型(后端 wsEvent 对应) // Logan.Li
|
// WS 事件类型(后端 wsEvent 对应) // Logan.Li
|
||||||
interface WsEvent {
|
interface WsEvent {
|
||||||
type: string;
|
type: string;
|
||||||
|
phase?: string;
|
||||||
|
summary?: string;
|
||||||
round?: number;
|
round?: number;
|
||||||
session_id?: number;
|
session_id?: number;
|
||||||
thought?: string;
|
thought?: string;
|
||||||
@@ -36,6 +38,7 @@ interface WsEvent {
|
|||||||
output?: string;
|
output?: string;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
duration?: string;
|
duration?: string;
|
||||||
|
output_size?: number;
|
||||||
tokens?: number;
|
tokens?: number;
|
||||||
answer?: string;
|
answer?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -99,6 +102,27 @@ function formatServerLabel(s: { hostname: string; ip: string }) {
|
|||||||
return `${s.hostname}(${s.ip})`;
|
return `${s.hostname}(${s.ip})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAgentPhase(phase: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
idle: '空闲',
|
||||||
|
accepted: '已接收',
|
||||||
|
context: '加载上下文',
|
||||||
|
thinking: '思考中',
|
||||||
|
planning: '规划中',
|
||||||
|
safety_check: '安全检查',
|
||||||
|
approval_required: '等待审批',
|
||||||
|
tool_running: '执行工具',
|
||||||
|
observing: '观察结果',
|
||||||
|
validating: '验证中',
|
||||||
|
summarizing: '总结中',
|
||||||
|
done: '已完成',
|
||||||
|
blocked: '已阻断',
|
||||||
|
error: '失败',
|
||||||
|
aborted: '已中断',
|
||||||
|
};
|
||||||
|
return map[phase] || phase || '运行中';
|
||||||
|
}
|
||||||
|
|
||||||
export default function AgentCLI() {
|
export default function AgentCLI() {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
@@ -116,6 +140,7 @@ export default function AgentCLI() {
|
|||||||
const [items, setItems] = useState<ChatItem[]>([]);
|
const [items, setItems] = useState<ChatItem[]>([]);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
|
const [phase, setPhase] = useState('idle');
|
||||||
const [round, setRound] = useState(0);
|
const [round, setRound] = useState(0);
|
||||||
const [tokens, setTokens] = useState(0);
|
const [tokens, setTokens] = useState(0);
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
@@ -209,7 +234,7 @@ export default function AgentCLI() {
|
|||||||
if (cliMode === 'host' && !serverId) return;
|
if (cliMode === 'host' && !serverId) return;
|
||||||
if (cliMode === 'kubernetes' && (!clusterCode || !namespace)) return;
|
if (cliMode === 'kubernetes' && (!clusterCode || !namespace)) return;
|
||||||
setItems([]);
|
setItems([]);
|
||||||
setRound(0); setTokens(0); setRunning(false);
|
setRound(0); setTokens(0); setRunning(false); setPhase('idle');
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setViewMode(false);
|
setViewMode(false);
|
||||||
setViewSession(null);
|
setViewSession(null);
|
||||||
@@ -242,6 +267,7 @@ export default function AgentCLI() {
|
|||||||
window.clearTimeout(connectTimer);
|
window.clearTimeout(connectTimer);
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
|
setPhase('idle');
|
||||||
if (!everConnected) {
|
if (!everConnected) {
|
||||||
push({ kind: 'error', content: cliMode === 'kubernetes'
|
push({ kind: 'error', content: cliMode === 'kubernetes'
|
||||||
? '集群连接被拒绝:请检查集群 kubeconfig/token 配置与大模型是否启用。'
|
? '集群连接被拒绝:请检查集群 kubeconfig/token 配置与大模型是否启用。'
|
||||||
@@ -310,11 +336,25 @@ export default function AgentCLI() {
|
|||||||
setCanContinue(true);
|
setCanContinue(true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'status':
|
||||||
|
if (ev.phase) setPhase(ev.phase);
|
||||||
|
if (ev.round) setRound(ev.round);
|
||||||
|
if (ev.tokens) setTokens(ev.tokens);
|
||||||
|
push({
|
||||||
|
kind: 'status',
|
||||||
|
round: ev.round,
|
||||||
|
phase: ev.phase,
|
||||||
|
content: ev.summary || ev.message || ev.phase || '',
|
||||||
|
toolName: ev.tool_name,
|
||||||
|
risk: ev.risk,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'progress':
|
case 'progress':
|
||||||
if (ev.round) setRound(ev.round);
|
if (ev.round) setRound(ev.round);
|
||||||
if (ev.tokens) setTokens(ev.tokens);
|
if (ev.tokens) setTokens(ev.tokens);
|
||||||
break;
|
break;
|
||||||
case 'thought_delta':
|
case 'thought_delta':
|
||||||
|
setPhase('thinking');
|
||||||
setItems((prev) => {
|
setItems((prev) => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
const last = next[next.length - 1];
|
const last = next[next.length - 1];
|
||||||
@@ -327,6 +367,7 @@ export default function AgentCLI() {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'thought':
|
case 'thought':
|
||||||
|
setPhase('planning');
|
||||||
setItems((prev) => {
|
setItems((prev) => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
const last = next[next.length - 1];
|
const last = next[next.length - 1];
|
||||||
@@ -339,6 +380,7 @@ export default function AgentCLI() {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'tool_call': {
|
case 'tool_call': {
|
||||||
|
setPhase(ev.phase || 'tool_running');
|
||||||
const args = ev.tool_args || {};
|
const args = ev.tool_args || {};
|
||||||
push({
|
push({
|
||||||
kind: 'tool', round: ev.round, toolName: ev.tool_name,
|
kind: 'tool', round: ev.round, toolName: ev.tool_name,
|
||||||
@@ -347,13 +389,14 @@ export default function AgentCLI() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'tool_result':
|
case 'tool_result':
|
||||||
|
setPhase(ev.phase || 'observing');
|
||||||
setItems((prev) => {
|
setItems((prev) => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
for (let i = next.length - 1; i >= 0; i--) {
|
for (let i = next.length - 1; i >= 0; i--) {
|
||||||
if (next[i].kind === 'tool' && next[i].pending && next[i].toolName === ev.tool_name) {
|
if (next[i].kind === 'tool' && next[i].pending && next[i].toolName === ev.tool_name) {
|
||||||
next[i] = {
|
next[i] = {
|
||||||
...next[i], output: ev.output, success: ev.success,
|
...next[i], output: ev.output, success: ev.success,
|
||||||
duration: ev.duration, pending: false,
|
duration: ev.duration, outputSize: ev.output_size, pending: false,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -362,9 +405,11 @@ export default function AgentCLI() {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'confirm':
|
case 'confirm':
|
||||||
|
setPhase(ev.phase || 'approval_required');
|
||||||
handleConfirm(ev);
|
handleConfirm(ev);
|
||||||
break;
|
break;
|
||||||
case 'done': {
|
case 'done': {
|
||||||
|
setPhase('done');
|
||||||
const text = (ev.answer || ev.message || '').trim();
|
const text = (ev.answer || ev.message || '').trim();
|
||||||
setItems((prev) => {
|
setItems((prev) => {
|
||||||
const norm = (s?: string) => (s || '').trim().replace(/\s+/g, ' ');
|
const norm = (s?: string) => (s || '').trim().replace(/\s+/g, ' ');
|
||||||
@@ -394,6 +439,7 @@ export default function AgentCLI() {
|
|||||||
}
|
}
|
||||||
case 'error':
|
case 'error':
|
||||||
case 'aborted':
|
case 'aborted':
|
||||||
|
setPhase(type === 'aborted' ? 'aborted' : 'error');
|
||||||
push({ kind: type === 'aborted' ? 'info' : 'error', content: ev.message || (type === 'aborted' ? '任务已中断' : '发生错误') });
|
push({ kind: type === 'aborted' ? 'info' : 'error', content: ev.message || (type === 'aborted' ? '任务已中断' : '发生错误') });
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
setCanContinue(type !== 'aborted');
|
setCanContinue(type !== 'aborted');
|
||||||
@@ -433,6 +479,7 @@ export default function AgentCLI() {
|
|||||||
wsRef.current.send(JSON.stringify({ type: msgType, query: q, safety_mode: safetyMode }));
|
wsRef.current.send(JSON.stringify({ type: msgType, query: q, safety_mode: safetyMode }));
|
||||||
setInput('');
|
setInput('');
|
||||||
setRunning(true);
|
setRunning(true);
|
||||||
|
setPhase('accepted');
|
||||||
if (msgType === 'start') setCanContinue(false);
|
if (msgType === 'start') setCanContinue(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -451,6 +498,7 @@ export default function AgentCLI() {
|
|||||||
...replayItems,
|
...replayItems,
|
||||||
]);
|
]);
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
|
setPhase('idle');
|
||||||
};
|
};
|
||||||
|
|
||||||
// resumeHistorySession 恢复历史会话上下文,进入可追问状态 // Logan.Li
|
// resumeHistorySession 恢复历史会话上下文,进入可追问状态 // Logan.Li
|
||||||
@@ -471,6 +519,7 @@ export default function AgentCLI() {
|
|||||||
setItems([]);
|
setItems([]);
|
||||||
setRound(0);
|
setRound(0);
|
||||||
setTokens(0);
|
setTokens(0);
|
||||||
|
setPhase('idle');
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedServer = servers.find((s) => String(s.id) === serverId);
|
const selectedServer = servers.find((s) => String(s.id) === serverId);
|
||||||
@@ -658,7 +707,7 @@ export default function AgentCLI() {
|
|||||||
)}
|
)}
|
||||||
{running && (
|
{running && (
|
||||||
<Badge className="h-6 text-[10px] bg-blue-500/15 dg-text-blue border-blue-500/30">
|
<Badge className="h-6 text-[10px] bg-blue-500/15 dg-text-blue border-blue-500/30">
|
||||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />第 {round} 轮 · {tokens}t
|
<Loader2 className="w-3 h-3 mr-1 animate-spin" />{formatAgentPhase(phase)} · 第 {round} 轮 · {tokens}t
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import i18n from '@/i18n';
|
|||||||
// 对话流条目模型 // Logan.Li
|
// 对话流条目模型 // Logan.Li
|
||||||
export interface ChatItem {
|
export interface ChatItem {
|
||||||
id: number;
|
id: number;
|
||||||
kind: 'user' | 'thought' | 'tool' | 'done' | 'error' | 'info';
|
kind: 'user' | 'thought' | 'tool' | 'done' | 'error' | 'info' | 'status';
|
||||||
|
phase?: string;
|
||||||
round?: number;
|
round?: number;
|
||||||
content?: string;
|
content?: string;
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
@@ -21,6 +22,7 @@ export interface ChatItem {
|
|||||||
output?: string;
|
output?: string;
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
duration?: string;
|
duration?: string;
|
||||||
|
outputSize?: number;
|
||||||
pending?: boolean;
|
pending?: boolean;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
}
|
}
|
||||||
@@ -51,6 +53,9 @@ export function messagesToChatItems(msgs: AgentCliMessage[]): ChatItem[] {
|
|||||||
case 'thought':
|
case 'thought':
|
||||||
items.push({ id: nextId(), kind: 'thought', round: m.round, content: m.content });
|
items.push({ id: nextId(), kind: 'thought', round: m.round, content: m.content });
|
||||||
break;
|
break;
|
||||||
|
case 'status':
|
||||||
|
items.push({ id: nextId(), kind: 'status', round: m.round, phase: m.duration || undefined, content: m.content, toolName: m.tool_name, risk: m.risk });
|
||||||
|
break;
|
||||||
case 'tool_call': {
|
case 'tool_call': {
|
||||||
let command = '';
|
let command = '';
|
||||||
try {
|
try {
|
||||||
@@ -124,12 +129,12 @@ function groupChatItems(items: ChatItem[]): TimelineSegment[] {
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
while (i < items.length) {
|
while (i < items.length) {
|
||||||
const it = items[i];
|
const it = items[i];
|
||||||
if (it.round != null && (it.kind === 'thought' || it.kind === 'tool')) {
|
if (it.round != null && (it.kind === 'thought' || it.kind === 'tool' || it.kind === 'status')) {
|
||||||
const r = it.round;
|
const r = it.round;
|
||||||
const group: ChatItem[] = [];
|
const group: ChatItem[] = [];
|
||||||
while (i < items.length) {
|
while (i < items.length) {
|
||||||
const cur = items[i];
|
const cur = items[i];
|
||||||
if (cur.round !== r || (cur.kind !== 'thought' && cur.kind !== 'tool')) break;
|
if (cur.round !== r || (cur.kind !== 'thought' && cur.kind !== 'tool' && cur.kind !== 'status')) break;
|
||||||
group.push(cur);
|
group.push(cur);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@@ -234,6 +239,9 @@ export function MessageView({ item, inRoundGroup = false }: { item: ChatItem; in
|
|||||||
if (item.kind === 'tool') {
|
if (item.kind === 'tool') {
|
||||||
return <ToolBlock item={item} inRoundGroup={inRoundGroup} />;
|
return <ToolBlock item={item} inRoundGroup={inRoundGroup} />;
|
||||||
}
|
}
|
||||||
|
if (item.kind === 'status') {
|
||||||
|
return <StatusLine item={item} />;
|
||||||
|
}
|
||||||
if (item.kind === 'done') {
|
if (item.kind === 'done') {
|
||||||
const text = item.content?.trim() || i18n.t('aiops.taskDone');
|
const text = item.content?.trim() || i18n.t('aiops.taskDone');
|
||||||
return (
|
return (
|
||||||
@@ -257,6 +265,44 @@ export function MessageView({ item, inRoundGroup = false }: { item: ChatItem; in
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function phaseLabel(phase?: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
accepted: '已接收',
|
||||||
|
context: '上下文',
|
||||||
|
thinking: '思考中',
|
||||||
|
planning: '规划',
|
||||||
|
safety_check: '安全检查',
|
||||||
|
approval_required: '等待审批',
|
||||||
|
tool_running: '工具执行中',
|
||||||
|
observing: '观察结果',
|
||||||
|
validating: '验证',
|
||||||
|
summarizing: '总结',
|
||||||
|
done: '完成',
|
||||||
|
blocked: '已阻断',
|
||||||
|
error: '失败',
|
||||||
|
};
|
||||||
|
return map[phase || ''] || phase || '状态';
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusLine({ item }: { item: ChatItem }) {
|
||||||
|
const danger = item.phase === 'blocked' || item.phase === 'error';
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs ${danger ? 'border-red-500/25 bg-red-500/10' : 'border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/35'}`}>
|
||||||
|
{item.phase === 'thinking' || item.phase === 'tool_running' ? (
|
||||||
|
<Loader2 className="w-3.5 h-3.5 dg-text-blue animate-spin shrink-0" />
|
||||||
|
) : danger ? (
|
||||||
|
<AlertTriangle className="w-3.5 h-3.5 dg-text-red shrink-0" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5 dg-text-secondary shrink-0" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium text-[var(--dg-text-primary)]">{phaseLabel(item.phase)}</span>
|
||||||
|
{item.toolName && <span className="dg-text-muted">tool={item.toolName}</span>}
|
||||||
|
{item.content && <span className="dg-text-muted truncate">{item.content}</span>}
|
||||||
|
{item.risk && <Badge className={`ml-auto text-[10px] border ${riskClass(item.risk)}`}>{item.risk}</Badge>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ThoughtBlock AI 思考(可折叠,灰底斜体,支持流式) // Logan.Li
|
// ThoughtBlock AI 思考(可折叠,灰底斜体,支持流式) // Logan.Li
|
||||||
function ThoughtBlock({
|
function ThoughtBlock({
|
||||||
text, round, streaming, hideRoundLabel,
|
text, round, streaming, hideRoundLabel,
|
||||||
@@ -305,7 +351,13 @@ function ToolBlock({ item, inRoundGroup }: { item: ChatItem; inRoundGroup?: bool
|
|||||||
)}
|
)}
|
||||||
{!item.pending && item.output != null && (
|
{!item.pending && item.output != null && (
|
||||||
<>
|
<>
|
||||||
{item.duration && <div className="text-[10px] dg-text-muted">{i18n.t('aiops.durationLabel', { duration: item.duration })}</div>}
|
{(item.duration || item.outputSize != null) && (
|
||||||
|
<div className="text-[10px] dg-text-muted">
|
||||||
|
{item.duration && i18n.t('aiops.durationLabel', { duration: item.duration })}
|
||||||
|
{item.duration && item.outputSize != null ? ' · ' : ''}
|
||||||
|
{item.outputSize != null ? `输出 ${formatBytes(item.outputSize)}` : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{(item.toolName === 'analyze_output' || looksLikeMarkdown(item.output)) ? (
|
{(item.toolName === 'analyze_output' || looksLikeMarkdown(item.output)) ? (
|
||||||
<div className="bg-[var(--dg-bg-primary)]/40 rounded px-2 py-1.5 max-h-80 overflow-y-auto">
|
<div className="bg-[var(--dg-bg-primary)]/40 rounded px-2 py-1.5 max-h-80 overflow-y-auto">
|
||||||
<AgentMarkdown content={item.output || i18n.t('aiops.agentclimessage.s216caee5')} />
|
<AgentMarkdown content={item.output || i18n.t('aiops.agentclimessage.s216caee5')} />
|
||||||
@@ -320,3 +372,9 @@ function ToolBlock({ item, inRoundGroup }: { item: ChatItem; inRoundGroup?: bool
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytes(n: number) {
|
||||||
|
if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}MB`;
|
||||||
|
if (n >= 1024) return `${(n / 1024).toFixed(1)}KB`;
|
||||||
|
return `${n}B`;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user