fix(v2.2): StoryPlayer 推进竞态加固(双审查 SHOULD-FIX)

- speak 兜底定时器先于 speak 注册 + try/catch:speak 抛异常不再导致
  剧情永久卡死(估时/forceNext 未注册的失败模式)
- speak onEnd 与估时闭包加行号守卫(lineIndex===i && !ended):迟到的
  上一句 onEnd 不再提前放行当前句
- SceneTheater 估时封顶 20s(与 MAX_LINE_MS 对齐),注释同步 v2.2 语义
- plan Task 10 更新为实际实现(SetServerRoot + /static 挂载)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 09:13:05 +08:00
co-authored by Claude Opus 4.7
parent a775fc7b5c
commit 7386715846
3 changed files with 27 additions and 15 deletions
@@ -1392,9 +1392,10 @@ git commit -m "feat: 全自动演出——开场自动进入/回复自动继续/
> 用户走查反馈:① 必须点跳过才出选项(speak onEnd 部分环境不回调,逐句推进靠 20s forceNext 兜底,体验像卡死);② 剧情不需要文字展示区,直接语音播放;③ 前端应与后端共用 :8080。规格:spec「v2.2 修订」章节。
**Files:**
- Modify: `ui-src/src/components/StoryPlayer.vue`(纯语音 + 估时兜底)
- Modify: `ui-src/src/components/StoryPlayer.vue`(纯语音 + 估时兜底 + 审查加固
- Modify: `ui-src/src/components/SceneTheater.vue`(估时兜底 + 20s 封顶)
- Modify: `ui-src/src/pages/play/play.vue`(移除 :show-pinyin 传参)
- Modify: `main.go`AddStaticPath 托管前端产物
- Modify: `main.go`SetServerRoot 托管前端产物 + /static 素材挂载
- [ ] **Step 1: StoryPlayer 改纯语音演绎**
@@ -1522,12 +1523,15 @@ export default {
- [ ] **Step 3: main.go 托管前端产物(8080 单端口)**
`main.go` 在 `controller.Register(s)` 后加:
`main.go` 在 `controller.Register(s)` 后加**须用 SetServerRoot**——走查发现 GoFrame v2.10.2 `AddStaticPath("/", ...)` 因前缀防误匹配守卫 `uri[len(prefix)] != '/'` 只服务根路径,`/assets/*` 子路径全部 404;素材目录 `ui-src/static` 以 `/static` 前缀单独挂载,与后端 `scene.image` 返回的 `/static/generated/...` URL 对应)
```go
// 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口)
if _, err := os.Stat("ui-src/dist"); err == nil {
s.AddStaticPath("/", "ui-src/dist")
s.SetServerRoot("ui-src/dist")
}
if _, err := os.Stat("ui-src/static"); err == nil {
s.AddStaticPath("/static", "ui-src/static")
}
```
+4 -4
View File
@@ -79,8 +79,8 @@ export default {
},
mounted() {
this.speaking = true
// v2.1:朗读结束自动进入决策幕——最短展示 1.2s,之后 speak onEnd 即进入
// 无 TTS/onEnd 缺失最迟 15s 兜底(「跳过」仍可手动跳过)
// v2.1:朗读结束自动进入决策幕——最短展示 1.2s,之后 speak onEnd 即进入
// v2.2:估时兜底——onEnd 缺失时按文本长度估时自动进入(最长 20s,不卡死;「跳过」仍可手动跳过)
this.speechDone = false
this.ready = false
let done = false
@@ -91,8 +91,8 @@ export default {
this.$emit('done')
}
this.minTimer = setTimeout(() => { this.ready = true; if (this.speechDone) finishAuto() }, 1200)
// 兜底按文本长度估时(onEnd 缺失时按朗读预期时长自动进入,不卡 15s
this.autoTimer = setTimeout(finishAuto, Math.max(4000, this.content.length * 320 + 2000))
// 兜底按文本长度估时(onEnd 缺失时按朗读预期时长自动进入),封顶 20s 与 StoryPlayer MAX_LINE_MS 对齐
this.autoTimer = setTimeout(finishAuto, Math.max(4000, Math.min(this.content.length * 320 + 2000, 20000)))
const onSpeechEnd = () => {
this.speechDone = true
if (this.ready) finishAuto()
+15 -7
View File
@@ -61,22 +61,30 @@ export default {
}
this.lineIndex = i
this.ended = false
this.lineDone = { sp: false, fb: false }
this.lineDone = { sp: false }
this.moodTick = true
this.after(350, () => { this.moodTick = false })
const total = Array.from(line.text).length
speak(line.text, '', () => {
this.lineDone.sp = true
this.tryNext(i)
})
// 估时兜底:onEnd 缺失(浏览器静音策略/无语音)时按文本长度估时放行,不卡死
// 估时/forceNext 兜底定时器先于 speak 注册:speak 抛异常(stub 环境/无 TTS)也不会卡死
this.after(Math.max(2500, total * 320 + 1200), () => {
if (!this.lineDone.sp) {
if (this.lineIndex === i && !this.ended && !this.lineDone.sp) {
this.lineDone.sp = true
this.tryNext(i)
}
})
this.after(MAX_LINE_MS, () => this.forceNext(i))
try {
speak(line.text, '', () => {
// 行号守卫:迟到的第 i 句 onEnd 不得提前放行当前句
if (this.lineIndex === i && !this.ended) {
this.lineDone.sp = true
this.tryNext(i)
}
})
} catch (e) {
this.lineDone.sp = true
this.tryNext(i)
}
},
tryNext(i) {
if (this.ended || this.lineIndex !== i || !this.lineDone.sp) return