# Clarify 工具前端交互 + 微信降级 实现计划

> **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.
> **Post-task review:** After each task commit, run the `simplify` skill to review changed code for reuse, quality, and efficiency.

**Goal:** 在前端 Agent 对话页面实现 clarify 工具的交互 UI，并在后端为微信渠道实现纯文本降级方案。

**Architecture:** 收到 `clarify_request` WS 事件时，在聊天流中插入 `role='clarify'` 的特殊消息，由新建的 `clarify-card.vue` 组件渲染为交互卡片（选项按钮 + 自定义输入）。用户回答后通过 WS 发送 `clarify_response`，卡片变为只读。微信端通过 `agent_executor.ts` 透传 `onClarifyRequest` 回调，`agent_channel.ts` 用 pendingClarify Map 阻塞等待用户回复。

**Tech Stack:** Vue 3 + TypeScript + Element Plus + Pinia + Socket.IO (前端) / Midway.js + TypeScript (后端)

**Base path (前端):** `packages/frontend/src/modules/agent`
**Base path (后端):** `packages/backend/src/modules/netaclaw`

---

### Task 1: 类型定义扩展

**Files:**
- Modify: `packages/frontend/src/modules/agent/types/index.d.ts`

- [ ] **Step 1: 扩展 ChatMessage.role 联合类型**

在 `types/index.d.ts` 第 21 行，`ChatMessage.role` 联合追加 `'clarify'`：

```typescript
// 旧:
role: 'user' | 'assistant' | 'tool' | 'system';

// 新:
role: 'user' | 'assistant' | 'tool' | 'system' | 'clarify';
```

- [ ] **Step 2: 扩展 WSClientMessage.type 联合类型并追加字段**

在 `types/index.d.ts` 第 131-136 行，修改 `WSClientMessage`：

```typescript
// 旧:
export interface WSClientMessage {
	type: 'chat' | 'ping';
	sessionId?: string;
	content?: string;
	agentId?: number;
}

// 新:
export interface WSClientMessage {
	type: 'chat' | 'ping' | 'clarify_response';
	sessionId?: string;
	content?: string;
	agentId?: number;
	requestId?: string;
	answer?: string;
}
```

- [ ] **Step 3: 扩展 WSServerEvent.type 联合类型**

在 `types/index.d.ts` 第 142 行，`WSServerEvent.type` 联合追加 `'clarify_request'`：

```typescript
// 旧:
type: 'token' | 'thinking' | 'tool_call' | 'tool_result' | 'skill_start' | 'skill_end' | 'progress' | 'token_update' | 'done' | 'error' | 'pong' | 'todo_update' | 'thinking_delta' | 'thinking_done';

// 新:
type: 'token' | 'thinking' | 'tool_call' | 'tool_result' | 'skill_start' | 'skill_end' | 'progress' | 'token_update' | 'done' | 'error' | 'pong' | 'todo_update' | 'thinking_delta' | 'thinking_done' | 'clarify_request';
```

- [ ] **Step 4: 新增 ClarifyRequestData 和 ClarifyMessageMeta 接口**

在 `types/index.d.ts` 文件末尾（`TokenUpdateEvent` 之后）追加：

```typescript
// === Clarify 工具类型 ===
export interface ClarifyRequestData {
	requestId: string;
	question: string;
	choices?: string[];
}

export interface ClarifyMessageMeta {
	requestId: string;
	choices?: string[];
	status: 'pending' | 'answered';
	answer?: string;
}
```

- [ ] **Step 5: Commit**

```bash
git add packages/frontend/src/modules/agent/types/index.d.ts
git commit -m "feat(agent): extend WS types for clarify tool interaction"
```

---

### Task 2: Store 变更 — 处理 clarify_request + 发送 clarify_response

**Files:**
- Modify: `packages/frontend/src/modules/agent/store/chat.ts`

- [ ] **Step 1: 在 handleWSEvent 中新增 clarify_request 提前处理**

在 `store/chat.ts` 的 `handleWSEvent` 函数中，clarify_request 不依赖 assistantMsg，需要在现有的 `if (!loading.value ...)` 和 `const assistantMsg = ...` 守卫之前处理。在函数开头（第 246 行 `function handleWSEvent` 之后）插入：

```typescript
	function handleWSEvent(event: WSServerEvent) {
		// clarify_request 不依赖 assistantMsg，提前处理
		if (event.type === 'clarify_request') {
			const data = event.data;
			if (data?.requestId && data?.question) {
				messages.value.push({
					role: 'clarify',
					content: data.question,
					metadata: {
						requestId: data.requestId,
						choices: data.choices || [],
						status: 'pending',
						answer: undefined,
					},
				});
				_onTokenCbs.forEach(cb => cb());
			}
			return;
		}

		// 以下是原有逻辑，不变
		if (!loading.value && event.type !== 'done') return;
		const assistantMsg = messages.value[messages.value.length - 1];
		if (!assistantMsg || assistantMsg.role !== 'assistant') return;

		switch (event.type) {
			// ... 现有 cases 不变 ...
		}
	}
```

- [ ] **Step 2: 新增 sendClarifyResponse 函数**

在 `store/chat.ts` 的 `setThinkLevel` 函数之后（第 562 行后）追加：

```typescript
	/**
	 * 发送 clarify 回答
	 */
	function sendClarifyResponse(requestId: string, answer: string) {
		const msg = messages.value.find(
			m => m.role === 'clarify' && m.metadata?.requestId === requestId
		);
		if (msg && msg.metadata) {
			msg.metadata.status = 'answered';
			msg.metadata.answer = answer;
		}
		if (wsInstance) {
			wsInstance.send({
				type: 'clarify_response',
				sessionId: sessionId.value,
				requestId,
				answer,
			} as any);
		}
	}
```

- [ ] **Step 3: 在 return 对象中导出 sendClarifyResponse**

在 `store/chat.ts` 第 738 行 `init` 之后追加 `sendClarifyResponse`：

```typescript
	return {
		// ... 现有导出 ...
		init,
		sendClarifyResponse,
	};
```

- [ ] **Step 4: Commit**

```bash
git add packages/frontend/src/modules/agent/store/chat.ts
git commit -m "feat(agent): handle clarify_request WS event and send clarify_response"
```

---

### Task 3: clarify-card.vue 交互卡片组件

**Files:**
- Create: `packages/frontend/src/modules/agent/components/clarify-card.vue`

- [ ] **Step 1: 创建 clarify-card.vue 模板和脚本**

创建 `packages/frontend/src/modules/agent/components/clarify-card.vue`：

```vue
<template>
  <div class="clarify-card" :class="{ 'is-answered': isAnswered }">
    <div class="clarify-card__header">
      <span class="clarify-card__icon">{{ isAnswered ? '✅' : '🤔' }}</span>
      <span class="clarify-card__title">{{ isAnswered ? '已回答' : 'Agent 需要你的确认' }}</span>
    </div>

    <div class="clarify-card__question">{{ message.content }}</div>

    <!-- pending 状态：显示选项和输入 -->
    <template v-if="!isAnswered">
      <div class="clarify-card__choices" v-if="choices.length">
        <el-button
          v-for="(choice, idx) in choices"
          :key="idx"
          class="clarify-card__choice-btn"
          @click="selectChoice(choice)"
        >
          {{ choice }}
        </el-button>
      </div>

      <div class="clarify-card__custom">
        <el-input
          v-model="customAnswer"
          :placeholder="choices.length ? '或输入自定义回答...' : '输入你的回答...'"
          size="default"
          @keydown.enter.prevent="submitCustom"
        />
        <el-button
          type="primary"
          :disabled="!customAnswer.trim()"
          @click="submitCustom"
        >
          发送
        </el-button>
      </div>
    </template>

    <!-- answered 状态 -->
    <div v-else class="clarify-card__answer">
      {{ message.metadata?.answer }}
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref, computed } from 'vue';
import type { ChatMessage } from '../types/index.d';

const props = defineProps<{
  message: ChatMessage;
}>();

const emit = defineEmits<{
  answer: [requestId: string, answer: string];
}>();

const customAnswer = ref('');

const choices = computed(() => props.message.metadata?.choices || []);
const isAnswered = computed(() => props.message.metadata?.status === 'answered');

function selectChoice(choice: string) {
  const requestId = props.message.metadata?.requestId;
  if (requestId) emit('answer', requestId, choice);
}

function submitCustom() {
  const text = customAnswer.value.trim();
  if (!text) return;
  const requestId = props.message.metadata?.requestId;
  if (requestId) emit('answer', requestId, text);
  customAnswer.value = '';
}
</script>
```

- [ ] **Step 2: 添加样式**

在同一文件 `</script>` 之后追加样式（遵循 todo-card 的卡片风格）：

```vue
<style lang="scss" scoped>
.clarify-card {
  width: min(100%, var(--chat-content-max-width, 760px));
  margin: 8px 0 10px;
  border-radius: 12px;
  border: 1px solid var(--el-color-warning-light-5);
  background: linear-gradient(180deg, var(--el-bg-color) 0%, color-mix(in srgb, var(--el-color-warning) 4%, var(--el-bg-color)) 100%);
  box-shadow: 0 8px 18px rgba(15, 23, 42, 0.04);
  overflow: hidden;
  box-sizing: border-box;
  animation: clarify-enter 0.4s ease-out;

  &.is-answered {
    border-color: var(--el-border-color-lighter);
    background: var(--el-fill-color-lighter);
  }
}

.clarify-card__header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  font-size: 13px;
  font-weight: 600;
}

.clarify-card__icon {
  font-size: 16px;
}

.clarify-card__question {
  padding: 0 14px 10px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--el-text-color-primary);
}

.clarify-card__choices {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 0 14px 10px;
}

.clarify-card__choice-btn {
  border-radius: 999px !important;
}

.clarify-card__custom {
  display: flex;
  gap: 8px;
  padding: 0 14px 14px;
}

.clarify-card__answer {
  padding: 0 14px 14px;
  font-size: 13px;
  color: var(--el-text-color-secondary);
  font-style: italic;
}

@keyframes clarify-enter {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
</style>
```

- [ ] **Step 3: Commit**

```bash
git add packages/frontend/src/modules/agent/components/clarify-card.vue
git commit -m "feat(agent): add clarify-card interactive component"
```

---

### Task 4: 集成到聊天视图

**Files:**
- Modify: `packages/frontend/src/modules/agent/views/chat.vue`
- Modify: `packages/frontend/src/modules/agent/components/message-item.vue`

- [ ] **Step 1: chat.vue — 导入 ClarifyCard 组件**

在 `views/chat.vue` 的 `<script>` 部分 import 区域（第 284 行 `import MessageItem` 附近）追加：

```typescript
import ClarifyCard from '../components/clarify-card.vue';
```

- [ ] **Step 2: chat.vue — 在消息列表中渲染 clarify 卡片**

在 `views/chat.vue` 模板的消息列表中，`message-item` 循环（第 40-44 行）需要区分 clarify 消息。将：

```vue
<message-item
	v-for="(msg, index) in precedingMessages"
	:key="index"
	:message="msg"
/>
```

改为：

```vue
<template v-for="(msg, index) in precedingMessages" :key="index">
	<clarify-card
		v-if="msg.role === 'clarify'"
		:message="msg"
		@answer="handleClarifyAnswer"
	/>
	<message-item v-else :message="msg" />
</template>
```

- [ ] **Step 3: chat.vue — 添加 handleClarifyAnswer 方法和 store 导出**

在 `views/chat.vue` 的 `<script setup>` 中，从 chatStore 解构中追加 `sendClarifyResponse`（第 335 行附近）：

```typescript
const {
	sendMessage,
	stopGeneration,
	loadSessions,
	loadAgents,
	selectAgent,
	newSession,
	switchSession,
	deleteSession,
	deleteAllSessions,
	onToken,
	init: initChat,
	setThinkLevel,
	sendClarifyResponse,
} = chatStore;
```

然后在 `handleSend` 函数之前添加：

```typescript
function handleClarifyAnswer(requestId: string, answer: string) {
	sendClarifyResponse(requestId, answer);
}
```

- [ ] **Step 4: message-item.vue — hasVisibleBody 兼容 clarify**

在 `components/message-item.vue` 第 204 行的 `hasVisibleBody` computed 中，clarify 消息不应由 message-item 渲染（已在 chat.vue 中用 `v-if` 分流），但为安全起见，确保 `hasVisibleBody` 对 `role='clarify'` 返回 false：

```typescript
const hasVisibleBody = computed(() => {
	if (props.message.role === 'clarify') return false;
	return Boolean(
		props.message.skillName ||
		(props.message.role === 'assistant' && props.message.thinking) ||
		props.message.content ||
		messageImageUrl.value ||
		(tokenUsage.value && props.message.role === 'assistant')
	);
});
```

- [ ] **Step 5: Commit**

```bash
git add packages/frontend/src/modules/agent/views/chat.vue \
  packages/frontend/src/modules/agent/components/message-item.vue
git commit -m "feat(agent): integrate clarify-card into chat view"
```

---

### Task 5: 后端 — agent_executor.ts 透传 onClarifyRequest

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/service/agent_executor.ts`

- [ ] **Step 1: 扩展 execute() 参数类型**

在 `agent_executor.ts` 第 43 行的 `execute` 方法参数中追加 `onClarifyRequest`：

```typescript
  async execute(params: {
    sessionId?: string;
    message: string;
    agentId?: number;
    agentName?: string;
    userId?: string;
    onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
  }) {
```

- [ ] **Step 2: 透传 onClarifyRequest 给 runAgent**

在 `agent_executor.ts` 第 142 行的 `runAgent` 调用中追加 `onClarifyRequest`：

```typescript
    const result = await runAgent({
      agentConfig,
      tools: [...this.defaultTools, ...memoryTools, ...skillTools],
      userMessage: params.message,
      history: history.slice(0, -1),
      onClarifyRequest: params.onClarifyRequest,
    });
```

- [ ] **Step 3: 添加 clarifyTool 到 defaultTools**

在 `agent_executor.ts` 顶部 import 区域追加：

```typescript
import { clarifyTool } from '../tools/builtin/clarify.js';
```

在第 41 行 `defaultTools` 数组中追加 `clarifyTool`：

```typescript
  private readonly defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool, clarifyTool];
```

- [ ] **Step 4: 验证构建**

Run: `cd packages/backend && npm run build`
Expected: 构建成功

- [ ] **Step 5: Commit**

```bash
git add packages/backend/src/modules/netaclaw/service/agent_executor.ts
git commit -m "feat(netaclaw): agent_executor 透传 onClarifyRequest + 添加 clarifyTool"
```

---

### Task 6: 后端 — agent_channel.ts 微信 clarify 降级

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/service/agent_channel.ts`

- [ ] **Step 1: 添加 pendingClarify Map 类型和实例**

在 `agent_channel.ts` 第 11-15 行 `RunnerState` 类型定义之后，添加 pendingClarify Map 属性到类中。在第 39 行 `private readonly runners` 之后追加：

```typescript
  /** 微信 clarify 阻塞 Map — key: `${channelId}:${senderId}` */
  private readonly pendingClarify = new Map<string, {
    resolve: (answer: string) => void;
    choices?: string[];
  }>();
```

- [ ] **Step 2: handleInboundMessage — 检查 pending clarify**

在 `agent_channel.ts` 的 `handleInboundMessage` 方法中，在 `const text = this.weixinService.extractText(...)` 之后、`await this.persistRuntime(...)` 之前（第 339 行后），插入 pending clarify 检查：

```typescript
    const text = this.weixinService.extractText(message.item_list || []);
    if (!text) return;

    // 检查是否有 pending clarify
    const clarifyKey = `${channel.id}:${senderId}`;
    const pending = this.pendingClarify.get(clarifyKey);
    if (pending) {
      this.pendingClarify.delete(clarifyKey);
      // 数字映射到选项
      let answer = text;
      if (pending.choices?.length) {
        const num = parseInt(text, 10);
        if (num >= 1 && num <= pending.choices.length) {
          answer = pending.choices[num - 1];
        }
      }
      pending.resolve(answer);
      return; // 不启动新对话
    }

    await this.persistRuntime(channel.id, state);
```

- [ ] **Step 3: handleInboundMessage — 传递 onClarifyRequest 回调**

将 `handleInboundMessage` 中现有的 `agentExecutor.execute()` 调用（第 344-350 行）改为传递 `onClarifyRequest`：

```typescript
    const clarifyKey = `${channel.id}:${senderId}`;
    const result = await this.agentExecutor.execute({
      sessionId,
      message: text,
      agentId: channel.agentId,
      agentName: channel.agentName || undefined,
      userId: senderId,
      onClarifyRequest: async (question, choices) => {
        let msg = `❓ ${question}`;
        if (choices?.length) {
          msg += '\n' + choices.map((c, i) => `${i + 1}. ${c}`).join('\n');
          msg += '\n\n请回复数字或直接输入你的回答';
        }
        await this.weixinService.sendText(credential, senderId, msg, state.contextTokens[senderId]);
        return new Promise<string>((resolve) => {
          this.pendingClarify.set(clarifyKey, { resolve, choices });
        });
      },
    });
```

注意：`clarifyKey` 变量在 Step 2 中已定义，这里需要确保 `clarifyKey` 的声明位置在两处都可访问。将 `const clarifyKey` 提到 `const text = ...` 之后即可。

- [ ] **Step 4: 验证构建**

Run: `cd packages/backend && npm run build`
Expected: 构建成功

- [ ] **Step 5: Commit**

```bash
git add packages/backend/src/modules/netaclaw/service/agent_channel.ts
git commit -m "feat(netaclaw): 微信渠道 clarify 纯文本降级 — pendingClarify Map + 编号映射"
```

---

### Task 7: 端到端验证

**Files:** 无新文件

- [ ] **Step 1: 前端构建验证**

Run: `cd packages/frontend && pnpm build`
Expected: 构建成功

- [ ] **Step 2: 后端构建验证**

Run: `cd packages/backend && npm run build`
Expected: 构建成功

- [ ] **Step 3: 最终 Commit（如有修复）**

```bash
git add -A
git commit -m "fix(agent): clarify frontend + WeChat degradation integration fixes"
```
