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

8.0 KiB
Raw Blame History

author, created_at, purpose
author created_at purpose
AI Assistant 2026-07-19 小程序心愿实现页面的短篇小说生成接口从内部 AI Runtime 迁移到外部 short-novel-service 的设计方案

短篇小说服务接口迁移设计

概述

将小程序"心愿实现"页面(ScriptView.vue)的短篇小说生成流程,从当前内部 AI Runtime 同步调用模式,迁移到外部 short-novel-servicehttp://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 事件后,后端执行一次性保存:

  1. 从 SSE 流中捕获 novel_done 事件的 full_text
  2. 创建 EpicScript 记录
  3. 创建 Conversation 记录
  4. 创建 3 条 Message 记录(系统欢迎、用户初始输入、AI 小说全文)
  5. 在最后一个 SSE 事件(novel_done)中附加 scriptIdconversationId

实现方式:在 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