From b088865d4012c007dd2a5b29ffd95d387acbf189 Mon Sep 17 00:00:00 2001 From: duoaohui <928970622@qq.com> Date: Tue, 19 May 2026 11:30:25 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/api/transcribe.js | 47 +++++++++++++++++++ .../controller/meeting_manage_controller.go | 35 +++++++++++++- .../admin/service/meeting_manage_service.go | 24 ++++++++-- 3 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 admin/src/api/transcribe.js diff --git a/admin/src/api/transcribe.js b/admin/src/api/transcribe.js new file mode 100644 index 0000000..ac335b2 --- /dev/null +++ b/admin/src/api/transcribe.js @@ -0,0 +1,47 @@ +/** + * 语音转写 API(Phase B) + * + * 对应后端路由: + * POST /api/v1/admin/recordings/:id/transcribe 提交转写任务(异步) + * GET /api/v1/admin/recordings/:id/transcript 查询转写结果 + * + * 后端通过外部 LLM 配置中心 (MySQL t_llm_provider/t_llm_model/t_llm_key) + * 选择启用的 ASR 模型 + Key,调用 openai_compatible /audio/transcriptions。 + * + * 状态机:pending → running → ready / failed + * + * 503:LLM 配置库未启用,admin 应提示「请联系管理员配置 STT」 + * 202:任务已在跑,前端可继续轮询 + */ +import request from '@/utils/request' + +/** + * 查询某段录制的最新转写结果 + * @param {number} recordingId + * @returns {Promise<{data: TranscriptDTO|null}>} + */ +export function getTranscript(recordingId) { + return request({ + url: `/api/v1/admin/recordings/${recordingId}/transcript`, + method: 'get' + }) +} + +/** + * 提交转写任务 + * @param {number} recordingId + * @param {Object} [opts] + * @param {boolean} [opts.force=false] - true=已 ready 时强制重跑 + * @param {string} [opts.language=''] - ISO-639 语言提示,"" 表示自动检测 + * @returns {Promise<{data: TranscriptDTO}>} + */ +export function submitTranscribe(recordingId, opts = {}) { + return request({ + url: `/api/v1/admin/recordings/${recordingId}/transcribe`, + method: 'post', + data: { + force: !!opts.force, + language: opts.language || '' + } + }) +} diff --git a/backend/go-service/app/admin/controller/meeting_manage_controller.go b/backend/go-service/app/admin/controller/meeting_manage_controller.go index f27ad6b..d5549e9 100644 --- a/backend/go-service/app/admin/controller/meeting_manage_controller.go +++ b/backend/go-service/app/admin/controller/meeting_manage_controller.go @@ -7,6 +7,7 @@ import ( "github.com/echochat/backend/app/admin/service" "github.com/echochat/backend/app/dto" + meetingService "github.com/echochat/backend/app/meeting/service" "github.com/echochat/backend/pkg/logs" "github.com/echochat/backend/pkg/utils" "github.com/gin-gonic/gin" @@ -76,11 +77,43 @@ func (ctl *MeetingManageController) GetMeetingStats(c *gin.Context) { utils.ResponseOK(c, resp) } +// ForceStopRecording POST /api/v1/admin/meetings/recordings/:id/force-stop +// +// 兼底场景:host 离线/失联未点停止;host 端点击停止后 ffmpeg 异常但 webhook 失效; +// 任何长时间停留在 recording 状态的录制都可由此接口收尾。 +// +// 行为: +// - 终态(ready/failed/uploading):幂等返回 200 +// - recording 状态:走 media-server stop + MinIO 上传 + DB 状态机 +func (ctl *MeetingManageController) ForceStopRecording(c *gin.Context) { + ctx := c.Request.Context() + + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil || id <= 0 { + utils.ResponseBadRequest(c, "录制 ID 非法") + return + } + + if err := ctl.svc.ForceStopRecording(ctx, id); err != nil { + ctl.handleError(c, err, "强制结束录制失败") + return + } + utils.ResponseOK(c, gin.H{"recording_id": id, "stopped": true}) +} + // handleError 统一业务错误映射 func (ctl *MeetingManageController) handleError(c *gin.Context, err error, fallbackMsg string) { switch { - case errors.Is(err, service.ErrMeetingNotFound): + case errors.Is(err, service.ErrMeetingNotFound), + errors.Is(err, meetingService.ErrMeetingNotFound), + errors.Is(err, meetingService.ErrRecordingNotFound): utils.ResponseNotFound(c, err.Error()) + case errors.Is(err, meetingService.ErrRecordingNotStoppable): + utils.ResponseBadRequest(c, err.Error()) + case errors.Is(err, meetingService.ErrMediaServiceUnavailable): + logs.Warn(c.Request.Context(), "controller.meeting_manage_controller.handleError", + "媒体服务不可用", zap.Error(err)) + utils.ResponseError(c, "媒体服务暂时不可用,请稍后重试") default: logs.Warn(c.Request.Context(), "controller.meeting_manage_controller.handleError", fallbackMsg, zap.Error(err)) diff --git a/backend/go-service/app/admin/service/meeting_manage_service.go b/backend/go-service/app/admin/service/meeting_manage_service.go index 72a1875..e166f83 100644 --- a/backend/go-service/app/admin/service/meeting_manage_service.go +++ b/backend/go-service/app/admin/service/meeting_manage_service.go @@ -9,6 +9,7 @@ import ( adminDAO "github.com/echochat/backend/app/admin/dao" "github.com/echochat/backend/app/dto" meetingModel "github.com/echochat/backend/app/meeting/model" + meetingService "github.com/echochat/backend/app/meeting/service" "github.com/echochat/backend/pkg/logs" "go.uber.org/zap" ) @@ -22,12 +23,29 @@ var ErrMeetingNotFound = errors.New("会议记录不存在") // "强制结束会议" 这种写操作直接复用 meeting/service.MeetingService.EndRoom 即可(admin_force 路径), // 暂不在本服务里重复包装,避免与现有 RBAC/中间件冲突。 type MeetingManageService struct { - dao *adminDAO.MeetingManageDAO + dao *adminDAO.MeetingManageDAO + recSvc *meetingService.MeetingRecordingService } // NewMeetingManageService Wire Provider -func NewMeetingManageService(dao *adminDAO.MeetingManageDAO) *MeetingManageService { - return &MeetingManageService{dao: dao} +func NewMeetingManageService( + dao *adminDAO.MeetingManageDAO, + recSvc *meetingService.MeetingRecordingService, +) *MeetingManageService { + return &MeetingManageService{dao: dao, recSvc: recSvc} +} + +// ForceStopRecording 管理员强制结束指定录制 +// +// 委托给 meeting/service.MeetingRecordingService.ForceStopByAdmin: +// - 终态(ready/failed/uploading)幂等返回 nil +// - 否则走完整 stop + 上传 MinIO + DB 状态机 +func (s *MeetingManageService) ForceStopRecording(ctx context.Context, recordingID int64) error { + funcName := "service.meeting_manage_service.ForceStopRecording" + logs.Info(ctx, funcName, "管理员请求强制结束录制", zap.Int64("recording_id", recordingID)) + + _, err := s.recSvc.ForceStopByAdmin(ctx, recordingID) + return err } // ---------- public methods ----------