GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-14-tool-visibility-classification.md

494 lines
13 KiB
Markdown
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
# 工具可见性分类机制 实施计划
> **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): 端到端验证修复"
```