GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-14-tool-visibility-classification.md
2026-05-20 21:39:12 +08:00

494 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 工具可见性分类机制 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在工具定义层引入 `visibility` 属性,从源头区分内部工具/普通工具/Skill消除前端重复展示和 filter hack。
**Architecture:** 后端工具类型新增 `visibility` 字段,执行引擎根据该字段决定是否触发 UI 回调。前端移除 todo 过滤 hack新建 `tool-card.vue` 组件区分 skill 和工具的展示。
**Tech Stack:** TypeScript, Midway.js, Vue 3, Element Plus, Socket.IO
**Spec:** `docs/superpowers/specs/2026-04-14-tool-visibility-classification-design.md`
---
### Task 1: 后端 — 工具类型定义新增 visibility 字段
**Files:**
- Modify: `packages/backend/src/modules/netaclaw/tools/common.ts:5-19`
- [ ] **Step 1: 在 common.ts 中新增 ToolVisibility 类型和 visibility 字段**
`AgentTool` 类型定义之前,新增类型:
```typescript
export type ToolVisibility = 'internal' | 'tool' | 'skill';
```
`AgentToolWithMeta` 类型中新增 `visibility` 可选字段:
```typescript
export type AgentToolWithMeta<TParams extends TSchema, TResult> =
AgentTool<TParams, TResult> & {
ownerOnly?: boolean;
displaySummary?: string;
visibility?: ToolVisibility;
};
```
- [ ] **Step 2: 验证后端编译通过**
Run: `cd packages/backend && npx tsc --noEmit 2>&1 | head -20`
Expected: 无新增错误(已有错误可忽略)
- [ ] **Step 3: Commit**
```bash
git add packages/backend/src/modules/netaclaw/tools/common.ts
git commit -m "feat(netaclaw): 工具类型新增 visibility 字段 (internal/tool/skill)"
```
---
### Task 2: 后端 — 执行引擎根据 visibility 过滤回调
**Files:**
- Modify: `packages/backend/src/modules/netaclaw/runtime/attempt.ts:59-78`
- [ ] **Step 1: 修改 attempt.ts 工具执行循环**
将当前的工具执行循环(第 59-78 行)修改为根据 `visibility` 决定是否触发回调:
```typescript
for (const tc of response.toolCalls) {
toolCallCount++;
const tool = tools.find(t => t.name === tc.name);
const visibility = tool?.visibility ?? 'tool';
// 仅非 internal 工具触发 UI 回调
if (visibility !== 'internal') {
onToolCall?.(tc.name, JSON.parse(tc.arguments));
}
let resultText: string;
if (!tool) {
resultText = `错误: 工具 "${tc.name}" 不存在`;
} else {
try {
const result = await tool.execute(tc.id, JSON.parse(tc.arguments));
resultText = typeof result === 'string' ? result : JSON.stringify(result);
} catch (err: any) {
resultText = `工具执行错误: ${err.message}`;
}
}
// 仅非 internal 工具触发 UI 回调
if (visibility !== 'internal') {
onToolResult?.(tc.name, resultText);
}
conversation.push({ role: 'tool', content: resultText, toolCallId: tc.id });
}
```
关键点:
- `tool` 变量的查找提前到回调判断之前
- `visibility` 从 tool 对象读取,默认 `'tool'`
- `conversation.push` 不受影响LLM 仍需看到工具返回值)
- [ ] **Step 2: 验证后端编译通过**
Run: `cd packages/backend && npx tsc --noEmit 2>&1 | head -20`
Expected: 无新增错误
- [ ] **Step 3: Commit**
```bash
git add packages/backend/src/modules/netaclaw/runtime/attempt.ts
git commit -m "feat(netaclaw): 执行引擎根据 visibility 过滤 tool_call/tool_result 回调"
```
---
### Task 3: 后端 — todo 工具标记为 internal
**Files:**
- Modify: `packages/backend/src/modules/netaclaw/runtime/agent.ts:70-79`
- [ ] **Step 1: 在 agent.ts 中为 todoTool 设置 visibility: 'internal'**
将第 70-79 行的 todoTool 定义修改为:
```typescript
const todoTool: AnyAgentTool = {
...todoToolSchema,
label: '任务规划',
visibility: 'internal',
execute: async (_id: string, args: any) => {
const result = executeTodo(todoStore, args);
params.onTodoUpdate?.(result);
return JSON.stringify(result);
},
};
```
唯一改动:新增 `visibility: 'internal'` 一行。
- [ ] **Step 2: 验证后端编译通过**
Run: `cd packages/backend && npx tsc --noEmit 2>&1 | head -20`
Expected: 无新增错误
- [ ] **Step 3: Commit**
```bash
git add packages/backend/src/modules/netaclaw/runtime/agent.ts
git commit -m "feat(netaclaw): todo 工具标记为 internal不再推送 tool_call/tool_result"
```
---
### Task 4: 前端 — SkillProgress 类型新增 source 字段
**Files:**
- Modify: `packages/frontend/src/modules/agent/types/index.d.ts:151-173`
- [ ] **Step 1: 在 SkillProgress 接口中新增 source 字段**
`SkillProgress` 接口中新增 `source` 可选字段(第 152 行之后):
```typescript
export interface SkillProgress {
name: string;
label?: string;
status: 'running' | 'done' | 'error';
/** 来源类型skill=Skill技能, tool=普通工具调用 */
source?: 'skill' | 'tool';
input?: any;
result?: any;
step?: string;
percent?: number;
detail?: string;
current?: number;
total?: number;
taskId?: string;
images?: string[];
tokens?: { input: number; output: number; total: number; apiCalls?: number };
}
```
- [ ] **Step 2: Commit**
```bash
git add packages/frontend/src/modules/agent/types/index.d.ts
git commit -m "feat(agent): SkillProgress 类型新增 source 字段区分 skill/tool"
```
---
### Task 5: 前端 — Store 层标记 source 字段
**Files:**
- Modify: `packages/frontend/src/modules/agent/store/chat.ts:264-316`
- [ ] **Step 1: skill_start 事件处理中标记 source: 'skill'**
修改第 271-275 行,在 push 时添加 `source: 'skill'`
```typescript
case 'skill_start': {
const startName = event.name;
activeSkill.value = startName;
const existingDone = skillProgress.value.find(s => s.name === startName && s.status === 'done');
const existingRunning = skillProgress.value.find(s => s.name === startName && s.status === 'running');
if (!existingDone && !existingRunning) {
skillProgress.value.push({
name: startName,
label: event.label || startName,
status: 'running',
source: 'skill',
});
}
break;
}
```
- [ ] **Step 2: tool_call 事件处理中标记 source: 'tool'**
修改第 308-314 行,在 push 时添加 `source: 'tool'`
```typescript
case 'tool_call': {
const toolName = event.toolName || event.name || 'unknown';
activeSkill.value = toolName;
const existingRunning = skillProgress.value.find(s => s.name === toolName && s.status === 'running');
if (!existingRunning) {
skillProgress.value.push({
name: toolName,
label: toolName,
status: 'running',
source: 'tool',
});
}
break;
}
```
- [ ] **Step 3: done 事件序列化中加入 source 字段**
`handleWSEvent``done` 分支中(约第 383 行),修改 `skillExecutions` 序列化,加入 `source`
```typescript
lastAssistant.metadata.skillExecutions = skillProgress.value.map(sp => ({
name: sp.name, label: sp.label, status: sp.status, result: sp.result, tokens: sp.tokens, source: sp.source
}));
```
唯一改动:在 map 对象中新增 `source: sp.source`
- [ ] **Step 4: Commit**
```bash
git add packages/frontend/src/modules/agent/store/chat.ts
git commit -m "feat(agent): Store 层 skill/tool 事件标记 source 字段,序列化保留 source"
```
---
### Task 6: 前端 — 新建 tool-card.vue 组件
**Files:**
- Create: `packages/frontend/src/modules/agent/components/tool-card.vue`
- [ ] **Step 1: 创建 tool-card.vue**
```vue
<template>
<div class="tool-card" :class="[`tool-card--${status}`]">
<div class="tool-card__header">
<el-icon :class="{ 'is-rotating': status === 'running' }">
<Loading v-if="status === 'running'" />
<CircleCheck v-else-if="status === 'done'" />
<CircleClose v-else />
</el-icon>
<span class="tool-card__name">{{ label || name }}</span>
<el-tag v-if="status === 'done'" size="small" type="info">完成</el-tag>
<el-tag v-else-if="status === 'error'" size="small" type="danger">失败</el-tag>
</div>
<transition name="fade">
<div v-if="status === 'done' && result" class="tool-card__result">
<el-button text size="small" @click="showResult = !showResult">
{{ showResult ? '收起' : '查看结果' }}
</el-button>
</div>
</transition>
<el-collapse-transition>
<div v-if="showResult && result" class="tool-card__result-content">
<pre class="tool-card__result-pre">{{ typeof result === 'string' ? result : JSON.stringify(result, null, 2) }}</pre>
</div>
</el-collapse-transition>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Loading, CircleCheck, CircleClose } from '@element-plus/icons-vue';
defineProps<{
name: string;
label?: string;
status: 'running' | 'done' | 'error';
result?: any;
}>();
const showResult = ref(false);
</script>
```
样式部分(与 skill-card 类似但更简洁,无进度条区域):
```vue
<style lang="scss" scoped>
.tool-card {
width: min(100%, var(--chat-content-max-width, 760px));
margin: 6px 0 8px;
padding: 8px 12px;
border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--tool-border-color) 18%, transparent);
border-left-width: 3px;
background: var(--el-bg-color);
font-size: 13px;
box-sizing: border-box;
transition: all 0.24s ease;
}
.tool-card--running { --tool-border-color: var(--el-color-info); }
.tool-card--done { --tool-border-color: var(--el-color-info); }
.tool-card--error { --tool-border-color: var(--el-color-danger); }
.tool-card__header {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.tool-card__name {
flex: 1;
font-weight: 500;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--el-text-color-secondary);
}
.is-rotating {
animation: rotating 1.5s linear infinite;
}
@keyframes rotating {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.tool-card__result { margin-top: 6px; }
.tool-card__result-content { margin-top: 6px; }
.tool-card__result-pre {
margin: 0;
padding: 8px 10px;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color-extra-light);
border-radius: 8px;
font-size: 12px;
line-height: 1.5;
max-height: 160px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
:deep(.el-tag) {
height: 18px;
padding: 0 6px;
border-radius: 999px;
font-size: 11px;
}
:deep(.el-button.is-text) {
padding: 0;
font-size: 12px;
}
.fade-enter-active,
.fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
</style>
```
- [ ] **Step 2: Commit**
```bash
git add packages/frontend/src/modules/agent/components/tool-card.vue
git commit -m "feat(agent): 新建 tool-card.vue 工具调用卡片组件"
```
---
### Task 7: 前端 — chat.vue 移除 hack 并根据 source 选择组件
**Files:**
- Modify: `packages/frontend/src/modules/agent/views/chat.vue:56-59` (模板)
- Modify: `packages/frontend/src/modules/agent/views/chat.vue:341-343` (计算属性)
- [ ] **Step 1: 在 chat.vue 的 script 中导入 tool-card 组件**
在已有的 `import` 区域skill-card 导入附近)添加:
```typescript
import ToolCard from '../components/tool-card.vue';
```
- [ ] **Step 2: 移除 visibleSkillProgress 计算属性**
删除第 341-343 行:
```typescript
// 删除这段
const visibleSkillProgress = computed(() => {
return skillProgress.value.filter(sp => sp.name !== 'todo');
});
```
- [ ] **Step 3: 修改模板中的 Skill 卡片渲染区域**
将第 56-59 行:
```vue
<!-- Skill 执行卡片替代 skill-indicator -->
<div class="skill-card-row" v-for="sp in visibleSkillProgress" :key="sp.name">
<skill-card v-bind="sp" />
</div>
```
替换为:
```vue
<!-- Skill / 工具执行卡片 -->
<div class="skill-card-row" v-for="sp in skillProgress" :key="sp.name">
<skill-card v-if="sp.source === 'skill'" v-bind="sp" />
<tool-card v-else v-bind="sp" />
</div>
```
- [ ] **Step 4: 验证前端编译通过**
Run: `cd packages/frontend && npx vue-tsc --noEmit 2>&1 | head -20`
Expected: 无新增错误
- [ ] **Step 5: Commit**
```bash
git add packages/frontend/src/modules/agent/views/chat.vue
git commit -m "feat(agent): chat.vue 移除 todo filter hack根据 source 区分 skill-card/tool-card"
```
---
### Task 8: 端到端验证
**Files:** 无新增改动
- [ ] **Step 1: 启动后端验证编译**
Run: `cd packages/backend && npx tsc --noEmit 2>&1 | head -30`
Expected: 无新增错误
- [ ] **Step 2: 启动前端验证编译**
Run: `cd packages/frontend && npx vue-tsc --noEmit 2>&1 | head -30`
Expected: 无新增错误
- [ ] **Step 3: 功能验证清单**
手动测试(需启动 `pnpm dev`
1. 发送一条需要任务规划的消息(如"帮我分析这个项目的架构列出3个改进点"
2. 验证 todo-card 正常显示并实时更新(划掉已完成任务)
3. 验证对话流中不再出现 todo 的"工具完成"卡片
4. 如果 Agent 调用了其他工具(如搜索),验证 tool-card 正常显示
5. 如果 Agent 调用了 Skill验证 skill-card 正常显示(带进度条)
6. 切换会话后,验证历史消息中的 skill/tool 卡片正确恢复
- [ ] **Step 4: 最终 Commit如有修复**
```bash
git add -A
git commit -m "fix(agent): 端到端验证修复"
```