Ctrl K
ComunidadGuías…

docs/plan/2026-06-05-message-queue-design.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 消息队列 & 消息插入 技术方案

## 1. 需求概述

### 1.1 消息队列

当 Agent 正在执行任务时,允许用户继续发送消息。新消息进入队列等待,Agent 完成当前任务后自动消费队列中的下一条消息。

**核心规则**:
- Agent 空闲时,消息直接发送,不进队列
- Agent 忙碌时,消息进入队列
- 队列消息需要持久化,刷新页面不丢失
- 发送即移除,不关心执行结果
- 自动消费由后端触发
- 超过 5 条消息时,UI 折叠展示

### 1.2 消息插入

在消息队列基础上,用户可以主动选择队列中某条消息立即发送,打断当前 Agent 任务。

**核心规则**:
- 先停止当前 Agent 任务,再发送新消息
- 被打断的任务上下文保留在会话中,显示在对话窗口
- 防并发处理:避免连续点击,但连续发送的消息不能丢弃

### 1.3 队列管理

- 支持删除队列中的消息
- 支持上下箭头调整顺序
- 不支持编辑

---

## 2. 现状分析

### 2.1 当前消息发送流程

```
前端 ChatInput.handleSend()
  → TaskView.handleSend()
    → useChat.sendMessage()
      → WS 发送 { type: 'send_message', sessionId, data: { content, eventId, images } }
        → 后端 StreamingWsHandler.handleSendMessage()
          → 校验 session 未在运行中(非 RUNNING/RESUMING)
          → 持久化 USER 消息
          → 提交到 agentExecutor 线程池异步执行
```

**关键约束**:后端 `handleSendMessage()` 第一步就校验 session 状态,若为 RUNNING/RESUMING 则拒绝。

### 2.2 当前 Agent 状态管理

| 状态 | 含义 |
|------|------|
| `IDLE` | 空闲 |
| `RUNNING` | 执行中 |
| `RESUMING` | 恢复中(崩溃恢复) |
| `WAITING_APPROVAL` | 等待工具审批 |
| `COMPLETED` | 完成 |
| `FAILED` | 失败 |
| `CANCELLED` | 已取消 |
| `CANCELLING` | 取消中(前端状态) |

判断 Agent 是否忙碌:`phase === 'RUNNING' || phase === 'WAITING_APPROVAL'`

### 2.3 当前停止执行流程

```
前端 useChat.stopExecution()
  → 设 cancelling = true
  → WS 发送 { type: 'cancel', sessionId }
    → 后端设置 cancelFlag = true
    → AgentLoop 检测到 cancelFlag,退出循环
    → 后端更新 phase = CANCELLED,通知前端
    → 前端收到 session_status(CANCELLED),resolve pendingCallback
```

---

## 3. 整体设计

### 3.1 架构概览

```
┌─────────────────────────────────────────────────────────┐
│  Frontend (Vue 3 + Pinia)                                │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────┐  │
│  │ ChatInput    │  │ QueuePanel   │  │ MessageBubble  │  │
│  │ (输入框)     │  │ (队列面板)   │  │ (消息气泡)     │  │
│  └──────┬──────┘  └──────┬───────┘  └────────────────┘  │
│         │                │                                │
│  ┌──────┴────────────────┴───────────────────────────┐   │
│  │              useChat + sessionStore                │   │
│  └──────────────────────┬────────────────────────────┘   │
│                         │ WebSocket                        │
└─────────────────────────┼─────────────────────────────────┘
                          │
┌─────────────────────────┼─────────────────────────────────┐
│  Backend (Spring Boot)   │                                  │
│  ┌──────────────────────┴────────────────────────────┐    │
│  │            StreamingWsHandler                      │    │
│  │  ┌──────────────┐  ┌────────────────────────────┐ │    │
│  │  │ handleSend   │  │ handleInsert (新增)         │ │    │
│  │  │ _Message     │  │ handleQueueOps (新增)       │ │    │
│  │  └──────────────┘  └────────────────────────────┘ │    │
│  └──────────────────────┬────────────────────────────┘    │
│  ┌──────────────────────┴────────────────────────────┐    │
│  │            MessageQueueService (新增)              │    │
│  │  - enqueue / dequeue / delete / reorder            │    │
│  │  - autoConsume (Agent 完成后自动消费)               │    │
│  └──────────────────────┬────────────────────────────┘    │
│  ┌──────────────────────┴────────────────────────────┐    │
│  │            SessionService (修改)                   │    │
│  │  - updatePhase 增加自动消费钩子                     │    │
│  └───────────────────────────────────────────────────┘    │
│  ┌───────────────────────────────────────────────────┐    │
│  │            AgentLoop (修改)                        │    │
│  │  - cancelFlag 检查增强,支持外部中断               │    │
│  └───────────────────────────────────────────────────┘    │
└───────────────────────────────────────────────────────────┘
```

### 3.2 核心流程

#### 消息队列流程(Agent 忙碌时)

```
用户输入消息 → 前端判断 Agent 忙碌
  → WS 发送 { type: 'enqueue_message', sessionId, data: { content, images, eventId } }
    → 后端持久化到 message_queue 表
    → 返回 { type: 'queue_updated', sessionId, data: { queue: [...] } }
    → 前端更新队列面板
```

#### 自动消费流程(Agent 完成后)

```
Agent 执行完成 → 后端 updatePhase(COMPLETED)
  → 检查 message_queue 是否有待消费消息
  → 若有:dequeue 头部消息 → 自动触发 handleSendMessage 逻辑
  → 若无:正常完成
```

#### 消息插入流程(打断当前任务)

```
用户点击"立即发送" → 前端判断 Agent 忙碌
  → WS 发送 { type: 'insert_message', sessionId, data: { queueId } }
    → 后端设置 cancelFlag = true,停止当前 Agent
    → 等待 Agent 停止(超时 10s)
    → 从 message_queue 中移除该消息
    → 触发 handleSendMessage 逻辑
```

---

## 4. 数据库设计

### 4.1 新增表:message_queue

```sql
CREATE TABLE message_queue (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    session_id  BIGINT NOT NULL COMMENT '关联会话',
    user_id     BIGINT NOT NULL COMMENT '关联用户',
    content     TEXT NOT NULL COMMENT '消息内容',
    images      TEXT COMMENT '图片 URL JSON 数组',
    sort_order  INT NOT NULL DEFAULT 0 COMMENT '排序序号(越小越靠前)',
    status      VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING / DELETED',
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted     TINYINT NOT NULL DEFAULT 0,
    INDEX idx_session_id (session_id),
    INDEX idx_session_status (session_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

**字段说明**:
| 字段 | 说明 |
|------|------|
| `id` | 主键自增 |
| `session_id` | 关联会话 ID |
| `user_id` | 关联用户 ID(冗余,便于查询) |
| `content` | 消息文本内容 |
| `images` | 图片 URL 数组 JSON,可为空 |
| `sort_order` | 排序序号,新消息默认追加到末尾(取 max + 1) |
| `status` | 状态:PENDING(待发送)、DELETED(已删除) |
| `deleted` | 逻辑删除标志 |

### 4.2 迁移脚本

文件:`V032__add_message_queue.sql`

```sql
CREATE TABLE message_queue (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    session_id  BIGINT NOT NULL,
    user_id     BIGINT NOT NULL,
    content     TEXT NOT NULL,
    images      TEXT,
    sort_order  INT NOT NULL DEFAULT 0,
    status      VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted     TINYINT NOT NULL DEFAULT 0,
    INDEX idx_session_id (session_id),
    INDEX idx_session_status (session_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

---

## 5. 后端设计

### 5.1 新增实体类

**文件**:`backend/src/main/java/cn/etarch/mao/session/entity/MessageQueue.java`

```java
@Data
@TableName("message_queue")
public class MessageQueue {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long sessionId;
    private Long userId;
    private String content;
    private String images;  // JSON array
    private Integer sortOrder;
    private String status;  // PENDING / DELETED
    @TableLogic
    private Integer deleted;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdAt;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedAt;
}
```

### 5.2 新增 Mapper

**文件**:`backend/src/main/java/cn/etarch/mao/session/mapper/MessageQueueMapper.java`

```java
@Mapper
public interface MessageQueueMapper extends BaseMapper<MessageQueue> {
}
```

### 5.3 新增 MessageQueueService

**文件**:`backend/src/main/java/cn/etarch/mao/session/service/MessageQueueService.java`

核心方法:

```java
@Service
@RequiredArgsConstructor
public class MessageQueueService {
    private final MessageQueueMapper messageQueueMapper;

    /**
     * 入队:将消息加入队列末尾
     */
    public MessageQueue enqueue(Long sessionId, Long userId, String content, String images) {
        // 1. 查询当前队列最大 sortOrder
        Integer maxOrder = messageQueueMapper.selectOne(
            new LambdaQueryWrapper<MessageQueue>()
                .eq(MessageQueue::getSessionId, sessionId)
                .eq(MessageQueue::getStatus, "PENDING")
                .orderByDesc(MessageQueue::getSortOrder)
                .last("LIMIT 1")
        ).map(MessageQueue::getSortOrder).orElse(0);

        // 2. 创建队列消息
        MessageQueue item = new MessageQueue();
        item.setSessionId(sessionId);
        item.setUserId(userId);
        item.setContent(content);
        item.setImages(images);
        item.setSortOrder(maxOrder + 1);
        item.setStatus("PENDING");
        messageQueueMapper.insert(item);
        return item;
    }

    /**
     * 出队:取出并删除队列头部消息
     */
    public MessageQueue dequeue(Long sessionId) {
        MessageQueue head = messageQueueMapper.selectOne(
            new LambdaQueryWrapper<MessageQueue>()
                .eq(MessageQueue::getSessionId, sessionId)
                .eq(MessageQueue::getStatus, "PENDING")
                .orderByAsc(MessageQueue::getSortOrder)
                .last("LIMIT 1")
        );
        if (head != null) {
            head.setStatus("DELETED");
            messageQueueMapper.updateById(head);
        }
        return head;
    }

    /**
     * 删除指定队列消息
     */
    public void delete(Long queueId) {
        MessageQueue item = messageQueueMapper.selectById(queueId);
        if (item != null) {
            item.setStatus("DELETED");
            messageQueueMapper.updateById(item);
        }
    }

    /**
     * 重新排序:将指定消息上移/下移
     */
    public void reorder(Long queueId, String direction) {
        // direction: "up" / "down"
        // 通过交换 sort_order 实现
    }

    /**
     * 查询队列列表
     */
    public List<MessageQueue> listPending(Long sessionId) {
        return messageQueueMapper.selectList(
            new LambdaQueryWrapper<MessageQueue>()
                .eq(MessageQueue::getSessionId, sessionId)
                .eq(MessageQueue::getStatus, "PENDING")
                .orderByAsc(MessageQueue::getSortOrder)
        );
    }

    /**
     * 清空队列
     */
    public void clear(Long sessionId) {
        // 批量更新 status = DELETED
    }
}
```

### 5.4 修改 StreamingWsHandler

**新增事件处理**:

#### 5.4.1 enqueue_message(消息入队)

```java
case "enqueue_message":
    handleEnqueueMessage(sessionId, userId, data);
    break;
```

```java
private void handleEnqueueMessage(Long sessionId, Long userId, Map<String, Object> data) {
    String content = (String) data.get("content");
    String images = data.get("images") != null ? JsonUtils.toJson(data.get("images")) : null;

    MessageQueue item = messageQueueService.enqueue(sessionId, userId, content, images);

    // 通知前端队列更新
    List<MessageQueue> queue = messageQueueService.listPending(sessionId);
    registry.send(userId, WsEvent.of("queue_updated", sessionId, Map.of("queue", queue)));
}
```

#### 5.4.2 insert_message(消息插入 - 打断当前任务)

```java
case "insert_message":
    handleInsertMessage(sessionId, userId, data);
    break;
```

```java
private void handleInsertMessage(Long sessionId, Long userId, Map<String, Object> data) {
    Long queueId = Long.valueOf(data.get("queueId").toString());

    // 1. 设置取消标志,停止当前 Agent
    AtomicBoolean cancelFlag = cancelFlags.get(sessionId);
    if (cancelFlag != null) {
        cancelFlag.set(true);
    }

    // 2. 等待 Agent 停止(最多 10 秒)
    long deadline = System.currentTimeMillis() + 10_000;
    Session session = sessionService.getById(sessionId);
    while (System.currentTimeMillis() < deadline) {
        if (!"RUNNING".equals(session.getPhase()) && !"RESUMING".equals(session.getPhase())) {
            break;
        }
        Thread.sleep(200);
        session = sessionService.getById(sessionId);
    }

    // 3. 从队列中移除该消息
    MessageQueue item = messageQueueService.getById(queueId);
    messageQueueService.delete(queueId);

    // 4. 通知前端队列更新
    List<MessageQueue> queue = messageQueueService.listPending(sessionId);
    registry.send(userId, WsEvent.of("queue_updated", sessionId, Map.of("queue", queue)));

    // 5. 触发消息发送(复用 handleSendMessage 逻辑)
    Map<String, Object> sendData = new HashMap<>();
    sendData.put("content", item.getContent());
    sendData.put("images", item.getImages() != null ? JsonUtils.fromJson(item.getImages(), List.class) : null);
    sendData.put("eventId", UUID.randomUUID().toString());
    handleSendMessage(sessionId, userId, sendData);
}
```

#### 5.4.3 delete_queue_message(删除队列消息)

```java
case "delete_queue_message":
    handleDeleteQueueMessage(sessionId, userId, data);
    break;
```

```java
private void handleDeleteQueueMessage(Long sessionId, Long userId, Map<String, Object> data) {
    Long queueId = Long.valueOf(data.get("queueId").toString());
    messageQueueService.delete(queueId);

    List<MessageQueue> queue = messageQueueService.listPending(sessionId);
    registry.send(userId, WsEvent.of("queue_updated", sessionId, Map.of("queue", queue)));
}
```

#### 5.4.4 reorder_queue_message(调整队列顺序)

```java
case "reorder_queue_message":
    handleReorderQueueMessage(sessionId, userId, data);
    break;
```

```java
private void handleReorderQueueMessage(Long sessionId, Long userId, Map<String, Object> data) {
    Long queueId = Long.valueOf(data.get("queueId").toString());
    String direction = (String) data.get("direction"); // "up" / "down"
    messageQueueService.reorder(queueId, direction);

    List<MessageQueue> queue = messageQueueService.listPending(sessionId);
    registry.send(userId, WsEvent.of("queue_updated", sessionId, Map.of("queue", queue)));
}
```

### 5.5 修改 SessionService - 自动消费钩子

**文件**:`backend/src/main/java/cn/etarch/mao/session/service/SessionService.java`

在 `updatePhase()` 方法中,当 phase 变为终态(IDLE/COMPLETED/FAILED/CANCELLED)时,检查队列并自动消费:

```java
public void updatePhase(Long sessionId, String phase) {
    Session session = getById(sessionId);
    // ... 原有逻辑 ...

    // 更新 phase
    session.setPhase(phase);
    updateById(session);

    // 新增:自动消费队列
    if (isTerminalPhase(phase)) {
        autoConsumeQueue(sessionId);
    }
}

private boolean isTerminalPhase(String phase) {
    return "IDLE".equals(phase) || "COMPLETED".equals(phase)
        || "FAILED".equals(phase) || "CANCELLED".equals(phase);
}

private void autoConsumeQueue(Long sessionId) {
    MessageQueue head = messageQueueService.dequeue(sessionId);
    if (head != null) {
        // 异步触发消息发送
        // 需要获取 userId 和 registry,通过事件总线或直接调用
        // 这里需要将自动消费逻辑集成到 StreamingWsHandler 中
    }
}
```

**更好的方案**:将自动消费逻辑放在 `StreamingWsHandler` 的 agentExecutor 完成回调中,因为该位置已有 userId 和 registry 的引用。

```java
// StreamingWsHandler 中,agentExecutor 完成后
finally {
    cancelFlags.remove(sessionId);

    // 新增:自动消费队列
    MessageQueue head = messageQueueService.dequeue(sessionId);
    if (head != null) {
        // 通知前端队列更新
        List<MessageQueue> queue = messageQueueService.listPending(sessionId);
        registry.send(userId, WsEvent.of("queue_updated", sessionId, Map.of("queue", queue)));

        // 延迟一小段时间后触发下一条消息
        agentExecutor.submit(() -> {
            try {
                Thread.sleep(500); // 短暂延迟,确保前端状态同步
            } catch (InterruptedException ignored) {}

            Map<String, Object> sendData = new HashMap<>();
            sendData.put("content", head.getContent());
            sendData.put("images", head.getImages() != null ? JsonUtils.fromJson(head.getImages(), List.class) : null);
            sendData.put("eventId", UUID.randomUUID().toString());
            handleSendMessage(sessionId, userId, sendData);
        });
    }
}
```

### 5.6 修改 handleSendMessage - 移除忙碌校验

当前 `handleSendMessage()` 开头有校验:

```java
if ("RUNNING".equals(session.getPhase()) || "RESUMING".equals(session.getPhase())) {
    registry.send(userId, WsEvent.of("error", sessionId, Map.of("message", "Agent is currently running")));
    return;
}
```

这个校验保留不变。消息队列的入队通过 `enqueue_message` 处理,不走 `handleSendMessage`。自动消费和消息插入时调用 `handleSendMessage`,此时 Agent 已经停止,校验会通过。

### 5.7 Phase 扩展

当前 phase 枚举已足够,无需新增状态。`CANCELLING` 状态在前端维护,后端使用 `cancelFlag` 机制。

---

## 6. 前端设计

### 6.1 类型定义

**文件**:`desktop/src/types/chat.ts`

新增类型:

```typescript
export interface QueueMessage {
  id: string
  sessionId: string
  content: string
  images?: string[]
  sortOrder: number
  status: 'PENDING' | 'DELETED'
  createdAt: string
}
```

### 6.2 Session Store 扩展

**文件**:`desktop/src/stores/session.ts`

新增状态和方法:

```typescript
// 新增状态
const sessionQueueMessages = ref<Map<string, QueueMessage[]>>(new Map())

// 新增 computed
const activeQueueMessages = computed(() => {
  const sid = activeSessionId.value
  return sid ? sessionQueueMessages.value.get(sid) || [] : []
})

// 新增方法
function setQueueMessages(sessionId: string, queue: QueueMessage[]) {
  sessionQueueMessages.value.set(sessionId, queue)
}

function addToQueue(sessionId: string, msg: QueueMessage) {
  const list = sessionQueueMessages.value.get(sessionId) || []
  sessionQueueMessages.value.set(sessionId, [...list, msg])
}

function removeFromQueue(sessionId: string, queueId: string) {
  const list = sessionQueueMessages.value.get(sessionId) || []
  sessionQueueMessages.value.set(sessionId, list.filter(m => m.id !== queueId))
}

function clearQueue(sessionId: string) {
  sessionQueueMessages.value.delete(sessionId)
}

// 判断 Agent 是否忙碌(已有,这里列出供参考)
const isActive = computed(() => {
  const phase = activeSession.value?.phase
  return phase === 'RUNNING' || phase === 'WAITING_APPROVAL'
})
```

### 6.3 useChat 扩展

**文件**:`desktop/src/composables/useChat.ts`

新增方法:

```typescript
/**
 * 发送消息(带队列判断)
 * 如果 Agent 空闲,直接发送
 * 如果 Agent 忙碌,加入队列
 */
async function sendMessageWithQueue(text: string, files: File[]) {
  if (sessionStore.isActive) {
    // Agent 忙碌,加入队列
    await enqueueMessage(text, files)
  } else {
    // Agent 空闲,直接发送
    await sendMessage(text, files)
  }
}

/**
 * 消息入队
 */
async function enqueueMessage(text: string, files: File[]) {
  // 1. 上传图片(如果有)
  const imageUrls = files.length > 0 ? await uploadImages(files) : []

  // 2. 通过 WS 发送入队请求
  const eventId = crypto.randomUUID()
  wsEnqueueMessage(sessionId.value, text, eventId, imageUrls)
}

/**
 * 立即发送队列消息(打断当前任务)
 */
async function insertQueueMessage(queueId: string) {
  wsInsertMessage(sessionId.value, queueId)
}

/**
 * 删除队列消息
 */
async function deleteQueueMessage(queueId: string) {
  wsDeleteQueueMessage(sessionId.value, queueId)
}

/**
 * 调整队列顺序
 */
async function reorderQueueMessage(queueId: string, direction: 'up' | 'down') {
  wsReorderQueueMessage(sessionId.value, queueId, direction)
}
```

### 6.4 useStreamWS 扩展

**文件**:`desktop/src/composables/useStreamWS.ts`

新增发送方法:

```typescript
function wsEnqueueMessage(sessionId: string, content: string, eventId: string, images: string[]) {
  send({ type: 'enqueue_message', sessionId: Number(sessionId), data: { content, eventId, images } })
}

function wsInsertMessage(sessionId: string, queueId: string) {
  send({ type: 'insert_message', sessionId: Number(sessionId), data: { queueId } })
}

function wsDeleteQueueMessage(sessionId: string, queueId: string) {
  send({ type: 'delete_queue_message', sessionId: Number(sessionId), data: { queueId } })
}

function wsReorderQueueMessage(sessionId: string, queueId: string, direction: string) {
  send({ type: 'reorder_queue_message', sessionId: Number(sessionId), data: { queueId, direction } })
}
```

新增事件处理(在 `routeEvent` 中):

```typescript
case 'queue_updated':
  sessionStore.setQueueMessages(String(sessionId), data.queue)
  break
```

### 6.5 新增组件:QueuePanel

**文件**:`desktop/src/components/chat/QueuePanel.vue`

```vue
<template>
  <div v-if="queueMessages.length > 0" class="queue-panel">
    <div class="queue-header">
      <span class="queue-title">待发送消息 ({{ queueMessages.length }})</span>
      <el-button text size="small" @click="toggleExpand">
        {{ expanded ? '收起' : '展开' }}
      </el-button>
    </div>

    <Transition name="queue-expand">
      <div v-if="expanded || queueMessages.length <= 5" class="queue-list">
        <div
          v-for="(msg, index) in queueMessages"
          :key="msg.id"
          class="queue-item"
        >
          <div class="queue-item-content">
            <span class="queue-index">{{ index + 1 }}.</span>
            <span class="queue-text">{{ truncate(msg.content, 50) }}</span>
            <span v-if="msg.images?.length" class="queue-images">
              [{{ msg.images.length }}张图片]
            </span>
          </div>
          <div class="queue-item-actions">
            <el-button
              text size="small"
              :disabled="index === 0"
              @click="handleReorder(msg.id, 'up')"
            >
              <el-icon><Top /></el-icon>
            </el-button>
            <el-button
              text size="small"
              :disabled="index === queueMessages.length - 1"
              @click="handleReorder(msg.id, 'down')"
            >
              <el-icon><Bottom /></el-icon>
            </el-button>
            <el-button
              type="primary" text size="small"
              @click="handleInsert(msg.id)"
            >
              立即发送
            </el-button>
            <el-button
              type="danger" text size="small"
              @click="handleDelete(msg.id)"
            >
              <el-icon><Delete /></el-icon>
            </el-button>
          </div>
        </div>
      </div>
    </Transition>

    <!-- 折叠状态:只显示数量和最近一条 -->
    <div v-if="!expanded && queueMessages.length > 5" class="queue-collapsed">
      <span class="queue-text">{{ truncate(queueMessages[0].content, 30) }}...</span>
      <span class="queue-more">还有 {{ queueMessages.length - 1 }} 条</span>
    </div>
  </div>
</template>
```

**样式要点**:
- 位于输入框上方
- 背景色区分于消息列表
- 每条消息一行,右侧操作按钮
- 超过 5 条时折叠,显示第一条摘要 + "还有 N 条"

### 6.6 修改 ChatInput 组件

**文件**:`desktop/src/components/chat/ChatInput.vue`

修改发送逻辑:

```typescript
function handleSend() {
  if (!canSend.value) return

  // 修改:使用 sendMessageWithQueue 替代 emit('send')
  // 由 TaskView 层判断是直接发送还是入队
  emit('send', inputText.value.trim(), [...pendingFiles.value])

  inputText.value = ''
  // ... 清理逻辑不变
}
```

### 6.7 修改 TaskView

**文件**:`desktop/src/views/task/TaskView.vue`

修改 `handleSend` 方法:

```typescript
function handleSend(text: string, files: File[]) {
  // 修改:使用带队列判断的发送方法
  sendMessageWithQueue(text, files)
  nextTick(scrollToBottomSmooth)
}
```

在模板中添加 QueuePanel:

```vue
<template>
  <div class="task-view">
    <!-- 消息列表 -->
    <MessageList ... />

    <!-- 新增:队列面板 -->
    <QueuePanel />

    <!-- 输入框 -->
    <ChatInput ... />
  </div>
</template>
```

### 6.8 防并发处理

在 `QueuePanel` 组件中,对"立即发送"按钮做防抖:

```typescript
const inserting = ref(false)

async function handleInsert(queueId: string) {
  if (inserting.value) return
  inserting.value = true

  try {
    await insertQueueMessage(queueId)
  } finally {
    // 延迟重置,避免连续点击
    setTimeout(() => { inserting.value = false }, 500)
  }
}
```

---

## 7. WebSocket 事件协议

### 7.1 客户端 → 服务端

| type | 说明 | data 字段 |
|------|------|-----------|
| `enqueue_message` | 消息入队 | `sessionId`, `content`, `images`, `eventId` |
| `insert_message` | 立即发送(打断) | `sessionId`, `queueId` |
| `delete_queue_message` | 删除队列消息 | `sessionId`, `queueId` |
| `reorder_queue_message` | 调整顺序 | `sessionId`, `queueId`, `direction` |

### 7.2 服务端 → 客户端

| type | 说明 | data 字段 |
|------|------|-----------|
| `queue_updated` | 队列变更通知 | `sessionId`, `queue: [QueueMessage]` |

---

## 8. 边界情况处理

### 8.1 并发插入

**场景**:用户快速连续点击两条消息的"立即发送"。

**处理**:
- 前端防抖:500ms 内只允许一次插入操作
- 后端保证:第一条插入会先停止 Agent,第二条在第一条完成后执行
- 两条消息都不会丢失,按顺序追加到消息列表

### 8.2 队列消息发送失败

**场景**:队列消息发出后,Agent 执行失败。

**处理**:
- 消息已在发送时从队列移除
- Agent 执行失败不影响队列状态
- 失败结果显示在对话窗口

### 8.3 页面刷新

**场景**:用户刷新页面,队列消息需要恢复。

**处理**:
- 队列消息持久化在数据库中
- 前端切换 session 时,通过 REST API 或 WS 订阅获取队列列表
- 新增 REST 接口:`GET /api/v1/sessions/{id}/queue`

### 8.4 Session 切换

**场景**:用户在 Agent 执行时切换到其他 session。

**处理**:
- 队列按 sessionId 隔离,切换 session 自动切换队列
- 切换回来时重新加载队列

### 8.5 Agent 空闲时收到队列消费请求

**场景**:自动消费时,Agent 已经被其他操作启动。

**处理**:
- 在触发 handleSendMessage 前,再次检查 session phase
- 若 Agent 已在运行,将消息重新入队(不消费)

### 8.6 被打断任务的上下文保留

**场景**:Agent 执行到一半被打断。

**处理**:
- AgentLoop 检测到 cancelFlag 后退出循环
- 已执行的 assistant 消息和 tool 调用结果已通过 persistenceCallback 持久化
- 前端收到 CANCELLED 状态后,将当前 assistant 消息标记为完成
- 新消息发送时,buildContext() 会从 DB 加载完整历史(包含被打断的部分)

### 8.7 图片消息入队

**场景**:用户发送带图片的消息,Agent 忙碌时入队。

**处理**:
- 入队前先上传图片到 OSS
- 队列中存储图片 URL 数组
- 消费时直接使用 URL,无需重新上传

---

## 9. REST API 补充

### 9.1 获取队列列表

```
GET /api/v1/sessions/{sessionId}/queue
```

**响应**:
```json
{
  "code": 0,
  "data": [
    {
      "id": 1,
      "sessionId": 100,
      "content": "帮我写一个函数",
      "images": null,
      "sortOrder": 1,
      "createdAt": "2024-01-01T12:00:00"
    }
  ]
}
```

**用途**:页面刷新后恢复队列展示。

---

## 10. 实现计划

按模块划分,每个模块包含完整的前后端实现,模块间无依赖关系,可按任意顺序实现。

### 模块 A:数据层与基础服务

**涉及文件**:
- 新增:`V032__add_message_queue.sql`
- 新增:`MessageQueue.java`、`MessageQueueMapper.java`、`MessageQueueService.java`
- 新增:`GET /api/v1/sessions/{id}/queue` REST 接口

**实现内容**:
1. 数据库迁移脚本
2. 实体类与 Mapper
3. MessageQueueService 核心方法(enqueue、dequeue、delete、reorder、listPending、clear)
4. REST 接口(页面刷新后恢复队列)

**完成标志**:可通过 REST 接口完成队列的增删查改操作。

---

### 模块 B:WebSocket 协议与后端逻辑

**涉及文件**:
- 修改:`StreamingWsHandler.java`

**实现内容**:
1. 新增事件处理:`enqueue_message`、`insert_message`、`delete_queue_message`、`reorder_queue_message`
2. 自动消费逻辑(在 agentExecutor 完成回调中,检查队列并触发下一条消息)
3. 消息插入逻辑(设置 cancelFlag → 等待 Agent 停止 → 从队列移除 → 触发 handleSendMessage)
4. 发送 `queue_updated` 事件通知前端

**完成标志**:通过 WebSocket 客户端工具可完成队列操作,自动消费和插入打断流程正常。

---

### 模块 C:前端状态与通信层

**涉及文件**:
- 修改:`desktop/src/types/chat.ts`
- 修改:`desktop/src/stores/session.ts`
- 修改:`desktop/src/composables/useChat.ts`
- 修改:`desktop/src/composables/useStreamWS.ts`

**实现内容**:
1. `QueueMessage` 类型定义
2. sessionStore 新增队列状态(`sessionQueueMessages`、`activeQueueMessages`)和方法(`setQueueMessages`、`clearQueue`)
3. useChat 新增 `sendMessageWithQueue`(带队列判断)、`enqueueMessage`、`insertQueueMessage`、`deleteQueueMessage`、`reorderQueueMessage`
4. useStreamWS 新增 WS 发送方法和 `queue_updated` 事件处理

**完成标志**:前端可通过 composable 方法完成队列操作,Store 状态正确更新。

---

### 模块 D:前端 UI 组件

**涉及文件**:
- 新增:`desktop/src/components/chat/QueuePanel.vue`
- 修改:`desktop/src/views/task/TaskView.vue`
- 修改:`desktop/src/components/chat/ChatInput.vue`

**实现内容**:
1. QueuePanel 组件(队列列表、上下排序、删除、立即发送、折叠/展开)
2. TaskView 集成 QueuePanel(位于输入框上方)
3. ChatInput 发送逻辑修改(使用 `sendMessageWithQueue` 替代直接 `emit('send')`)
4. 防并发处理("立即发送"按钮 500ms 防抖)

**完成标志**:UI 完整展示,所有交互可操作。

---

### 模块 E:端到端联调

**涉及文件**:无新增,验证所有模块协同工作

**验证项**:
1. Agent 空闲时发送消息 → 直接执行(不进队列)
2. Agent 忙碌时发送消息 → 进入队列 → 队列面板显示
3. Agent 完成 → 自动消费队列头部消息
4. 点击"立即发送" → 打断当前 Agent → 执行选中消息
5. 队列排序(上下箭头)→ 顺序正确
6. 队列删除 → 消息移除
7. 超过 5 条 → 折叠展示
8. 页面刷新 → 队列恢复
9. 被打断任务上下文保留在对话窗口
10. 快速连续点击"立即发送" → 防并发生效,消息不丢失

---

## 11. 附录:关键文件索引

| 文件 | 说明 |
|------|------|
| `backend/.../session/entity/MessageQueue.java` | 新增:队列消息实体 |
| `backend/.../session/mapper/MessageQueueMapper.java` | 新增:队列 Mapper |
| `backend/.../session/service/MessageQueueService.java` | 新增:队列服务 |
| `backend/.../session/service/SessionService.java` | 修改:updatePhase 增加自动消费钩子 |
| `backend/.../session/ws/StreamingWsHandler.java` | 修改:新增事件处理 |
| `backend/.../resources/db/migration/V032__add_message_queue.sql` | 新增:迁移脚本 |
| `desktop/src/types/chat.ts` | 修改:新增 QueueMessage 类型 |
| `desktop/src/stores/session.ts` | 修改:新增队列状态管理 |
| `desktop/src/composables/useChat.ts` | 修改:新增队列相关方法 |
| `desktop/src/composables/useStreamWS.ts` | 修改:新增 WS 事件处理 |
| `desktop/src/components/chat/QueuePanel.vue` | 新增:队列面板组件 |
| `desktop/src/views/task/TaskView.vue` | 修改:集成 QueuePanel |
| `desktop/src/components/chat/ChatInput.vue` | 修改:发送逻辑调整 |