---
author: Peanut
created_at: 2026-06-29
purpose: 完整修复 ScriptView 对话模式的按钮缺失问题，补齐 spec 要求的版本/改写/续写/历史版本/删除功能
---

# ScriptView 对话模式按钮完整修复设计

## 背景

`bf6f279 feat: ScriptView 改造为对话中心查看/修改模式` 重构后，对话模式的按钮出现多处丢失：

1. **旧版按钮丢失**：`MessageCard` 在 `isShortMessage=true`（短消息）分支没有按钮组，而旧版 assistant 消息后面有「复制/换个方向/不像我/继续生成/播放」按钮组（commit `0db434c` 添加）
2. **Spec 功能未实现**：`2026-06-28-script-dialogue-design.md` 第 5.4 节要求 AI 剧本消息有「改写 / 续写 / 查看历史版本 / 删除版本」按钮，当前完全没实现
3. **根版本被过滤**：`displayMessages` 用 `m.type !== 'script' || !m.parentMessageId` 过滤，把根版本 AI 剧本消息丢掉了
4. **版本标签缺失**：AI 消息没有 `V1 / V2` 版本号标签和「(当前)」标记

## 设计

### 1. MessageCard.vue 重构

**新增 props**：
- `messageType`: `'script' | 'chat' | 'system'`，默认 `'script'`
- `versionLabel`: 版本号显示（如 `V1 (当前)`），script 类型必填，其他类型不传
- `hasChildren`: 是否有子版本（布尔，用于控制「查看历史版本」按钮是否显示）
- `canDelete`: 是否可删除（布尔，当前生效版本不可删除）
- `canRewrite`: 是否可改写（布尔，默认 true）
- `canContinue`: 是否可续写（布尔，默认 true）

**渲染规则**：

| messageType | 渲染方式 |
|---|---|
| `'script'` | **永远**走长文本 story-card 分支（不论 `isShortMessage`），因为剧本消息总是需要按钮组 |
| `'chat'` 或 `'system'` | 保持原逻辑（短消息气泡 / 长消息 story-card） |

**按钮组**（按 messageType 切换）：

```
script 消息按钮组：
  ┌─ 顶部操作（story-head-actions，不变）
  │   · 收起/展开
  │   · 复制
  ├─ 版本标签（新增，仅在 versionLabel 有值时显示）
  │   · V1 (当前)  /  V2  /  ...
  ├─ 功能按钮（新增，script 专属）
  │   · 改写（emit 'rewrite'）
  │   · 续写（emit 'continue-script'）
  │   · 查看历史版本（emit 'view-versions'，hasChildren=true 才显示）
  │   · 删除版本（emit 'delete-version'，canDelete=true 才显示）
  └─ 通用操作（保留旧版）
      · TTS 播放（emit 'play-tts'）

chat 消息按钮组（isShortMessage=false 时）：
  ┌─ 复制
  ├─ 收起/展开
  └─ TTS 播放（emit 'play-tts'）

system 消息（isShortMessage=true 默认走气泡）：
  └─ 不变
```

**新增 emits**：
- `rewrite` — 改写按钮
- `continue-script` — 续写按钮（避免和旧 `continue` 语义冲突，但保留旧 `continue` 兼容）
- `view-versions` — 查看历史版本
- `delete-version` — 删除版本

### 2. ScriptView.vue 改动

**改动 2.1：显示根版本**

```js
// 旧
const displayMessages = computed(() => {
  return messages.value.filter(m => m.type !== 'script' || !m.parentMessageId)
})

// 新：保留所有消息，根版本作为 chat 列表首条
const displayMessages = computed(() => messages.value)
```

**改动 2.2：传 messageType 给 MessageCard**

```vue
<MessageCard
  v-if="message.sender === 'assistant' || message.type === 'script'"
  message-type="script"
  :version-label="formatVersionLabel(message)"
  :has-children="messageHasChildren(message)"
  :can-delete="message.id !== currentVersionMessageId"
  :can-rewrite="true"
  :can-continue="true"
  :content="message.content"
  :collapsed="isMessageCollapsed(message)"
  :content-length="message.content.length"
  :is-short-message="false"
  :tts-icon="..."
  :tts-text="..."
  @toggle-collapse="toggleMessageCollapse(message)"
  @copy="copyMessageContent(message)"
  @rewrite="rewriteMessage(message)"
  @continue-script="continueMessage(message)"
  @view-versions="viewMessageVersions(message)"
  @delete-version="removeVersion(message)"
  @play-tts="playMessageTts(message)"
  @change-direction="changeDirection"
  @not-like-me="notLikeMe"
/>
```

**改动 2.3：版本标签生成函数**

```js
const formatVersionLabel = (message) => {
  if (message.type !== 'script') return ''
  const versionNum = message.versionNumber || 1
  const isCurrent = message.id === currentVersionMessageId.value
  return isCurrent ? `V${versionNum} (当前)` : `V${versionNum}`
}

const messageHasChildren = (message) => {
  // 通过 versions 数组长度判断（已加载的）或 metadata 推断
  return versions.value.some(v => v.parentMessageId === message.id)
    || (message.versionNumber && message.versionNumber > 1)
    || (message.childrenCount && message.childrenCount > 0)
}
```

**改动 2.4：查看历史版本**

新增 `viewMessageVersions(msg)` 函数：
1. 调用 `listMessageVersions(msg.id)`
2. 用 `uni.showActionSheet` 列出所有版本（V1、V2...）
3. 用户选择后：
   - 如果选的是当前版本：`uni.showToast` 提示「当前已生效」
   - 如果选的是其他版本：调用 `switchVersion({ scriptId, messageId: selectedId })` → 更新 `currentVersionMessageId` → `loadMessages()` 刷新

### 4. 历史剧本兼容层（高优先级）

### 现状

DB 中绝大多数历史剧本的 `conversation_id` 是前端早期自造的临时 ID（`conv-<ts>-<rand>` 格式），**`t_conversation` 表中没有对应记录**。

```
329987360219471872  我的人生剧本     → YES (最新剧本，走新流程创建)
f82818b702d5ad4a...  高考            → NO
a3dadb7e85a6c75b...  我高考了        → NO
49ae0f432253aaff...  我中了100w      → NO
... 其余 6 条 → NO
```

### 影响分析

| 场景 | 是否受影响 | 原因 |
|---|---|---|
| ScriptDetailView 阅读模式 | ✅ 正常 | 读 `t_epic_script.plotJson`，不依赖 conversation |
| ScriptView `viewMode='read'` 阅读模式 | ✅ 正常 | 通过 `currentScriptContent` 读 `currentVersionMessageId` 指向的 message；fallback 到 `plotJson` |
| ScriptView `viewMode='chat'` 聊天模式 | ❌ 异常 | `listByConversation` 返回空，对话历史丢失 |
| 改写 / 续写 / 版本切换 | ❌ 异常 | 需要有效的 conversation 和 message 才能操作 |

### 兼容策略：前端 fallback + 后端自动建表

**改动 A：后端自动建 conversation**

在 `EpicScriptServiceImpl.getScriptById`（或 `getListByCurrentUser` 返回前）检查：
- 若 `script.conversation_id` 指向的 conversation 不存在 → 自动创建一条 `t_conversation` 记录，写入 `user_id`、`script_id`、`type='script'`、`status='active'`，并回填 `t_epic_script.conversation_id` 为新 ID

**改动 B：前端首次进入聊天模式时，基于 `plotJson` 初始化首条消息**

在 ScriptView.vue 的 `loadMessages` 中：
- 若 API 返回空列表 && `script.plotJson?.fullContent` 存在 → 自动调用 `/message/create` 创建一条 `type='script'` 的 AI 剧本消息（content 来自 plotJson）
- 后续改写/续写/版本切换基于这条真实消息操作

**改动 C：ScriptView 阅读模式 fallback 强化**

当前 `currentScriptContent` 已在 message 不存在时 fallback 到 `plotJson`。需确保：
- `viewMode='read'` 下，版本标签在没有 versions 数据时隐藏（不显示"V0"之类）
- "进入对话修改"按钮在没有 conversation 时仍可点击，触发改动 B 的初始化流程

### 验收标准（历史剧本）

- [ ] 历史剧本在 ScriptDetailView 完整显示标题、summary、content
- [ ] 历史剧本在 ScriptView `viewMode='read'` 完整显示标题、章节内容（无版本标签时 UI 不异常）
- [ ] 历史剧本点击"进入对话修改" → 自动创建 conversation + 初始化首条 AI 消息 → 进入 chat 模式
- [ ] 历史剧本进入 chat 模式后，能看到完整对话历史（首条为用户心愿 + AI 剧本）
- [ ] 历史剧本的"改写/续写/查看历史版本"功能可正常使用
- [ ] 新创建的剧本所有功能不受影响（回归验证）

## 5. 实施顺序

| 顺序 | 任务 | 类型 |
|---|---|---|
| 1 | 后端 `getScriptById` 自动建 conversation | 数据兼容 |
| 2 | 前端 MessageCard 重构 + 按钮组 | 功能补齐 |
| 3 | 前端 ScriptView displayMessages 去掉根版本过滤 | Bug 修复 |
| 4 | 前端 loadMessages 空结果时自动初始化首条消息 | 数据兼容 |
| 5 | 前端 chat 模式传 messageType/versionLabel 等 props | 功能补齐 |
| 6 | 端到端验收（新剧本 + 历史剧本） | 验收 |

## 6. 影响范围

| 文件 | 改动量 |
|---|---|
| `server/.../EpicScriptServiceImpl.java` | 小（getScriptById 检查 + 自动建 conversation） |
| `mini-program/src/components/MessageCard.vue` | 中（+props、+emits、+按钮组条件渲染） |
| `mini-program/src/pages/main/ScriptView.vue` | 中（displayMessages filter、MessageCard props、formatVersionLabel、viewMessageVersions、loadMessages 初始化） |

## 验收标准（代码层面）

- [ ] chat 模式下，根版本 AI 剧本消息显示为 chat 列表首条
- [ ] AI 剧本消息显示 V1 / V2 版本号 + (当前) 标记
- [ ] AI 剧本消息按钮组包含：收起/展开、复制、TTS 播放、改写、续写、查看历史版本、删除版本
- [ ] 用户消息（type=chat）保持气泡样式，有收起/展开按钮
- [ ] 点击「改写」弹出 modal 输入改写意图，提交后生成新版本
- [ ] 点击「续写」直接生成新版本
- [ ] 点击「查看历史版本」弹 ActionSheet 列出版本，切换后 UI 更新
- [ ] 当前生效版本不显示「删除版本」按钮
- [ ] 历史剧本进入 ScriptView chat 模式 → 自动创建 conversation + 初始化首条 AI 消息
- [ ] 浏览器 Console 无报错
- [ ] 不影响 read 模式（阅读模式保持现状）

## H5 端到端验收流程（强制）

**后端验证码**：`AuthServiceImpl.java:80` 已硬编码 `DEFAULT_SMS_CODE = "123456"`，无需改动。

**测试账号**：手机号 `19928748688`，验证码 `123456`。

### 步骤 1：启动本地 H5
```bash
python dev-services.py start mini-program
# 访问 http://localhost:5180
```

### 步骤 2：登录
1. 打开 `http://localhost:5180`
2. 输入手机号 `19928748688`
3. **必须点击"发送验证码"按钮**（触发 `/auth/sms-code` 接口，后端写入 Redis 并返回固定验证码）
4. 输入验证码 `123456`
5. 提交登录

### 步骤 3：新剧本完整流程
1. 首页输入心愿 → 生成第一个剧本
2. 查看剧本详情 → 标题、summary、content 完整显示
3. 点击"继续" → 进入 ScriptView chat 模式
4. **验证**：
   - 用户心愿气泡显示
   - AI 剧本消息显示，带 `V1 (当前)` 版本标签
   - AI 消息按钮组包含：收起/展开、复制、TTS 播放、**改写**、**续写**、**查看历史版本**、**删除版本**（当前版本不显示删除）
5. 点"改写" → 输入改写意图 → 生成新版本 V2
6. 点"查看历史版本" → ActionSheet 列出 V1/V2，点击切换
7. 点"续写" → 生成新内容

### 步骤 4：历史剧本完整流程
1. 点"历史"按钮 → 进入剧本库
2. 点任一**历史剧本**（非本次新生成的）→ 进入 ScriptDetailView
3. **验证**：
   - 标题、summary、content 完整显示（读 `plotJson.fullContent`）
   - 风格、篇幅、字数正确
4. 点"继续" → 进入 ScriptView
5. **验证**：
   - 自动创建 `t_conversation`（DB 中新增记录）
   - 自动初始化首条 AI 消息（基于 plotJson）
   - chat 模式看到完整对话历史（心愿 + AI 剧本）
   - AI 消息带版本标签
   - 改写/续写/查看历史版本 可正常操作

### 步骤 5：Console 与 Network 检查
- DevTools Console 无报错
- Network 中所有 API 调用返回 2xx（无 5xx、无 401、无 403）
- `listByConversation` 调用返回 200 且有数据

### 步骤 6：验收通过标准
- ✅ 新剧本 + 历史剧本所有功能正常
- ✅ 按钮组完整
- ✅ 版本标签正确
- ✅ Console 无报错
- ✅ 历史剧本 conversation 自动创建成功

**未通过的处理**：任一验收项失败 → 停止 → 定位问题 → 修复 → 重新验收 → 直到全部通过才算完成

## 风险与回退

- **MessageCard 重构风险**：新增 props 都带默认值，不会破坏非对话模式（生成页、结果页）的现有用法
- **向后兼容**：旧 `continue` emit 保留，新增 `continue-script` 作为 script 专属，避免语义冲突
- **回退方案**：git revert 单个 commit 即可
