---
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
