Files
memind/asr-proxy.mjs
T
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

76 lines
3.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
});
}