GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-16-clarify-frontend-design.md
2026-05-20 21:39:12 +08:00

316 lines
8.3 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.

# Clarify 工具前端交互设计
> 日期: 2026-04-16
> 背景: 后端已实现 clarify 工具WebSocket 协议 + 阻塞机制),但前端 Agent 对话页面缺少对应 UI导致 clarify_request 事件无人处理。后端 Promise 无限等待直到用户回答。
## 1. 目标
在前端 Agent 对话页面实现 clarify 工具的交互 UI
- 收到 `clarify_request` 时,在聊天流中显示交互卡片
- 用户可点选预设选项或输入自定义回答
- 回答后发送 `clarify_response` 回后端
- 卡片状态从"等待回答"变为"已回答"
## 2. 数据流
```
后端 clarify_request → WS message 事件
→ store/chat.ts handleWSEvent 新增 case
→ 创建 role='clarify' 的 ChatMessage 插入 messages
→ chat.vue 渲染时识别 clarify 消息
→ 渲染 clarify-card.vue 交互卡片
用户点选/输入 → clarify-card emit('answer', text)
→ store.sendClarifyResponse(requestId, answer)
→ WS emit clarify_response
→ 后端 Promise resolveAgent 继续执行
```
## 3. 涉及文件
| 文件 | 操作 | 说明 |
|------|------|------|
| `types/index.d.ts` | 修改 | 扩展 WSServerEvent/WSClientMessage 类型,新增 ClarifyRequestData 接口 |
| `store/chat.ts` | 修改 | handleWSEvent 新增 `clarify_request` case新增 `sendClarifyResponse()` |
| `components/clarify-card.vue` | 新建 | 交互卡片组件 |
| `components/message-item.vue` | 修改 | 识别 `role='clarify'` 消息,渲染 clarify-card |
所有路径基于 `packages/frontend/src/modules/agent/`
## 4. 类型定义扩展
```typescript
// types/index.d.ts
// WSServerEvent.type 联合追加 'clarify_request'
// WSClientMessage.type 联合追加 'clarify_response'
interface ClarifyRequestData {
requestId: string;
question: string;
choices?: string[];
}
// ChatMessage.role 联合追加 'clarify'
// ChatMessage.metadata 中存储 ClarifyRequestData + status
```
## 5. Store 变更 (store/chat.ts)
### handleWSEvent 新增 case
```typescript
case 'clarify_request': {
const data = event.data as ClarifyRequestData;
messages.value.push({
role: 'clarify',
content: data.question,
metadata: {
requestId: data.requestId,
choices: data.choices,
status: 'pending', // pending | answered
answer: null,
},
});
scrollToBottom();
break;
}
```
### sendClarifyResponse 函数
```typescript
function sendClarifyResponse(requestId: string, answer: string) {
// 更新消息状态
const msg = messages.value.find(
m => m.role === 'clarify' && m.metadata?.requestId === requestId
);
if (msg) {
msg.metadata.status = 'answered';
msg.metadata.answer = answer;
}
// 发送 WS 消息
wsInstance.send({
type: 'clarify_response',
sessionId: sessionId.value,
requestId,
answer,
});
}
```
## 6. clarify-card.vue 组件设计
### Props
- `message: ChatMessage` — role='clarify' 的消息对象
### 状态
- `pending`: 显示问题 + 选项按钮 + 自定义输入框
- `answered`: 只读,显示"已回答xxx"
### 模板结构
```
<div class="clarify-card">
<!-- 头部:图标 + 标题 -->
<div class="header">
🤔 Agent 需要你的确认
</div>
<!-- 问题文本 -->
<div class="question">{{ message.content }}</div>
<!-- pending 状态 -->
<template v-if="status === 'pending'">
<!-- 选项按钮(如果有 choices -->
<div class="choices" v-if="choices?.length">
<el-button v-for="choice in choices" @click="selectChoice(choice)">
{{ choice }}
</el-button>
</div>
<!-- 自定义输入 -->
<div class="custom-input">
<el-input v-model="customAnswer" placeholder="输入自定义回答..." />
<el-button @click="submitCustom" :disabled="!customAnswer.trim()">
发送
</el-button>
</div>
</template>
<!-- answered 状态 -->
<div v-else class="answered">
✅ 已回答:{{ answer }}
</div>
</div>
```
### 样式
- 遵循 skill-card / todo-card 的卡片风格
- 圆角边框,浅色背景
- 选项按钮使用 Element Plus 的 `el-button` plain 样式
- pending 状态有轻微脉冲动画提示用户需要操作
## 7. message-item.vue 变更
在消息渲染逻辑中新增 clarify 分支:
```vue
<template v-if="message.role === 'clarify'">
<clarify-card :message="message" @answer="handleClarifyAnswer" />
</template>
```
`handleClarifyAnswer` 调用 store 的 `sendClarifyResponse`
## 8. 微信端降级
微信端无法渲染自定义卡片,采用纯文本问答降级。
### 8.1 数据流
```
Agent 调用 clarify tool → onClarifyRequest 回调
→ 格式化为纯文本(带编号选项)
→ weixinService.sendText() 发送给用户
→ Promise 阻塞等待(无超时,与 Web 端一致)
用户回复微信消息 → handleInboundMessage()
→ 检查 pendingClarify Map 是否有该用户的 pending 请求
→ 有resolve PromiseAgent 继续执行
→ 无:正常走 agentExecutor.execute() 新对话
```
### 8.2 涉及文件
| 文件 | 操作 | 说明 |
|------|------|------|
| `service/agent_executor.ts` | 修改 | execute() 新增可选 `onClarifyRequest` 回调,传递给 runAgent() |
| `service/agent_channel.ts` | 修改 | 新增 pendingClarify Map + clarify 文本格式化 + 消息路由 |
所有路径基于 `packages/backend/src/modules/netaclaw/`
### 8.3 agent_executor.ts 变更
`execute()` 方法签名新增可选回调:
```typescript
async execute(params: {
sessionId?: string;
message: string;
agentId?: number;
agentName?: string;
userId?: string;
onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
}) {
// ... 现有逻辑 ...
const result = await runAgent({
agentConfig,
tools: [...this.defaultTools, ...memoryTools, ...skillTools],
userMessage: params.message,
history: history.slice(0, -1),
onClarifyRequest: params.onClarifyRequest, // 透传
});
}
```
### 8.4 agent_channel.ts 变更
#### 模块级 Map
```typescript
/** 微信 clarify 阻塞 Map — key: `${channelId}:${senderId}` */
private readonly pendingClarify = new Map<string, { resolve: (answer: string) => void }>();
```
#### handleInboundMessage 消息路由
在现有 `agentExecutor.execute()` 调用之前,检查是否有 pending clarify
```typescript
// 检查是否有 pending clarify
const clarifyKey = `${channel.id}:${senderId}`;
const pending = this.pendingClarify.get(clarifyKey);
if (pending) {
this.pendingClarify.delete(clarifyKey);
pending.resolve(text);
return; // 不启动新对话
}
```
#### onClarifyRequest 回调
传递给 `agentExecutor.execute()`
```typescript
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 });
});
},
});
```
#### 选项编号解析
用户回复数字时自动映射到对应选项(在 resolve 前处理):
```typescript
if (pending) {
this.pendingClarify.delete(clarifyKey);
// 数字映射(如果有 choices 上下文)
pending.resolve(text);
return;
}
```
注:数字映射需要在 pendingClarify Map 中额外存储 choices 数组。Map value 扩展为:
```typescript
private readonly pendingClarify = new Map<string, {
resolve: (answer: string) => void;
choices?: string[];
}>();
```
resolve 前检查:
```typescript
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);
```
### 8.5 微信用户体验
**Agent 提问时用户看到:**
```
❓ 你想要什么风格的商品描述?
1. 简洁专业
2. 活泼有趣
3. 高端奢华
请回复数字或直接输入你的回答
```
**用户回复 "2"** → 自动映射为 "活泼有趣"
**用户回复 "我想要文艺风"** → 直接使用原文