99a3cb883e
- 后端 SSE 代理 + 前端多轮交互状态机改造 - 外部 short-novel-service 接口替代内部 AI Runtime - 数据持久化改为小说完成后一次性保存 - 短信验证码修复(888888→123456)
8.0 KiB
8.0 KiB
author, created_at, purpose
| author | created_at | purpose |
|---|---|---|
| AI Assistant | 2026-07-19 | 小程序心愿实现页面的短篇小说生成接口从内部 AI Runtime 迁移到外部 short-novel-service 的设计方案 |
短篇小说服务接口迁移设计
概述
将小程序"心愿实现"页面(ScriptView.vue)的短篇小说生成流程,从当前内部 AI Runtime 同步调用模式,迁移到外部 short-novel-service(http://49.232.138.53:8010)的流式 SSE 多轮交互模式。同时开启小程序的手机号短信登录功能(验证码固定 123456)。
架构决策
- 架构模式:后端 SSE 代理 + 前端完整多轮交互适配
- 持久化策略:小说生成完成后一次性保存(中间交互阶段不持久化)
- 认证方式:外部服务 API Token 保存在后端配置中,前端不接触
当前架构(AS-IS)
前端流程
ScriptView.vue
→ submitWish() → runGeneration()
→ createScriptWithDialogue(payload) // POST /epicScript/createWithDialogue
→ 返回 { scriptId, conversationId, title, plotIntro, plotTurning, plotClimax, plotEnding }
→ loadMessages()
→ viewState = 'result'
后端流程
EpicScriptController.createWithDialogue()
→ EpicScriptDialogueServiceImpl.createWithDialogue()
→ 创建 EpicScript + Conversation 记录
→ invokeScriptGenerate() → aiRuntimeService.test("script_generate") // 同步 AI 调用
→ parseScriptOutput() → 解析 JSON 输出
→ 创建 3 条 Message(系统欢迎、用户输入、AI 回复)
→ 返回 EpicScriptCreateWithDialogueResponse
当前接口特征
- 同步调用,一次性返回完整结果
- AI 输出为 JSON 格式(title、plotIntro、plotTurning、plotClimax、plotEnding)
- 前端无中间交互阶段
目标架构(TO-BE)
前端新流程
ScriptView.vue 状态机:
home → clarifying ⇄ outlining → novel-generating → result
阶段详情
| 阶段 | 触发事件 | UI 行为 | 用户操作 |
|---|---|---|---|
home |
用户输入心愿 | 展示输入框+灵感推荐+语音按钮 | 输入文本/语音,点击发送 |
clarifying |
clarification_card SSE 事件 |
展示卡片式问题 | 选择选项或输入文本回答 |
outlining |
outline_created SSE 事件 |
展示大纲(标题/logline/beats/结局) | 确认大纲 或 提出修改意见 |
novel-generating |
novel_start SSE 事件 |
逐字流式展示小说正文 | 等待生成完成 |
result |
novel_done SSE 事件 |
展示完成的小说+后续对话 | 继续对话修改/朗读/分享 |
澄清卡片类型
| card_type | UI 渲染 | 交互方式 |
|---|---|---|
single_select |
单选按钮组 | 选择一个选项 |
multi_select |
多选标签组 | 选择多个选项 |
text_input |
文本输入框 | 自由输入 |
mixed |
选项 + 输入框 | 选择或自由输入 |
后端新架构
新增 Controller
@RestController
@RequestMapping("/shortNovel")
public class ShortNovelController {
// 首次发起(SSE 流式代理)
@PostMapping("/stream")
public SseEmitter stream(@RequestBody ShortNovelStreamRequest request);
// 后续轮次(SSE 流式代理)
@PostMapping("/followup")
public SseEmitter followup(@RequestBody ShortNovelFollowupRequest request);
}
请求/响应格式
POST /shortNovel/stream
入参:
{ "query": "用户的心愿文本" }
后端转发到外部服务:
{
"user_id": "当前登录用户ID",
"message_id": "web_{timestamp}",
"query": "用户的心愿文本"
}
POST /shortNovel/followup
入参:
{
"sessionId": "外部服务返回的 session_id",
"action": "answer_clarification | confirm_outline | modify_outline | retry",
"payload": {
"answer": "用户回答(answer_clarification 时)",
"feedback": "修改意见(modify_outline 时)"
}
}
后端转发到外部服务:
{
"session_id": "sessionId",
"user_id": "当前登录用户ID",
"message_id": "web_{timestamp}",
"action": "answer_clarification | confirm_outline | modify_outline | retry",
"payload": { ... }
}
SSE 事件格式(透传)
外部服务返回的 SSE 事件格式:
data: {"type":"status","session_id":"xxx","payload":{"message":"处理中","stage":"clarification"}}
data: {"type":"clarification_card","session_id":"xxx","payload":{"card":{"card_type":"single_select","question":"...","options":[...]}}}
data: {"type":"outline_created","session_id":"xxx","payload":{"outline":{"title":"...","beats":[...]}}}
data: {"type":"novel_start","session_id":"xxx","payload":{}}
data: {"type":"novel_delta","session_id":"xxx","payload":{"delta":"文本片段"}}
data: {"type":"novel_done","session_id":"xxx","payload":{"full_text":"完整小说文本"}}
data: {"type":"error","session_id":"xxx","payload":{"code":"ERROR_CODE","message":"错误描述"}}
后端 SseEmitter 代理逐行转发这些事件,不做任何修改。
SSE 代理实现
// 使用 RestTemplate 读取外部服务 SSE 流
// 逐行解析 data: 行,通过 SseEmitter.send() 转发
// 超时设置 300 秒
// 错误处理:连接失败、超时、外部服务错误
配置
# application.yml
short-novel:
api-base-url: http://49.232.138.53:8010
api-token: c67d4a95b0bb92470a24d534302c0d40
connect-timeout: 10000
read-timeout: 300000
数据持久化
在收到 novel_done 事件后,后端执行一次性保存:
- 从 SSE 流中捕获
novel_done事件的full_text - 创建
EpicScript记录 - 创建
Conversation记录 - 创建 3 条
Message记录(系统欢迎、用户初始输入、AI 小说全文) - 在最后一个 SSE 事件(
novel_done)中附加scriptId和conversationId
实现方式:在 SSE 代理层拦截 novel_done 事件,在转发给前端之前执行保存逻辑,并将 ID 信息注入到事件的 payload 中。
短信验证码修复
改动范围
| 文件 | 行号 | 当前值 | 修改为 |
|---|---|---|---|
mini-program/src/pages/login/index.vue |
149 | '验证码已发送(模拟: 888888)' |
'验证码已发送(模拟: 123456)' |
后端已使用 DEFAULT_SMS_CODE = "123456"(AuthServiceImpl.java 第 80 行),无需修改。
前端新增/修改文件清单
| 文件 | 操作 | 说明 |
|---|---|---|
services/shortNovel.js |
新增 | 短篇小说流式 API 服务 |
components/ClarificationCard.vue |
新增 | 澄清卡片组件 |
pages/main/ScriptView.vue |
修改 | 生成流程改为状态机,新增 clarifying/outlining/novel-generating 阶段 |
后端新增/修改文件清单
| 文件 | 操作 | 说明 |
|---|---|---|
controller/ShortNovelController.java |
新增 | SSE 代理接口 |
service/ShortNovelService.java |
新增 | SSE 代理转发逻辑 |
service/impl/ShortNovelServiceImpl.java |
新增 | SSE 代理实现 |
dto/request/ShortNovelStreamRequest.java |
新增 | 首次请求 DTO |
dto/request/ShortNovelFollowupRequest.java |
新增 | 后续轮次 DTO |
config/ShortNovelConfig.java |
新增 | 配置类(api-base-url、api-token) |
application.yml |
修改 | 新增 short-novel 配置段 |
EpicScriptDialogueServiceImpl.java |
修改 | 抽取保存逻辑为可复用方法 |
错误处理
| 场景 | 处理方式 |
|---|---|
| 外部服务不可达 | SSE 发送 error 事件,前端显示"服务暂时不可用" |
| 外部服务超时(>300s) | SseEmitter 超时回调,前端显示"生成超时" |
| 外部服务返回错误事件 | 透传 error 事件给前端 |
| 用户中途退出 | 前端关闭 SSE 连接,后端 SseEmitter 自动清理 |
| 小说生成失败 | 外部服务发送 error 事件,前端提供"重试"按钮 |
不在本次范围内
- 外部服务的知识库图谱(knowledge-graph)功能 — 暂不集成
- 外部服务的 prompt 自定义配置 — 使用外部服务默认配置
- 历史对话的恢复/继续 — 已完成的历史记录按现有方式展示
- 真实的短信发送服务 — 继续使用固定验证码 123456