fix(audio): 断连后能接着跑完 —— 失败消息上的重试 + 步骤编号统一

用户报「卡在步骤 3」,实际断在第 5 步 structure:请求发出 46ms 后浏览器
连接断开,Gin 的 Request.Context() 被取消,上游回 499,后端包成 400。
服务端与前端应用层都没有任何主动取消(全仓 grep cancel/AbortController
零命中),是一次客户端断连。

断连本身无法在代码里杜绝,但它暴露了三个真 bug:

1. 报错文案说「第 1 步」,右栏说第 5 步 —— 尾部两步按自己这一轮从 1 数。
   新增 STEP_NUMBERS,报错与右栏共用同一份编号(这就是「步骤 3」这个
   说法的来源:用户看到的第一个数字就对不上)。
2. 断连后界面上没有任何续跑入口,任务永远停在 4/6(右栏六步是只读的)。
   失败的那条助手消息上挂「🔁 重试」。
3. 无脑重跑会在同一任务里留第二份「结构化纪要」,两份差别用户看不出来。
   resumeAudioSkill 收 skipSteps,按 task_run.action_key 判「跑过没有」
   —— 不用产物类型判,document/checklist 别的技能也在用。

另修:待确认那条消息改用 reactive()。messages 是 ref([]),push 进去的对象
模板读时才被代理,而代码改的是那个变量,改普通对象不触发重渲染,
用户会一直看到「正在继续整理...」。

验证(临时任务 100,已删,先备份 db):CDP setBlockedURLs 掐掉 structure
复现同形断连 → 消息挂出重试按钮,文案与右栏同为第 5 步;后端补跑一次
structure(模拟响应丢失但已落库)→ 点重试只跑 minutes,落库 document 1 份、
checklist 1 份。反证:再手工跑一次 structure,同名产物立刻变 2 份。
详见 bugs_and_errors.md E14。

E15(未修,记录在案):DeleteMyTask 只删 task_record,run 与 artifact 全成
孤儿,产物 content_text 是完整逐字稿。删除语义务必先定硬删还是软删。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-27 00:04:19 +08:00
co-authored by Claude Code
parent b4c8ea38d1
commit 81366ca273
5 changed files with 784 additions and 10 deletions
+82
View File
@@ -620,6 +620,85 @@ torch / matplotlib 之类的 dotfile 也落在同一个可写位置。
---
## E14 「卡在第 3 步」——三步说的不是同一步
**日期**:2026-09-26 **区域**:语音转写技能 / 失败恢复
**症状**:用户报「看起来在步骤 3 卡住了,没有完成工作」。界面实况:右栏
「语音转写 · 技能工作流 **4 / 6** 完成」,第 1–4 步全绿,第 5 步「整理段落与重点」
与第 6 步「提炼可交付纪要」都是「未开始」;聊天区最后一条停在
「已记下你确认的说话人身份,正在继续整理段落与生成纪要...」。
**根因**:**用户说的第 3 步、右栏的第 5 步、报错文案里的第 1 步,是三个不同的编号。**
- 真正断掉的是 `audio-transcribe:structure`(右栏第 5 步)。请求发出后 **46ms**
浏览器↔dev server 的连接断开,Gin 的 `c.Request.Context()` 随之取消,上游 LMUAI
回 499 `context canceled`,后端包成 400。
- **不是服务端取消**:全仓 grep `WithCancel|WithTimeout|cancel()` 在 `internal/ai`、
`internal/skills/api`、`internal/middleware`、`audio_transcribe` 中**零命中**;
前端全 `src/` grep `AbortController|CancelToken|signal` 同样零命中。
日志里也没有 401(排除跳登录页杀请求),全文只此 1 次 `context canceled`。
结论:一次客户端断连,不是模型/路由/代码的问题。
- 但**失败文案把它叫「第 1 步」** —— 尾部两步按**自己这一轮**从 1 数,
而右栏按整条链数。用户看到的第一个数字就对不上,于是有了「卡在步骤 3」这个说法。
- 更麻烦的是:断连之后**界面上没有任何续跑入口**。右栏那六个步骤是只读的,
没有 `@click`,任务永远停在 4/6。
**修法**(三处,都在前端):
1. `audioSkill.js` 导出 `STEP_NUMBERS`,报错文案与右栏共用同一份编号 —— 两处各写一遍
就是这次对不上号的成因。
2. 失败的那条助手消息上挂 `🔁 重试`(`SmartAssistantPage.vue`)。复用 `speakerConfirmBusy`
做并发闸,不新造一套。
3. `resumeAudioSkill` 收 `skipSteps`,由页面按 `task_run.action_key` 算出「哪几步跑过了」。
**判据必须是 run 而不是产物类型**:产物类型(`document` / `checklist`)别的技能也在用,
拿它判断会把别人的产物误认成自己的。
4. 待确认那条消息要用 `reactive()` 包。`messages` 是 `ref([])`,push 进去的对象在模板里
读时才被代理,而代码改的是**那个变量** —— 改普通对象只改了原始值,不触发重渲染,
用户会一直看到「正在继续整理...」。
**验证**(临时任务 100,用完即删):
- 用 CDP `Network.setBlockedURLs` 掐掉 `*/api/skills/audio/structure`,复现与线上同形的
断连 → 消息变失败态并挂出重试按钮。截图 `/tmp/shot_retry_a_failed.png`:红字
「第 5 步「整理段落与重点」失败:Network Error」,与右栏那个 5 是同一个。
- 脚本直接对后端补跑一次 `structure`(模拟「后端跑完了,只是响应没回来」)→ 点重试
→ 只跑 `minutes`,跑完 6/6(`/tmp/shot_retry_b_done.png`)。
- 落库核对:`document` 1 份、`checklist` 1 份,没有重复。
- **反证**(先证明断言会失败):再手工 POST 一次 `structure`,同名「结构化纪要」立刻变成
**2 份** —— 这就是 `skipSteps` 缺席时会发生的事。
**怎么早点发现**:
> **同一步在界面上有两个编号,迟早有一天会被当成两步。** 序号这种东西只有一份来源,
> 谁要显示都从那里取。
>
> 而**失败现场必须自带出路**。把「怎么继续」放在一个用户当前看不到、也点不动的地方
> (右栏只读列表),等于没放 —— 用户会做的是刷新,而刷新之后连失败的那条消息都没了。
---
## E15 删除任务只删了 `task_record`,run 与 artifact 全成孤儿
**日期**:2026-09-26 **区域**:后端(`internal/api/my_task.go`)
**症状**:清理验证夹具时走真实接口 `DELETE /api/my/tasks/100`,返回 200。
删前 `task_record` 1 条、`task_run` 7 条、`task_artifact` 6 条;删后
**0 / 7 / 6** —— 只有主记录没了,run 与产物原样留着。
**根因**:`DeleteMyTask`(`my_task.go:46`)只调 `taskRecordDAO.Delete(&task)`,没有级联。
**影响**:产物的 `content_text` 是**完整逐字稿**(真实会议录音,本次夹具那份 1550 字,
线上任务是 26278 字)。也就是说「删掉任务」并不等于「删掉录音内容」。
`DELIVERY.md` 第 3 节手工清单 C 项「清空测试数据」若按这个删法走,
库里会留下一批**没有任何入口能看到、也没人能删**的孤儿逐字稿。
**状态**:**未修**。删除语义(硬删 / 软删留痕)是产品决定,不擅自改。
本次是我自己造的夹具,已手工清掉那 7 条 run 与 6 条产物(先备份
`/tmp/eai_backup_before_retry_test.db`,删后核对为 0 / 0)。
---
## 待沉淀(还没写进规则的)
- [ ] **装完必须导入自检**:带 C 扩展的包(torch / torchaudio / ctranslate2)装完立刻 import 一次(见 E01)。
@@ -634,3 +713,6 @@ torch / matplotlib 之类的 dotfile 也落在同一个可写位置。
ProtectSystem / ReadOnlyPaths)、并发模型(前台独占 vs 后台多请求)。
凡是「原型上验过」的东西,装成服务后必须**照着这三条重验一遍**。
这次的 E13 是账号那条,E12 是并发那条。
- [ ] **E14/E15 的通用形状**:**多步流程的「第 N 步」只能有一个编号来源**(E14);
**有从属数据的聚合根,删除必须级联,或者明确是软删**(E15)。
两条都属「界面/接口说了一件事,数据说另一件事」,候选合并成 P06 的一条。