# ScriptView 统一对话页改造实现计划

> **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:** 把 ScriptView 心愿实现页的澄清/大纲/小说生成从独立视图切换改为统一对话流，用户提交心愿后始终停留在同一对话页面

**Architecture:** viewState 简化为 home+chat；扩展 resultMessages 消息模型支持 kind: text/card/outline/novel + 交互态字段；handleShortNovelEvent 把 SSE 事件转为对话消息追加；卡片/大纲消息内嵌交互按钮，用户操作后转 user 消息并调 followupStream；novel_done 后底部输入栏按 generationPhase 切换到旧接口 streamScriptChat 做自由对话修改

**Tech Stack:** UniApp + Vue 3 Composition API（`<script setup>`），shortNovel.js（新接口 SSE），scriptChat.js（旧接口流式对话），H5 热加载验证

---

## 文件结构

### 修改文件
- `mini-program/src/pages/main/ScriptView.vue` — 核心改造文件（约 3300 行）
  - 扩展 `addResultMessage` 消息模型
  - 新增 `generationPhase` 状态
  - 改造 `handleShortNovelEvent` 为对话流追加消息
  - 改造 `runGeneration`/`submitClarification`/`confirmOutline`/`modifyOutline`/`resumeSession` 操作对话消息
  - 改造 `sendChat` 按 generationPhase 切换接口
  - 新增统一 chat 视图模板，删除 clarifying/outlining/novel-generating 独立视图

### 不变文件
- `mini-program/src/components/ClarificationCard.vue` — 复用，由父组件控制 submitted 态
- `mini-program/src/services/shortNovel.js` — 新接口不变
- `mini-program/src/services/scriptChat.js` — 旧接口不变

### 验证方式
本项目无前端单元测试框架，采用 **H5 热加载 + Playwright 浏览器验证**。每个任务结束后在浏览器确认页面行为。

---

## Task 1: 扩展消息模型 + 新增 generationPhase 状态

**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`（addResultMessage 函数约 1283 行，新增 generationPhase ref 约 350 行附近）

- [ ] **Step 1: 扩展 addResultMessage 支持 card/outline/交互态字段**

找到 `addResultMessage` 函数（约 1283 行），当前实现：

```javascript
const addResultMessage = ({ role, content, pending = false, kind = '' }) => {
  const message = {
    id: createMessageId(),
    role,
    kind,
    content,
    pending,
    time: formatMessageTime()
  }
  resultMessages.value.push(message)
  if (!pending) persistResultMessages()
  keepResultAtBottom()
  return message
}
```

替换为（新增 card/outline/submitted/confirmed/failed/answer/feedback 字段）：

```javascript
const addResultMessage = ({ role, content, pending = false, kind = 'text', card = null, outline = null, answer = '', feedback = '', failed = false }) => {
  const message = {
    id: createMessageId(),
    role,
    kind,
    content,
    pending,
    time: formatMessageTime(),
    card,
    outline,
    answer,
    feedback,
    failed,
    submitted: false,
    confirmed: false
  }
  resultMessages.value.push(message)
  if (!pending) persistResultMessages()
  keepResultAtBottom()
  return message
}
```

- [ ] **Step 2: 新增 generationPhase ref**

找到 `const resumeableSessionId = ref('')`（约 350 行），在其后新增一行：

```javascript
const resumeableSessionId = ref('')
const generationPhase = ref('idle')
```

`generationPhase` 取值：`'idle'`（未开始）/`'generating'`（生成流程中，用新接口）/`'done'`（novel_done 后，用旧接口）。

- [ ] **Step 3: 验证热加载无编译错误**

H5 服务在后台运行（端口 5180），保存文件后 Vite 自动热加载。打开浏览器 http://localhost:5180 确认页面无白屏、Console 无语法错误。

- [ ] **Step 4: 提交**

```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor(mini-program): 扩展 addResultMessage 消息模型 + 新增 generationPhase"
```

---

## Task 2: 改造 handleShortNovelEvent 为对话流追加消息

**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`（handleShortNovelEvent 函数约 1682 行）

- [ ] **Step 1: 改造 handleShortNovelEvent**

找到 `handleShortNovelEvent` 函数（约 1682 行），当前实现是切换 viewState。替换为往 resultMessages 追加消息：

```javascript
const handleShortNovelEvent = (event) => {
  const { type, session_id, payload = {} } = event
  if (session_id) novelSessionId.value = session_id

  switch (type) {
    case 'status':
      // 仅更新加载提示，不追加独立消息
      generationStatus.value = payload.stage || 'processing'
      break
    case 'clarification_card':
      addResultMessage({ role: 'assistant', kind: 'card', card: payload.card || null })
      generationStatus.value = 'idle'
      break
    case 'outline_created':
      addResultMessage({ role: 'assistant', kind: 'outline', outline: payload.outline || null })
      generationStatus.value = 'idle'
      break
    case 'novel_start':
      addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true })
      generationStatus.value = 'streaming'
      break
    case 'novel_delta': {
      // 往最后一条 novel 消息追加 delta
      const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending)
      if (lastNovel) {
        lastNovel.content += payload.delta || ''
      }
      keepResultAtBottom()
      break
    }
    case 'novel_done': {
      // 标记最后一条 novel 消息完成
      const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel')
      if (lastNovel) {
        if (payload.full_text) lastNovel.content = payload.full_text
        lastNovel.pending = false
      }
      scriptId.value = payload.scriptId || ''
      conversationId.value = payload.conversationId || ''
      currentVersionMessageId.value = payload.currentVersionMessageId || ''
      generationPhase.value = 'done'
      generationStatus.value = 'idle'
      persistResultMessages()
      break
    }
    case 'error':
      if (payload.code === 'DAILY_GENERATION_IN_PROGRESS' && session_id) {
        resumeableSessionId.value = session_id
        markGenerationFailed(payload.message || '今天已有未完成的创作，可点击继续创作恢复')
      } else {
        markGenerationFailed(payload.message || '生成失败')
        addResultMessage({ role: 'assistant', kind: 'text', content: payload.message || '生成失败', failed: true })
      }
      break
  }
}
```

关键变化：
- 不再切换 `viewState`（始终保持 `chat`）
- `clarification_card` → 追加 kind:'card' 消息
- `outline_created` → 追加 kind:'outline' 消息
- `novel_start` → 追加 pending 的 kind:'novel' 消息
- `novel_delta` → 往最后一条 pending novel 消息追加文本
- `novel_done` → 标记 novel 消息完成，设置 `generationPhase='done'`

- [ ] **Step 2: 验证热加载无错误**

保存后浏览器确认无 Console 错误（此时 UI 还未改造，但逻辑应无语法错误）。

- [ ] **Step 3: 提交**

```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor(mini-program): handleShortNovelEvent 改为对话流追加消息"
```

---

## Task 3: 改造 runGeneration 和交互函数

**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`（runGeneration 约 1626 行，submitClarification/confirmOutline/modifyOutline/resumeSession 约 1727 行）

- [ ] **Step 1: 改造 runGeneration 进入 chat 视图并追加用户消息**

找到 `runGeneration` 函数（约 1626 行）。当前在函数中段设置 `viewState.value = 'generating'` 和 `generationDisplayText.value = display`。

把 `viewState.value = 'generating'` 改为 `viewState.value = 'chat'`，并在 `generationDisplayText.value = display` 后新增追加用户消息的逻辑：

找到这一段：
```javascript
  streamWriter.reset()
  startGenerationFeedback()
  ttsPlayer.reset()
  viewState.value = 'generating'
  keepGenerationAtBottom()
```

替换为：
```javascript
  streamWriter.reset()
  startGenerationFeedback()
  ttsPlayer.reset()
  viewState.value = 'chat'
  generationPhase.value = 'generating'
  resultMessages.value = []
  addResultMessage({ role: 'user', kind: 'text', content: display })
  keepResultAtBottom()
```

说明：进入 chat 视图，清空旧消息，把用户心愿作为第一条 user 消息追加。

- [ ] **Step 2: 改造 submitClarification 接收消息对象**

找到 `submitClarification` 函数（约 1727 行），当前签名 `const submitClarification = (answer) =>`。替换为接收消息对象 + answer：

```javascript
const submitClarification = (msg, answer) => {
  if (answeringClarification.value) return
  if (msg) {
    msg.submitted = true
    msg.answer = answer
  }
  addResultMessage({ role: 'user', kind: 'text', content: answer })
  answeringClarification.value = true
  currentStreamTask.value = followupStream({
    sessionId: novelSessionId.value,
    action: 'answer_clarification',
    payload: { answer },
    onEvent: handleShortNovelEvent,
    onError: (msg) => markGenerationFailed(msg)
  })
  setTimeout(() => { answeringClarification.value = false }, 200)
}
```

- [ ] **Step 3: 改造 confirmOutline 接收消息对象**

找到 `confirmOutline` 函数（约 1740 行），替换为：

```javascript
const confirmOutline = (msg) => {
  if (msg) msg.confirmed = true
  addResultMessage({ role: 'user', kind: 'text', content: '确认大纲' })
  currentStreamTask.value = followupStream({
    sessionId: novelSessionId.value,
    action: 'confirm_outline',
    payload: null,
    onEvent: handleShortNovelEvent,
    onError: (msg) => markGenerationFailed(msg)
  })
}
```

- [ ] **Step 4: 改造 modifyOutline 接收消息对象**

找到 `modifyOutline` 函数（约 1750 行），替换为：

```javascript
const modifyOutline = (msg) => {
  const feedback = msg?.feedback || ''
  if (!feedback.trim()) {
    uni.showToast({ title: '请先填写修改意见', icon: 'none' })
    return
  }
  if (msg) msg.confirmed = true
  addResultMessage({ role: 'user', kind: 'text', content: feedback })
  currentStreamTask.value = followupStream({
    sessionId: novelSessionId.value,
    action: 'modify_outline',
    payload: { feedback },
    onEvent: handleShortNovelEvent,
    onError: (msg) => markGenerationFailed(msg)
  })
}
```

- [ ] **Step 5: 改造 resumeSession 在对话流内恢复**

找到 `resumeSession` 函数（约 1761 行），替换为：

```javascript
const resumeSession = () => {
  if (!resumeableSessionId.value || generating.value) return
  novelSessionId.value = resumeableSessionId.value
  resumeableSessionId.value = ''
  generating.value = true
  generationPhase.value = 'generating'
  generationStatus.value = 'waiting'
  generationError.value = ''
  addResultMessage({ role: 'user', kind: 'text', content: '继续之前的创作' })
  streamWriter.reset()
  startGenerationFeedback()
  keepResultAtBottom()

  try {
    currentStreamTask.value = followupStream({
      sessionId: novelSessionId.value,
      action: 'retry',
      payload: null,
      onEvent: handleShortNovelEvent,
      onError: (msg) => {
        markGenerationFailed(msg)
        analytics.track('script_generate_fail', {
          source: 'resume',
          error: msg
        }, { eventType: 'script', pagePath })
      }
    })
  } catch (error) {
    markGenerationFailed(error.message || '恢复失败')
  } finally {
    generating.value = false
  }
}
```

关键变化：不再切换 viewState（保持 chat），改为追加 user 消息"继续之前的创作"。

- [ ] **Step 6: 验证热加载无错误**

保存后浏览器确认无 Console 错误。

- [ ] **Step 7: 提交**

```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor(mini-program): runGeneration+交互函数改为操作对话消息"
```

---

## Task 4: 新增统一 chat 视图模板 + 删除旧独立视图

**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`（template 部分，约 89-183 行的 clarifying/outlining 块 + 139-183 的 generating 块 + 185-302 的 result 块）

- [ ] **Step 1: 删除 clarifying 和 outlining 独立视图块**

找到 `clarifying` 视图块（约 89 行 `<view v-else-if="viewState === 'clarifying'"` 到 102 行 `</view>`）和 `outlining` 视图块（约 104 行 `<view v-else-if="viewState === 'outlining'"` 到 137 行 `</view>`），**整块删除**。

删除这两个块后，template 里不再有 viewState==='clarifying' 和 'outlining' 的分支。

- [ ] **Step 2: 改造 generating 视图为 chat 视图**

找到 generating 视图块（约 139 行 `<view v-else-if="viewState === 'generating"`，但由于 Step 1 删除了前面两个块，行号会前移）。把整个 generating 块替换为统一 chat 视图：

把 `<view v-else-if="viewState === 'generating'" class="generation-view">` 整块（到对应的 `</view>` 结束）替换为：

```vue
    <view v-else-if="viewState === 'chat'" class="chat-page">
      <view class="chat-top-actions">
        <view class="history-button" @click="openScriptLibrary">
          <view class="history-lines">
            <view></view>
            <view></view>
            <view></view>
          </view>
          <text>历史</text>
        </view>
        <button class="page-close-btn" @click="closeResult">×</button>
      </view>

      <scroll-view
        class="chat-scroll"
        scroll-y
        :scroll-top="resultCommandScrollTop"
        :scroll-into-view="resultScrollTarget"
        :scroll-with-animation="true"
        :enhanced="true"
        :show-scrollbar="false"
        @scroll="handleResultScroll"
      >
        <view class="chat-scroll-content">
          <view id="result-scroll-top" class="result-scroll-top"></view>

          <view class="conversation compact">
            <view
              v-for="msg in resultMessages"
              :key="msg.id"
            >
              <!-- user text -->
              <view v-if="msg.role === 'user' && msg.kind === 'text'" class="chat-bubble user">
                <text>{{ msg.content }}</text>
                <text class="bubble-time">{{ msg.time }}</text>
              </view>

              <!-- assistant card -->
              <view v-else-if="msg.kind === 'card'" class="chat-bubble system">
                <ClarificationCard
                  v-if="!msg.submitted && msg.card"
                  :card="msg.card"
                  @submit="(answer) => submitClarification(msg, answer)"
                />
                <view v-else class="card-answered">
                  <text>已回答：{{ msg.answer }}</text>
                </view>
              </view>

              <!-- assistant outline -->
              <view v-else-if="msg.kind === 'outline'" class="chat-bubble system">
                <view v-if="msg.outline">
                  <view v-if="msg.outline.title" class="outline-title">
                    <text>{{ msg.outline.title }}</text>
                  </view>
                  <view v-if="msg.outline.logline" class="outline-logline">
                    <text>{{ msg.outline.logline }}</text>
                  </view>
                  <view v-if="Array.isArray(msg.outline.beats)" class="outline-beats">
                    <view v-for="(beat, idx) in msg.outline.beats" :key="idx" class="beat-item">
                      <text class="beat-order">{{ beat.order || idx + 1 }}</text>
                      <view class="beat-content">
                        <text class="beat-title">{{ beat.title || '' }}</text>
                        <text v-if="beat.summary" class="beat-summary">{{ beat.summary }}</text>
                      </view>
                    </view>
                  </view>
                  <view v-if="msg.outline.ending" class="outline-ending">
                    <text class="ending-label">结局</text>
                    <text class="ending-text">{{ msg.outline.ending }}</text>
                  </view>
                </view>
                <view v-if="!msg.confirmed" class="outline-actions">
                  <input v-model="msg.feedback" placeholder="如需修改请输入意见" class="outline-feedback" />
                  <view class="outline-buttons">
                    <view class="btn-secondary" @click="modifyOutline(msg)">修改大纲</view>
                    <view class="btn-primary" @click="confirmOutline(msg)">确认大纲</view>
                  </view>
                </view>
              </view>

              <!-- assistant novel -->
              <view v-else-if="msg.kind === 'novel'" class="chat-bubble system">
                <text class="novel-text">{{ msg.content }}<text v-if="msg.pending" class="typing-cursor">|</text></text>
              </view>

              <!-- assistant text (status/error) -->
              <view v-else class="chat-bubble system">
                <text :class="{ 'generation-error': msg.failed }">{{ msg.content }}</text>
                <text class="bubble-time">{{ msg.time }}</text>
              </view>
            </view>

            <!-- 底部 loading -->
            <view v-if="generating && generationStatus !== 'failed'" class="chat-loading">
              <view class="loading-orbit" :class="{ streaming: generationStatus === 'streaming' }">
                <view class="orbit-ring outer"></view>
                <view class="orbit-ring inner"></view>
                <view class="orbit-core"></view>
              </view>
              <text class="loading-copy">{{ generationCopy }}</text>
            </view>

            <!-- 失败继续创作入口 -->
            <view v-if="generationStatus === 'failed' && resumeableSessionId" class="resume-action">
              <button class="generation-action primary" @click="resumeSession">继续创作</button>
            </view>

            <view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
          </view>
        </view>
      </scroll-view>

      <!-- 底部输入栏：novel_done 后启用 -->
      <view v-if="generationPhase === 'done'" class="chat-input-bar">
        <view class="result-input-shell">
          <textarea
            class="result-chat-input"
            v-model="chatInput"
            :auto-height="true"
            :show-confirm-bar="false"
            maxlength="500"
            confirm-type="send"
            placeholder="输入修改建议，或让 AI 继续生成"
            @confirm="sendChat"
          />
        </view>
        <view class="chat-send-btn" :class="{ disabled: !chatInput.trim() || generating }" @click="sendChat">发送</view>
      </view>
    </view>
```

- [ ] **Step 3: 删除独立的 result 视图块**

找到原 result 视图块（`<view v-else class="result-page">`，约 185 行到 302 行）。由于 chat 视图已包含所有对话功能，**整块删除**原 result-page 块。

删除后 template 的 v-if/v-else-if 链为：`home` → `chat`。

- [ ] **Step 4: 新增 chat 视图相关样式**

在 `<style scoped>` 部分末尾添加 chat 视图样式（复用现有 result/generation 样式，新增缺失部分）：

```scss
.chat-page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: #13091f;
}

.chat-top-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 24rpx 32rpx;
}

.chat-scroll {
  flex: 1;
  height: calc(100vh - 180rpx);
}

.chat-scroll-content {
  padding: 0 24rpx 40rpx;
}

.card-answered {
  color: rgba(255, 255, 255, 0.7);
  font-size: 26rpx;
  padding: 16rpx 0;
}

.outline-title {
  color: #ffffff;
  font-size: 40rpx;
  font-weight: 700;
  margin-bottom: 16rpx;
}

.outline-logline {
  color: rgba(255, 255, 255, 0.85);
  font-size: 28rpx;
  line-height: 1.6;
  margin-bottom: 24rpx;
}

.outline-beats {
  display: flex;
  flex-direction: column;
  gap: 20rpx;
  margin-bottom: 24rpx;
}

.beat-item {
  display: flex;
  gap: 20rpx;
  align-items: flex-start;
}

.beat-order {
  flex-shrink: 0;
  width: 56rpx;
  height: 56rpx;
  background: #087e8b;
  border-radius: 50%;
  color: #ffffff;
  font-size: 28rpx;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}

.beat-content {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.beat-title {
  color: #ffffff;
  font-size: 28rpx;
  font-weight: 600;
}

.beat-summary {
  color: rgba(255, 255, 255, 0.7);
  font-size: 26rpx;
  margin-top: 8rpx;
  line-height: 1.5;
}

.outline-ending {
  margin-top: 24rpx;
  padding-top: 24rpx;
  border-top: 2rpx solid rgba(255, 255, 255, 0.1);
}

.ending-label {
  color: rgba(255, 255, 255, 0.6);
  font-size: 24rpx;
  display: block;
  margin-bottom: 8rpx;
}

.ending-text {
  color: #ffffff;
  font-size: 28rpx;
  line-height: 1.6;
}

.outline-actions {
  margin-top: 24rpx;
}

.outline-feedback {
  width: 100%;
  padding: 20rpx;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 12rpx;
  color: #ffffff;
  font-size: 28rpx;
  margin-bottom: 16rpx;
  box-sizing: border-box;
}

.outline-buttons {
  display: flex;
  gap: 16rpx;
  justify-content: flex-end;
}

.novel-text {
  color: #ffffff;
  font-size: 28rpx;
  line-height: 1.8;
  white-space: pre-wrap;
}

.typing-cursor {
  color: #087e8b;
  animation: blink 900ms steps(1) infinite;
}

.chat-loading {
  display: flex;
  align-items: center;
  gap: 16rpx;
  padding: 24rpx 0;
}

.resume-action {
  padding: 24rpx 0;
  display: flex;
  justify-content: center;
}

.chat-input-bar {
  display: flex;
  align-items: flex-end;
  gap: 16rpx;
  padding: 16rpx 24rpx;
  background: rgba(255, 255, 255, 0.04);
  border-top: 2rpx solid rgba(255, 255, 255, 0.1);
}

.result-input-shell {
  flex: 1;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 12rpx;
  padding: 8rpx 16rpx;
}

.result-chat-input {
  width: 100%;
  min-height: 60rpx;
  padding: 12rpx 0;
  color: #ffffff;
  font-size: 28rpx;
}

.chat-send-btn {
  padding: 16rpx 32rpx;
  background: #087e8b;
  border-radius: 32rpx;
  color: #ffffff;
  font-size: 28rpx;
}

.chat-send-btn.disabled {
  opacity: 0.5;
}

@keyframes blink {
  50% { opacity: 0; }
}
```

- [ ] **Step 5: 验证热加载渲染**

保存后浏览器打开 http://localhost:5180，进入心愿页输入心愿发送，确认：
- 页面进入 chat 视图（对话页）
- 用户心愿作为气泡显示
- 澄清卡片作为 AI 消息出现在对话流
- Console 无错误

- [ ] **Step 6: 提交**

```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat(mini-program): 统一 chat 视图，删除 clarifying/outlining/result 独立视图"
```

---

## Task 5: 改造 sendChat 按 generationPhase 切换接口

**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`（sendChat 函数约 701 行）

- [ ] **Step 1: 改造 sendChat 根据 generationPhase 切换接口**

找到 `sendChat` 函数（约 701 行）。当前实现用旧接口 `streamScriptChat` + `createMessage`。

在函数开头插入 generationPhase 判断：如果是 `'generating'`（生成流程中，理论上 novel_done 前底部输入栏不显示，但防御性处理），直接 return；如果是 `'done'`，走原有旧接口逻辑。

把 `sendChat` 函数替换为：

```javascript
const sendChat = async () => {
  if (!chatInput.value.trim() || generating.value) return
  const content = chatInput.value.trim()
  chatInput.value = ''

  // novel_done 前底部输入栏不显示，此处只处理 done 态的后续对话
  if (generationPhase.value !== 'done') return

  const messageId = currentVersionMessageId.value

  const userMessageRes = await createMessage({
    conversationId: conversationId.value,
    userId: getCurrentUserId(),
    content,
    senderType: 'user',
    contentType: 'chat'
  })
  const userMessageId = userMessageRes.data?.id

  resultMessages.value.push({
    id: userMessageId || createMessageId(),
    role: 'user',
    kind: 'text',
    content,
    pending: false,
    time: formatMessageTime(),
    submitted: false,
    confirmed: false
  })
  keepResultAtBottom()

  generating.value = true
  generationStatus.value = 'streaming'
  await streamScriptChat({
    operationType: 'chat',
    conversationId: conversationId.value,
    messageId,
    userMessageId,
    content,
    scriptId: scriptId.value,
    onDelta: (delta) => {
      // 往最后一条 AI 消息追加（如有）
      const lastAssistant = [...resultMessages.value].reverse().find(m => m.role === 'assistant' && m.kind === 'novel')
      if (lastAssistant) {
        lastAssistant.content += delta
      }
    },
    onDone: () => {
      generating.value = false
      generationStatus.value = 'idle'
      // 重新加载持久化消息以获取最新版本
      loadMessages()
    },
    onError: (error) => {
      generating.value = false
      generationStatus.value = 'idle'
      uni.showToast({ title: error || '生成失败', icon: 'none' })
    }
  })
}
```

关键变化：
- 只在 `generationPhase === 'done'` 时处理（底部输入栏此时才显示）
- 用户消息追加到 `resultMessages`（统一对话流）
- `onDelta` 往最后一条 AI 消息追加文本

- [ ] **Step 2: 验证热加载无错误**

保存后浏览器确认无 Console 错误。

- [ ] **Step 3: 提交**

```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat(mini-program): sendChat 按 generationPhase 切换接口，消息进统一对话流"
```

---

## Task 6: 端到端浏览器验证

**Files:**
- 无代码改动，纯浏览器验证

- [ ] **Step 1: 确认 H5 服务运行**

Run: `python dev-services.py status`
Expected: mini-program 端口 5180 运行中

若未运行，启动：`cd mini-program && npm run dev:h5 -- --host 127.0.0.1 --port 5180`（后台运行）

- [ ] **Step 2: Playwright 验证完整对话流程**

使用 Playwright MCP 打开 http://localhost:5180：

1. 登录（手机号 13800138000 + 验证码 123456）
2. 进入心愿页，输入"今天我想当 CEO"，发送
3. **验证**：进入 chat 对话页，用户心愿作为气泡显示在对话流
4. **验证**：澄清卡片作为 AI 消息出现在对话流（不在独立页面）
5. 选择选项，提交
6. **验证**：卡片变为"已回答"态，user 消息追加，下一轮澄清或大纲出现
7. 若进入大纲：**验证**大纲作为 AI 消息出现（带确认/修改按钮）
8. 确认大纲
9. **验证**：小说流式生成作为一条 AI 消息逐字出现
10. novel_done 后：**验证**底部输入栏出现，可输入修改建议

- [ ] **Step 3: 验证继续创作（DAILY_GENERATION_IN_PROGRESS）**

若触发每日限制：
1. **验证**：对话流内显示失败提示 + "继续创作"按钮
2. 点击"继续创作"
3. **验证**：恢复会话，新澄清/大纲作为消息追加到对话流

- [ ] **Step 4: 检查 Console 和 Network**

- Console：无业务错误（`uni.getRecorderManager not supported` 平台限制可忽略）
- Network：`/shortNovel/stream` 和 `/shortNovel/followup` 返回 SSE 事件流，无 4xx/5xx

- [ ] **Step 5: 验收报告**

输出验收报告：每个步骤通过/失败 + 截图证据 + 最终结论

---

## 验收标准

- [ ] 提交心愿后进入 chat 对话页，用户心愿作为气泡显示
- [ ] 澄清卡片作为 AI 消息出现在对话流（非独立页面）
- [ ] 卡片提交后变"已回答"态，user 消息追加
- [ ] 大纲作为 AI 消息出现，确认/修改后按钮消失
- [ ] 小说流式生成作为一条 AI 消息逐字出现
- [ ] novel_done 后底部输入栏出现，继续修改用旧接口工作
- [ ] 失败时对话流内显示"继续创作"入口
- [ ] 整个过程不切换视图，用户始终在同一对话页
