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

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