diff --git a/docs/superpowers/specs/2026-06-28-rollback-scriptview-temp-id-protection-design.md b/docs/superpowers/specs/2026-06-28-rollback-scriptview-temp-id-protection-design.md new file mode 100644 index 0000000..557eec5 --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-rollback-scriptview-temp-id-protection-design.md @@ -0,0 +1,84 @@ +--- +author: Peanut +created_at: 2026-06-28 +purpose: 回滚 ScriptView 的临时 conversationId 正则短路保护,修复历史剧本消息卡片不显示的回归 +--- + +# 回滚 ScriptView 临时 ID 正则短路保护 + +## 背景 + +commit 19337b9("ScriptView loadMessages 对临时 conversationId 正则短路保护")引入了一个回归 bug: +用户从"历史剧本"列表点进详情再点"继续"进入 ScriptView,**看不到完整的消息卡片**。 + +## 根因 + +DB 中历史剧本的 `conversation_id` 字段值就是 `conv--` 格式(前端在早期版本自造后写入): + +``` +conv-1782552967659-615gen +conv-1782228787632-fkifia +conv-1781497026473-7l8jco +... +``` + +这些 conversationId 完全匹配 commit 19337b9 加的正则 `^conv-\d+-[a-z0-9]{6}$`。 +并且这些 conversation 在 `t_conversation` 表中大多没有对应记录(`current_version_message_id` 字段全为 NULL,佐证了当时 conversation 没有完整落库)。 + +用户打开历史剧本的 ScriptView 时: +1. 从 store 拿到 script,`conversationId = script.conversationId` = `conv-1782552967659-615gen` +2. 进入 `loadMessages()` +3. 正则匹配 + `backedByScript=false`(script 上没有 conversationId 背书?错,有背书但被误判) +4. 实际上 `currentResult.value.conversationId` 在 `openScriptChat` 里被赋值为 `script.conversationId`,但 `openScriptChat` 是在 ScriptView 页内调用的,详情页 "继续" 按钮走的是 `continueCurrent` → `uni.reLaunch({ url: '/pages/main/index?tab=script' })`,ScriptView 重新初始化时 `currentResult.value` 还未被赋值 +5. 正则保护触发 → `messages.value = []` → **所有消息卡片不显示** + +## 修复方案 + +**删除** commit 19337b9 在 `mini-program/src/pages/main/ScriptView.vue` 的 `loadMessages` 中添加的正则短路保护代码。恢复为: + +```javascript +const loadMessages = async () => { + if (!conversationId.value) return + try { + const res = await listMessagesByConversation({ conversationId: conversationId.value, includeVersions: false }) + messages.value = res.data || [] + const scriptMessages = messages.value.filter(m => m.type === 'script') + if (scriptMessages.length > 0) { + const rootMessage = scriptMessages[0] + const versionRes = await listMessageVersions(rootMessage.id) + versions.value = versionRes.data || [] + } + } catch (error) { + uni.showToast({ title: error.message || '加载消息失败', icon: 'none' }) + } +} +``` + +**为什么可以直接回滚**: +- commit e3eb1af 已让后端 `listByConversation` 在 conversation 不存在时返回空列表(不抛 500) +- 所以前端调 API 时即使传了"不存在"的 conversationId,也只会得到空数据,不会再 500 +- 5xx 噪音问题已由后端修复兜底,前端的正则保护属于过度防御 + +## 验证步骤 + +1. 修改 `mini-program/src/pages/main/ScriptView.vue`,删除正则短路保护 +2. 启动 H5:`python dev-services.py start mini-program`(端口 5180) +3. 浏览器访问 `http://localhost:5180`,登录一个有历史剧本的账号 +4. 进入任一历史剧本的 ScriptView +5. 验证: + - 不再看到正则保护导致的"消息全空" + - Network 面板有 `/message/listByConversation` 请求 + - 响应状态 200,body 为 `{"code":200,"data":[]}`(conversation 不存在)或有数据 + - Console 无报错 + +## 风险与回退 + +- **回退方案**:如果回滚后出现 500 噪音,可以再在前端加判断(但后端已兜底,不应再出现) +- **影响面**:仅影响 ScriptView.vue 的 loadMessages 函数,其他页面不受影响 + +## 长期改进(不在本次范围) + +历史剧本的 conversation_id 是前端自造的临时 ID,但被当作"真实 conversationId"写入了 `t_epic_script.conversation_id`。这是数据模型上的不一致: +- 要么在写入 epic_script 时同步创建 t_conversation 记录 +- 要么 epic_script.conversation_id 字段只存真实落库的 ID +- 本次不处理,单独跟踪 diff --git a/mini-program/src/pages/main/ScriptView.vue b/mini-program/src/pages/main/ScriptView.vue index eab1358..19e5cc4 100644 --- a/mini-program/src/pages/main/ScriptView.vue +++ b/mini-program/src/pages/main/ScriptView.vue @@ -663,19 +663,6 @@ const continueCurrentVersion = () => { const loadMessages = async () => { if (!conversationId.value) return - // 临时 ID 短路保护: - // 当 conversationId 是 ensureConversationId() 在前端自造的 `conv--` 形式, - // 且没有 script.conversationId 背书(即后端还没通过 createWithDialogue 落库), - // 此时去查 listByConversation 必然是空且会产生 5xx 噪音。 - // 在 streamScriptChat 第一次成功落库前,直接跳过查询。 - const looksLikeTempId = /^conv-\d+-[a-z0-9]{6}$/.test(conversationId.value) - const backedByScript = Boolean(currentResult.value?.conversationId) - if (looksLikeTempId && !backedByScript) { - messages.value = [] - versions.value = [] - return - } - try { const res = await listMessagesByConversation({ conversationId: conversationId.value, includeVersions: false }) messages.value = res.data || []