import express from 'express'; import { fetch } from 'undici'; const ASR_TARGET = process.env.H5_ASR_TARGET ?? 'https://asr.tkmind.cn'; const ASR_MAX_BYTES = Number(process.env.H5_ASR_MAX_BYTES ?? 5 * 1024 * 1024); const ASR_TIMEOUT_MS = Number(process.env.H5_ASR_TIMEOUT_MS ?? 45_000); export function sanitizeAsrMessage(message) { if (!message || typeof message !== 'string') return '识别失败,请重试'; const trimmed = message.trim(); if (!trimmed) return '识别失败,请重试'; if (/Failed to load audio|ffmpeg|Invalid data found when processing input|moov atom not found/i.test(trimmed)) { return '音频格式无法识别,请重新录制'; } if (/timeout|timed out/i.test(trimmed)) return '识别超时,请重试'; if (trimmed.length > 160) return `${trimmed.slice(0, 160)}…`; return trimmed; } export function attachAsrRoutes(api, deps) { const multipartRaw = express.raw({ type: (req) => (req.headers['content-type'] ?? '').includes('multipart/form-data'), limit: ASR_MAX_BYTES, }); // 登录校验由 api 全局中间件负责(req.currentUser) api.post('/asr/oneshot', multipartRaw, async (req, res) => { const contentType = req.headers['content-type'] ?? ''; if (!contentType.includes('multipart/form-data')) { return deps.sendError(res, req, 400, 'invalid_request', 'Content-Type 必须为 multipart/form-data'); } if (!req.body?.length) { return deps.sendError(res, req, 400, 'invalid_request', '音频内容为空'); } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), ASR_TIMEOUT_MS); try { const upstream = await fetch(`${ASR_TARGET}/asr/oneshot`, { method: 'POST', headers: { 'content-type': contentType }, body: req.body, signal: controller.signal, }); const text = await upstream.text(); let payload = null; try { payload = JSON.parse(text); } catch { payload = null; } if (!upstream.ok) { const message = sanitizeAsrMessage(payload?.message ?? text ?? 'ASR 请求失败'); console.warn('[asr] upstream HTTP error', upstream.status, message); return deps.sendError(res, req, upstream.status, 'asr_failed', message); } if (payload?.code !== 200) { const message = sanitizeAsrMessage(payload?.message ?? '识别失败'); console.warn('[asr] upstream business error', payload?.code, message); return deps.sendError(res, req, 502, 'asr_failed', message); } return deps.sendData(res, req, { text: payload?.data?.text ?? '' }); } catch (err) { const message = err instanceof Error && err.name === 'AbortError' ? '识别超时,请重试' : sanitizeAsrMessage(err instanceof Error ? err.message : 'ASR 请求失败'); console.warn('[asr] proxy failed', message); return deps.sendError(res, req, 502, 'asr_failed', message); } finally { clearTimeout(timer); } }); }