/** * 取阿里云 NLS 的 token(实时语音识别用)——**走后端网关**。 * * 端点是 `{VITE_NLS_TOKEN_API}`(默认 `/aly-api/proxy/verify/getALYToken`,同源路径: * dev 由 vite 代理到 `http://121.43.55.7:10011`,生产由 nginx 转发)。 * 返回形态:`{ code: 200, message: "成功", content: "<32 位 token>" }`。 * * **前端不签 token**:签名要 AccessKeySecret,而前端 bundle 是公开的; * 网关侧(或后端)做这件事,前端只拿结果。 * 与阿里云 `CreateToken` 的差别:网关**不返回过期时间**,按官方文档的 24 小时估。 * * 缓存策略:缓存到过期前 5 分钟;并发调用共用同一个在途请求 * (说话时前端可能连着触发几次,不该打多次网关)。 */ import { getNlsAppkey, getNlsTokenApiUrl } from "@/utils/runtime-config"; export interface NlsTokenResult { token: string; /** 过期时间(epoch 毫秒)—— 网关不给,按 24 小时估 */ expireAt: number; } /** NLS token 的有效期(官方文档 24 小时;网关不返回,所以按它估) */ const TOKEN_LIFETIME_MS = 24 * 60 * 60 * 1000; /** 提前多久视为过期(留出换 token + 重连的余量) */ const REFRESH_AHEAD_MS = 5 * 60 * 1000; let cached: NlsTokenResult | null = null; let inflight: Promise | null = null; const isValid = () => !!cached && cached.expireAt - Date.now() > REFRESH_AHEAD_MS; export const fetchNlsToken = async (): Promise => { if (isValid()) { return cached as NlsTokenResult; } if (inflight) { return inflight; } inflight = (async () => { const response = await fetch(getNlsTokenApiUrl(), { method: "GET" }); let body: any = null; try { body = await response.json(); } catch { /* 非 JSON 时按状态码报错 */ } if (!response.ok) { throw new Error(body?.message || `获取语音识别 token 失败(HTTP ${response.status})`); } // 网关用 code 表达业务结果(HTTP 200 也可能是失败) if (Number(body?.code) !== 200 || !body?.content) { throw new Error(body?.message || "获取语音识别 token 失败:响应里没有 token"); } cached = { token: String(body.content).trim(), expireAt: Date.now() + TOKEN_LIFETIME_MS, }; return cached; })(); try { return await inflight; } finally { inflight = null; } }; /** NLS 的 appkey(识别请求要带)—— 见 `getNlsAppkey` 的说明 */ export const getNlsTokenAppkey = (): string => getNlsAppkey(); /** 丢掉缓存(排查用:怀疑 token 失效时强制重取) */ export const clearNlsTokenCache = (): void => { cached = null; };