Files
happy-life-star/docs/superpowers/plans/2026-07-19-scriptview-unified-chat.md
T
peanut 25d8a9f8f2 docs:ScriptView 统一对话页改造实现计划
6 个任务:扩展消息模型、handleShortNovelEvent 改对话流、
runGeneration+交互函数改造、统一 chat 视图模板、
sendChat 接口切换、端到端浏览器验证
2026-07-19 14:55:27 +08:00

27 KiB
Raw Blame History

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 消息并调 followupStreamnovel_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.vueaddResultMessage 函数约 1283 行,新增 generationPhase ref 约 350 行附近)

  • Step 1: 扩展 addResultMessage 支持 card/outline/交互态字段

找到 addResultMessage 函数(约 1283 行),当前实现:

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 字段):

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 行),在其后新增一行:

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

generationPhase 取值:'idle'(未开始)/'generating'(生成流程中,用新接口)/'done'novel_done 后,用旧接口)。

  • Step 3: 验证热加载无编译错误

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

  • Step 4: 提交
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.vuehandleShortNovelEvent 函数约 1682 行)

  • Step 1: 改造 handleShortNovelEvent

找到 handleShortNovelEvent 函数(约 1682 行),当前实现是切换 viewState。替换为往 resultMessages 追加消息:

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: 提交
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.vuerunGeneration 约 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 后新增追加用户消息的逻辑:

找到这一段:

  streamWriter.reset()
  startGenerationFeedback()
  ttsPlayer.reset()
  viewState.value = 'generating'
  keepGenerationAtBottom()

替换为:

  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:

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 行),替换为:

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 行),替换为:

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 行),替换为:

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: 提交
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.vuetemplate 部分,约 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> 结束)替换为:

    <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 链为:homechat

  • Step 4: 新增 chat 视图相关样式

<style scoped> 部分末尾添加 chat 视图样式(复用现有 result/generation 样式,新增缺失部分):

.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: 提交

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.vuesendChat 函数约 701 行)

  • Step 1: 改造 sendChat 根据 generationPhase 切换接口

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

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

sendChat 函数替换为:

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: 提交
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 后底部输入栏出现,继续修改用旧接口工作
  • 失败时对话流内显示"继续创作"入口
  • 整个过程不切换视图,用户始终在同一对话页