6 个任务:扩展消息模型、handleShortNovelEvent 改对话流、 runGeneration+交互函数改造、统一 chat 视图模板、 sendChat 接口切换、端到端浏览器验证
27 KiB
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 行),当前实现:
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.vue(handleShortNovelEvent 函数约 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.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 后新增追加用户消息的逻辑:
找到这一段:
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.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> 结束)替换为:
<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 样式,新增缺失部分):
.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.vue(sendChat 函数约 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:
- 登录(手机号 13800138000 + 验证码 123456)
- 进入心愿页,输入"今天我想当 CEO",发送
- 验证:进入 chat 对话页,用户心愿作为气泡显示在对话流
- 验证:澄清卡片作为 AI 消息出现在对话流(不在独立页面)
- 选择选项,提交
- 验证:卡片变为"已回答"态,user 消息追加,下一轮澄清或大纲出现
- 若进入大纲:验证大纲作为 AI 消息出现(带确认/修改按钮)
- 确认大纲
- 验证:小说流式生成作为一条 AI 消息逐字出现
- novel_done 后:验证底部输入栏出现,可输入修改建议
- Step 3: 验证继续创作(DAILY_GENERATION_IN_PROGRESS)
若触发每日限制:
- 验证:对话流内显示失败提示 + "继续创作"按钮
- 点击"继续创作"
- 验证:恢复会话,新澄清/大纲作为消息追加到对话流
-
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 后底部输入栏出现,继续修改用旧接口工作
- 失败时对话流内显示"继续创作"入口
- 整个过程不切换视图,用户始终在同一对话页