From f98f4d68b9634a27138fb4195eac36a74716bed1 Mon Sep 17 00:00:00 2001 From: matevip Date: Sat, 11 Apr 2026 14:25:37 +0800 Subject: [PATCH] refactor(ui): publish tested chat UI simplification --- TECHNOLOGY_OVERVIEW.md | 0 mateclaw-server/hello.txt | 1 + .../vip/mate/agent/AgentGraphBuilder.java | 2 +- .../graph/plan/node/DirectAnswerNode.java | 10 + .../graph/plan/node/PlanGenerationNode.java | 29 +- mateclaw-ui/TEST_CASES.md | 187 ++++++++++ mateclaw-ui/src/assets/main.css | 10 + .../src/components/chat/BrowserTimeline.vue | 2 +- mateclaw-ui/src/components/chat/ChatInput.vue | 20 +- .../src/components/chat/MessageBubble.vue | 336 +++--------------- .../src/components/chat/PlanStepsPanel.vue | 266 ++++++++++++++ .../src/components/chat/StreamLoadingBar.vue | 190 +++------- project-summary.md | 0 13 files changed, 618 insertions(+), 435 deletions(-) create mode 100644 TECHNOLOGY_OVERVIEW.md create mode 100644 mateclaw-server/hello.txt create mode 100644 mateclaw-ui/TEST_CASES.md create mode 100644 mateclaw-ui/src/components/chat/PlanStepsPanel.vue create mode 100644 project-summary.md diff --git a/TECHNOLOGY_OVERVIEW.md b/TECHNOLOGY_OVERVIEW.md new file mode 100644 index 00000000..e69de29b diff --git a/mateclaw-server/hello.txt b/mateclaw-server/hello.txt new file mode 100644 index 00000000..557db03d --- /dev/null +++ b/mateclaw-server/hello.txt @@ -0,0 +1 @@ +Hello World diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index 20f9ba28..b16d531e 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -233,7 +233,7 @@ public class AgentGraphBuilder { ChatModel fallbackModel = buildFallbackModel(chatModel); NodeStreamingChatHelper streamingHelper = new NodeStreamingChatHelper(streamTracker, fallbackModel); ToolExecutionExecutor executor = new ToolExecutionExecutor(toolSet, toolGuardService, approvalService, streamTracker, toolTimeoutProperties); - PlanGenerationNode planGenerationNode = new PlanGenerationNode(chatModel, planningService, streamingHelper, conversationWindowManager); + PlanGenerationNode planGenerationNode = new PlanGenerationNode(chatModel, planningService, streamingHelper, conversationWindowManager, toolSet); StepExecutionNode stepExecutionNode = new StepExecutionNode(chatModel, toolSet, executor, planningService, streamTracker, reasoningEffort, streamingHelper, conversationWindowManager); PlanSummaryNode planSummaryNode = new PlanSummaryNode(chatModel, planningService, streamingHelper); DirectAnswerNode directAnswerNode = new DirectAnswerNode(); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/DirectAnswerNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/DirectAnswerNode.java index 6a198959..550cbf3c 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/DirectAnswerNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/DirectAnswerNode.java @@ -3,6 +3,7 @@ package vip.mate.agent.graph.plan.node; import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.action.NodeAction; import vip.mate.agent.graph.plan.state.PlanStateKeys; +import vip.mate.agent.graph.state.MateClawStateKeys; import java.util.Map; @@ -11,6 +12,10 @@ import java.util.Map; *

* 当 PlanGenerationNode 判定用户消息是简单问答时, * 将 direct_answer 透传为 final_summary,直接结束图执行。 + *

+ * 如果 PlanGenerationNode 已通过 broadcastContent() 推送了内容 + * (contentStreamed=true),则不再复制到 FINAL_SUMMARY, + * 避免 StreamAccumulator 重复收集导致持久化内容翻倍。 * * @author MateClaw Team */ @@ -18,6 +23,11 @@ public class DirectAnswerNode implements NodeAction { @Override public Map apply(OverAllState state) { + boolean alreadyStreamed = state.value(MateClawStateKeys.CONTENT_STREAMED, false); + if (alreadyStreamed) { + // broadcastContent 已推送并被 accumulator 收集,不重复写入 FINAL_SUMMARY + return Map.of(); + } String directAnswer = state.value(PlanStateKeys.DIRECT_ANSWER, ""); return Map.of(PlanStateKeys.FINAL_SUMMARY, directAnswer); } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java index 848ce088..e4f2ac73 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java @@ -10,6 +10,7 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.Prompt; +import vip.mate.agent.AgentToolSet; import vip.mate.agent.GraphEventPublisher; import vip.mate.agent.graph.NodeStreamingChatHelper; import vip.mate.agent.graph.plan.state.PlanStateAccessor; @@ -22,6 +23,7 @@ import vip.mate.planning.service.PlanningService; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * 计划生成节点 @@ -47,6 +49,7 @@ public class PlanGenerationNode implements NodeAction { private final PlanningService planningService; private final NodeStreamingChatHelper streamingHelper; private final ConversationWindowManager conversationWindowManager; + private final AgentToolSet toolSet; private final ObjectMapper objectMapper = new ObjectMapper(); private static final String PLANNING_PROMPT = """ @@ -71,25 +74,27 @@ public class PlanGenerationNode implements NodeAction { - 每个步骤必须是可执行动作,不要写空话。 - 默认不要把 MEMORY.md、PROFILE.md、记忆文件当成独立步骤;但如果用户目标明显依赖历史偏好、长期约束、过往决策或持续上下文,可以加入必要的记忆读取步骤。 - 不要把技能文件当成独立步骤,除非用户任务明确要求。 - - 如果用户目标包含执行、修改、搜索、分析、生成文件、调用工具等多步行为,优先返回规划。 + - 如果用户目标需要调用任何工具才能完成(包括记忆读写、文件操作、搜索、命令执行等),必须返回 needs_planning: true。只有纯知识问答(不需要调用任何工具的简单问题)才返回 needs_planning: false。 - 如果无法确定,也必须返回合法 JSON,不能输出自然语言。 """; public PlanGenerationNode(ChatModel chatModel, PlanningService planningService, NodeStreamingChatHelper streamingHelper, - ConversationWindowManager conversationWindowManager) { + ConversationWindowManager conversationWindowManager, + AgentToolSet toolSet) { this.chatModel = chatModel; this.planningService = planningService; this.streamingHelper = streamingHelper; this.conversationWindowManager = conversationWindowManager; + this.toolSet = toolSet; } /** - * @deprecated Use constructor with NodeStreamingChatHelper + * @deprecated Use constructor with full parameters */ @Deprecated public PlanGenerationNode(ChatModel chatModel, PlanningService planningService) { - this(chatModel, planningService, null, null); + this(chatModel, planningService, null, null, null); } @Override @@ -123,12 +128,24 @@ public class PlanGenerationNode implements NodeAction { } try { - // 构建 prompt 消息列表:system + 历史上下文 + 当前规划请求 + // 构建 prompt 消息列表:PLANNING_PROMPT 作为独立 system message, + // 不拼接完整 systemPrompt(wiki/技能/记忆指南等与规划决策无关, + // 拼接后会稀释 PLANNING_PROMPT 的指令优先级) List promptMessages = new ArrayList<>(); - promptMessages.add(new SystemMessage(systemPrompt + "\n\n" + PLANNING_PROMPT)); + promptMessages.add(new SystemMessage(PLANNING_PROMPT)); // 注入运行时上下文(当前时间) promptMessages.add(new UserMessage(RuntimeContextInjector.buildContextMessage())); + // 注入可用工具名称,帮助 LLM 判断用户目标是否需要工具 + if (toolSet != null && !toolSet.callbacks().isEmpty()) { + String toolNames = toolSet.callbacks().stream() + .map(cb -> cb.getToolDefinition().name()) + .collect(Collectors.joining(", ")); + promptMessages.add(new UserMessage( + "你可以使用以下工具:" + toolNames + + "\n如果用户目标需要调用任何工具才能完成,必须返回 needs_planning: true。")); + } + // 注入 working context(对话历史摘要),让规划能感知之前对话的约束和补充条件 String workingContext = accessor.workingContext(); if (!workingContext.isEmpty()) { diff --git a/mateclaw-ui/TEST_CASES.md b/mateclaw-ui/TEST_CASES.md new file mode 100644 index 00000000..3151ac21 --- /dev/null +++ b/mateclaw-ui/TEST_CASES.md @@ -0,0 +1,187 @@ +# UI 精简改动 - 验证测试用例 + +> 基于 MateClaw 内置的 3 个 Agent、19 个工具、31 条 Guard 规则和审批工作流设计。 +> 默认登录:admin / admin123 + +--- + +## 前置条件 + +1. 后端启动:`cd mateclaw-server && mvn spring-boot:run`(需设置 `DASHSCOPE_API_KEY`) +2. 前端启动:`cd mateclaw-ui && pnpm dev` +3. 访问 http://localhost:5173,登录 + +--- + +## 一、StreamLoadingBar 状态简化验证 + +### TC-1.1 思考中状态(Thinking) +- **Agent**: MateClaw Assistant(ReAct) +- **操作**: 发送 "请分析一下量子计算的发展趋势" +- **预期**: + - 加载条显示 **"思考中…"** 和 ◐ 图标,不再显示 "准备上下文"/"读取记忆"/"推理中" 等内部阶段 + - 无 statusDetail 第二行解释文本 + - 无 slowHint(即使等待超过 8 秒也不出现 "耗时较长" 提示) + - 仅显示耗时计时器(如 "12s"),不显示 token 计数 + +### TC-1.2 执行中状态(Working) +- **Agent**: MateClaw Assistant(ReAct),绑定 WebSearch 工具 +- **操作**: 发送 "搜索一下今天的科技新闻" +- **预期**: + - 工具调用时加载条切换为 **"执行中…"** 和 ⚙ 图标 + - 显示工具名(如 `search`) + - 不显示 "正在执行工具" 的详情文本 + +### TC-1.3 撰写中状态(Writing) +- **Agent**: MateClaw Assistant(ReAct) +- **操作**: 发送 "写一篇 500 字的短文" +- **预期**: + - 内容流输出阶段,加载条显示 **"生成中…"** 和 ▸ 图标 + - 流结束后加载条消失 + +### TC-1.4 错误/中断状态 +- **操作**: 发送消息后立即点击停止按钮 +- **预期**: + - 加载条图标变为 ⊘,文本变红 + - 无 amber/blue 等其他颜色状态 + +--- + +## 二、审批 UI 精简验证 + +### TC-2.1 Shell 命令触发审批(高危操作) +- **Agent**: MateClaw Assistant(ReAct),绑定 Shell 工具 +- **操作**: 发送 "帮我删除 /tmp/test 目录下的所有临时文件" +- **预期**: + - Agent 推理后调用 `execute_shell_command`,参数含 `rm` + - Guard 规则 `SHELL_RM` 触发 → 进入审批流程 + - **ChatInput 区域**:替换为审批栏,显示工具名 + 批准/拒绝按钮(保留) + - **MessageBubble 中**:仅显示一行 "等待审批:`execute_shell_command`",无完整的审批卡片(无 severity 徽章、无 findings 列表、无参数展示、无等待 spinner) + - 点击"批准"后,气泡状态变为 "已批准:`execute_shell_command`" + +### TC-2.2 文件写入触发审批 +- **Agent**: MateClaw Assistant(ReAct),绑定 WriteFile 工具 +- **操作**: 发送 "创建一个 hello.txt 文件,内容写 Hello World" +- **预期**: + - `write_file` 工具触发审批 + - 输入栏显示审批操作,气泡仅一行状态 + - 拒绝后,气泡状态变为 "已拒绝:`write_file`" + +### TC-2.3 危险命令直接阻断(CRITICAL 级别) +- **Agent**: MateClaw Assistant(ReAct),绑定 Shell 工具 +- **操作**: 发送 "执行 rm -rf /" +- **预期**: + - Guard 规则 `SHELL_RM_RF_ROOT` 直接 BLOCK + - 不进入审批流程,直接返回阻断消息 + - 输入栏不显示审批栏 + +--- + +## 三、Plan-Execute 流程验证 + +### TC-3.1 PlanStepsPanel 渲染唯一性 +- **Agent**: Task Planner(Plan-Execute) +- **操作**: 发送 "帮我调研 MateClaw 项目的技术栈,列出前端和后端分别用了哪些核心技术,然后生成一个技术概览文档" +- **预期**: + - 生成计划后,PlanStepsPanel 只在消息气泡中出现**一次** + - 步骤进度正确显示(pending → running → completed) + - 不在分段式视图和传统模式中同时出现两个 PlanStepsPanel + +### TC-3.2 Plan 中触发审批的步骤暂停与恢复 +- **Agent**: Task Planner(Plan-Execute),绑定 Shell + WriteFile 工具 +- **操作**: 发送 "查看当前目录结构,然后创建一个 project-summary.md 文件" +- **预期**: + - 计划包含多个步骤 + - 涉及文件写入的步骤触发审批 + - 审批期间,PlanStepsPanel 该步骤显示 running 状态 + - 气泡中审批为一行极简文本 + - 批准后步骤继续执行,状态更新为 completed + +--- + +## 四、BrowserTimeline 默认收起验证 + +### TC-4.1 浏览器操作时间线默认折叠 +- **Agent**: MateClaw Assistant(ReAct),绑定 BrowserUse 工具 +- **操作**: 发送 "打开浏览器访问 baidu.com,截图" +- **预期**: + - 浏览器操作完成后,时间线默认**收起** + - 仅显示标题栏 "Browser: N actions" + - 点击标题栏可展开查看操作细节和截图 + +--- + +## 五、动画与视觉一致性验证 + +### TC-5.1 无 Typing Bounce Dots +- **Agent**: 任意 Agent +- **操作**: 发送消息,观察 AI 响应开始前 +- **预期**: + - 不再出现三个弹跳圆点的加载动画 + - 使用 TypingCursor(闪烁光标)代替 + +### TC-5.2 动画一致性 +- **操作**: 在不同场景触发加载状态 +- **预期**: + - StreamLoadingBar 的 icon-pulse 动画时长统一为 1.2s + - 所有 spinner 使用相同的旋转动画 + - 无竞争性的多重脉冲动画 + +--- + +## 六、ChatInput 占位符验证 + +### TC-6.1 加载中占位符不暴露键盘操作 +- **Agent**: 任意 Agent +- **操作**: 发送消息,在 AI 生成过程中观察输入框 +- **预期**: + - 占位符仅显示原始 placeholder 文本 + - 不再显示 "(Enter to send / interrupt)" + - 发送按钮变为红色停止图标已足够提示 + +--- + +## 七、深色模式主题变量验证 + +### TC-7.1 深色模式颜色正确性 +- **操作**: 切换到深色模式(侧边栏底部主题切换) +- **检查项**: + - StreamLoadingBar 文本颜色使用主题主色调(非硬编码 #f97316) + - 审批状态文字颜色正确(成功绿/失败红均跟随主题) + - 中断按钮使用 `--mc-warning` 变量的深色模式值 (#fbbf24) + - 排队指示器使用 `--mc-info` 变量的深色模式值 (#60a5fa) + - 工具调用状态图标(成功/失败/等待)颜色均来自 CSS 变量 + +### TC-7.2 浅色/深色快速切换 +- **操作**: 在生成过程中快速切换浅色/深色模式 +- **预期**: + - 所有颜色即时切换,无残留的硬编码颜色 + +--- + +## 八、回归测试 + +### TC-8.1 普通对话流程(无工具调用) +- **Agent**: MateClaw Assistant +- **操作**: 发送 "你好,介绍一下你自己" +- **预期**: 正常生成回复,无 UI 异常 + +### TC-8.2 多轮对话 + 工具调用 +- **Agent**: MateClaw Assistant,绑定多个工具 +- **操作**: 连续发送 3-5 条消息,触发不同工具 +- **预期**: 每轮消息的加载条、工具调用显示、内容输出均正常 + +### TC-8.3 消息中断与重试 +- **操作**: 发送消息 → 中断 → 重试 +- **预期**: 中断指示器正常显示,重试后正常生成 + +### TC-8.4 会话切换 +- **操作**: 在多个会话间切换 +- **预期**: 历史消息正确加载,审批状态(已批准/已拒绝)正确回显 + +### TC-8.5 移动端响应式 +- **操作**: 浏览器宽度缩小到 768px 以下 +- **预期**: + - 审批栏在 ChatInput 中正常自适应 + - 气泡中审批状态一行文本不溢出 + - 加载条内容不截断 diff --git a/mateclaw-ui/src/assets/main.css b/mateclaw-ui/src/assets/main.css index 036b6fa1..1a8a40eb 100644 --- a/mateclaw-ui/src/assets/main.css +++ b/mateclaw-ui/src/assets/main.css @@ -122,7 +122,12 @@ --mc-thinking-border: rgba(217, 119, 87, 0.2); --mc-danger: #C0392B; --mc-danger-bg: #fee2e2; + --mc-danger-border: #fca5a5; + --mc-danger-hover: #a93226; --mc-success: #5A8A5A; + --mc-warning: #f59e0b; + --mc-warning-hover: #d97706; + --mc-info: #3b82f6; /* scrollbar */ --mc-scrollbar-thumb: #C4B5A8; @@ -232,7 +237,12 @@ html.dark { --mc-thinking-border: rgba(224, 136, 96, 0.2); --mc-danger: #E05A4A; --mc-danger-bg: rgba(224, 90, 74, 0.15); + --mc-danger-border: rgba(224, 90, 74, 0.4); + --mc-danger-hover: #c94a3a; --mc-success: #7AB87A; + --mc-warning: #fbbf24; + --mc-warning-hover: #f59e0b; + --mc-info: #60a5fa; /* scrollbar */ --mc-scrollbar-thumb: #5A4438; diff --git a/mateclaw-ui/src/components/chat/BrowserTimeline.vue b/mateclaw-ui/src/components/chat/BrowserTimeline.vue index 1b7a4217..e48c6ffe 100644 --- a/mateclaw-ui/src/components/chat/BrowserTimeline.vue +++ b/mateclaw-ui/src/components/chat/BrowserTimeline.vue @@ -59,7 +59,7 @@ const props = defineProps<{ actions: BrowserAction[] }>() -const expanded = ref(true) +const expanded = ref(false) const latestScreenshot = computed(() => { for (let i = props.actions.length - 1; i >= 0; i--) { diff --git a/mateclaw-ui/src/components/chat/ChatInput.vue b/mateclaw-ui/src/components/chat/ChatInput.vue index 8aef9f0c..3684012c 100644 --- a/mateclaw-ui/src/components/chat/ChatInput.vue +++ b/mateclaw-ui/src/components/chat/ChatInput.vue @@ -279,7 +279,7 @@ const canSend = computed(() => { const inputPlaceholder = computed(() => { if (props.loading) { if (props.queuedMessage) return t('chat.queuedReplace') - return props.placeholder + ' (Enter to send / interrupt)' + return props.placeholder } return props.placeholder }) @@ -704,9 +704,9 @@ defineExpose({ } .approval-bar__btn--deny:hover { - background: #fee2e2; - color: #dc2626; - border-color: #fca5a5; + background: var(--mc-danger-bg, #fee2e2); + color: var(--mc-danger, #ef4444); + border-color: var(--mc-danger-border, #fca5a5); } /* 输入区域容器 */ @@ -733,7 +733,7 @@ defineExpose({ align-items: center; gap: 6px; font-size: 12px; - color: #3b82f6; + color: var(--mc-info, #3b82f6); } @media (max-width: 768px) { @@ -779,19 +779,19 @@ defineExpose({ } .queued-indicator__cancel:hover { - color: #ef4444; - border-color: #fca5a5; - background: #fee2e2; + color: var(--mc-danger, #ef4444); + border-color: var(--mc-danger-border, #fca5a5); + background: var(--mc-danger-bg, #fee2e2); } /* 中断发送按钮样式 */ .send-btn.is-interrupt { - background: #f59e0b; + background: var(--mc-warning, #f59e0b); color: white; } .send-btn.is-interrupt:hover:not(:disabled) { - background: #d97706; + background: var(--mc-warning-hover, #d97706); } /* ===== 移动端适配 ===== */ diff --git a/mateclaw-ui/src/components/chat/MessageBubble.vue b/mateclaw-ui/src/components/chat/MessageBubble.vue index 956195d2..f203fff0 100644 --- a/mateclaw-ui/src/components/chat/MessageBubble.vue +++ b/mateclaw-ui/src/components/chat/MessageBubble.vue @@ -21,6 +21,8 @@