Files
happy-life-star/docs/superpowers/specs/2026-07-19-short-novel-service-migration-design.md
T
peanut 99a3cb883e docs:短篇小说服务接口迁移设计方案
- 后端 SSE 代理 + 前端多轮交互状态机改造
- 外部 short-novel-service 接口替代内部 AI Runtime
- 数据持久化改为小说完成后一次性保存
- 短信验证码修复(888888→123456)
2026-07-19 11:52:41 +08:00

240 lines
8.0 KiB
Markdown
Raw 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.
---
author: AI Assistant
created_at: 2026-07-19
purpose: 小程序心愿实现页面的短篇小说生成接口从内部 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
```java
@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**
入参:
```json
{ "query": "用户的心愿文本" }
```
后端转发到外部服务:
```json
{
"user_id": "当前登录用户ID",
"message_id": "web_{timestamp}",
"query": "用户的心愿文本"
}
```
**POST /shortNovel/followup**
入参:
```json
{
"sessionId": "外部服务返回的 session_id",
"action": "answer_clarification | confirm_outline | modify_outline | retry",
"payload": {
"answer": "用户回答(answer_clarification 时)",
"feedback": "修改意见(modify_outline 时)"
}
}
```
后端转发到外部服务:
```json
{
"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 代理实现
```java
// 使用 RestTemplate 读取外部服务 SSE 流
// 逐行解析 data: 行,通过 SseEmitter.send() 转发
// 超时设置 300 秒
// 错误处理:连接失败、超时、外部服务错误
```
#### 配置
```yaml
# application.yml
short-novel:
api-base-url: http://49.232.138.53:8010
api-token: c67d4a95b0bb92470a24d534302c0d40
connect-timeout: 10000
read-timeout: 300000
```
### 数据持久化
在收到 `novel_done` 事件后,后端执行一次性保存:
1. 从 SSE 流中捕获 `novel_done` 事件的 `full_text`
2. 创建 `EpicScript` 记录
3. 创建 `Conversation` 记录
4. 创建 3 条 `Message` 记录(系统欢迎、用户初始输入、AI 小说全文)
5. 在最后一个 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