first commit

This commit is contained in:
douboer
2026-03-21 18:57:10 +08:00
commit c49aa1a5e9
570 changed files with 107167 additions and 0 deletions

View File

@@ -0,0 +1,277 @@
/**
* 小程序语音网关客户端:
* 1. 对齐 Web 端 /ws/asr 协议ready/result/round_end/error
* 2. 控制帧走 JSON音频分片走二进制帧。
*/
const READY_TIMEOUT_MS = 7000;
const CLOSE_TIMEOUT_MS = 2600;
function buildAsrWsUrl(rawGatewayUrl, token) {
const input = String(rawGatewayUrl || "").trim();
if (!input) {
throw new Error("网关地址为空");
}
let base = input;
if (base.startsWith("http://")) base = `ws://${base.slice(7)}`;
if (base.startsWith("https://")) base = `wss://${base.slice(8)}`;
if (!base.startsWith("ws://") && !base.startsWith("wss://")) {
base = `wss://${base}`;
}
base = base.replace(/\/+$/, "");
const safeToken = encodeURIComponent(String(token || ""));
return `${base}/ws/asr?token=${safeToken}`;
}
function createAsrGatewayClient(options) {
const config = options || {};
const endpoint = buildAsrWsUrl(config.gatewayUrl, config.gatewayToken);
let socketTask = null;
let ready = false;
let closed = false;
let stopCloseTimer = null;
let readyTimer = null;
let readyResolve = null;
let readyReject = null;
function clearReadyTimer() {
if (readyTimer) {
clearTimeout(readyTimer);
readyTimer = null;
}
}
function clearStopCloseTimer() {
if (stopCloseTimer) {
clearTimeout(stopCloseTimer);
stopCloseTimer = null;
}
}
function rejectPendingReady(error) {
if (typeof readyReject === "function") {
readyReject(error);
}
readyResolve = null;
readyReject = null;
}
function resolvePendingReady() {
if (typeof readyResolve === "function") {
readyResolve();
}
readyResolve = null;
readyReject = null;
}
function sendJson(frame) {
if (!socketTask || closed) return;
socketTask.send({ data: JSON.stringify(frame) });
}
function connect() {
if (socketTask && !closed) {
return Promise.resolve();
}
closed = false;
ready = false;
return new Promise((resolve, reject) => {
readyResolve = resolve;
readyReject = reject;
socketTask = wx.connectSocket({ url: endpoint, timeout: READY_TIMEOUT_MS });
clearReadyTimer();
readyTimer = setTimeout(() => {
if (ready) return;
close("ready_timeout");
rejectPendingReady(new Error("语音网关连接超时"));
}, READY_TIMEOUT_MS);
socketTask.onOpen(() => {
if (typeof config.onOpen === "function") {
config.onOpen();
}
});
socketTask.onMessage((event) => {
const raw = event && event.data;
if (typeof raw !== "string") {
return;
}
let frame = null;
try {
frame = JSON.parse(raw);
} catch {
return;
}
if (frame.type === "ready") {
ready = true;
clearReadyTimer();
resolvePendingReady();
if (typeof config.onReady === "function") {
config.onReady(frame.payload || {});
}
return;
}
if (frame.type === "result") {
if (typeof config.onResult === "function") {
config.onResult(frame.payload || {});
}
return;
}
if (frame.type === "round_end") {
clearStopCloseTimer();
if (typeof config.onRoundEnd === "function") {
config.onRoundEnd(frame.payload || {});
}
return;
}
if (frame.type === "pong") {
return;
}
if (frame.type === "error") {
const message = String((frame.payload && frame.payload.message) || "语音网关异常");
const error = new Error(message);
if (!ready) {
clearReadyTimer();
rejectPendingReady(error);
}
if (typeof config.onError === "function") {
config.onError(error);
}
return;
}
});
socketTask.onError((event) => {
const detail = event && event.errMsg ? String(event.errMsg) : "";
const error = new Error(detail ? `语音网关连接失败: ${detail}` : "语音网关连接失败");
if (!ready) {
clearReadyTimer();
rejectPendingReady(error);
}
if (typeof config.onError === "function") {
config.onError(error);
}
});
socketTask.onClose((event) => {
const wasReady = ready;
ready = false;
closed = true;
clearReadyTimer();
clearStopCloseTimer();
socketTask = null;
if (!wasReady) {
const code = event && typeof event.code === "number" ? event.code : "";
const reason = event && event.reason ? String(event.reason) : "";
rejectPendingReady(
new Error(`语音连接已关闭${code ? `(${code})` : ""}${reason ? `: ${reason}` : ""}`)
);
}
if (typeof config.onClose === "function") {
config.onClose(event || {});
}
});
});
}
function startRound(payload) {
if (!ready) {
throw new Error("语音网关未就绪");
}
sendJson({
type: "start",
payload:
payload && typeof payload === "object"
? payload
: {
audio: {
format: "pcm",
codec: "raw",
rate: 16000,
bits: 16,
channel: 1
},
request: {
model_name: "bigmodel",
enable_itn: true,
enable_punc: true,
result_type: "full"
}
}
});
}
function sendAudio(buffer) {
if (!ready || !socketTask || closed) {
return;
}
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength <= 0) {
return;
}
socketTask.send({ data: buffer });
}
function stopRound() {
if (!ready) return;
sendJson({ type: "stop" });
clearStopCloseTimer();
stopCloseTimer = setTimeout(() => {
close("round_timeout");
}, CLOSE_TIMEOUT_MS);
}
function cancelRound() {
if (!ready) {
close("cancel_before_ready");
return;
}
sendJson({ type: "cancel" });
close("cancel");
}
function ping() {
if (!ready) return;
sendJson({ type: "ping" });
}
function close(reason) {
clearReadyTimer();
clearStopCloseTimer();
closed = true;
ready = false;
if (!socketTask) return;
try {
socketTask.close({ code: 1000, reason: reason || "client_close" });
} catch (error) {
console.warn("[asrGateway.close]", error);
}
socketTask = null;
}
return {
connect,
startRound,
sendAudio,
stopRound,
cancelRound,
ping,
close,
isReady() {
return ready;
}
};
}
module.exports = {
READY_TIMEOUT_MS,
CLOSE_TIMEOUT_MS,
buildAsrWsUrl,
createAsrGatewayClient
};