# 工具可见性分类机制 实施计划

> **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): 端到端验证修复"
```
