refactor: 优化代码质量,遵循 KISS 原则

- 移除自签证书回退逻辑,简化为仅使用 AnyIP 证书
- 删除 internal/tls/generate.go(不再需要)
- 重构 main.go:提取初始化逻辑,main() 从 156 行降至 13 行
- 重构 internal/ws/handler.go:提取消息处理,handleConn() 从 131 行降至 25 行
- 重构 internal/config/load.go:使用 map 驱动消除重复代码
- 优化前端 startRecording():使用标准 AbortController API
- 优化前端 showToast():预定义 DOM 元素,代码减少 50%

代码行数减少 90 行,复杂度显著降低,所有构建通过
This commit is contained in:
2026-03-02 00:25:14 +08:00
parent 8c7b9b45fd
commit b87fead2fd
8 changed files with 316 additions and 371 deletions

View File

@@ -29,7 +29,7 @@ interface AppState {
connected: boolean;
recording: boolean;
pendingStart: boolean;
startCancelled: boolean;
abortController: AbortController | null;
audioCtx: AudioContext | null;
workletNode: AudioWorkletNode | null;
stream: MediaStream | null;
@@ -65,7 +65,7 @@ const state: AppState = {
connected: false,
recording: false,
pendingStart: false,
startCancelled: false,
abortController: null,
audioCtx: null,
workletNode: null,
stream: null,
@@ -128,20 +128,19 @@ function connectWS(): void {
micBtn.disabled = false;
};
ws.onmessage = (e: MessageEvent) => handleServerMsg(e.data);
ws.onclose = () => {
state.connected = false;
state.ws = null;
micBtn.disabled = true;
if (state.recording) stopRecording();
// Clean up pending async start on disconnect
if (state.pendingStart) {
state.pendingStart = false;
state.startCancelled = true;
micBtn.classList.remove("recording");
}
setStatus("disconnected", "已断开");
scheduleReconnect();
};
ws.onclose = () => {
state.connected = false;
state.ws = null;
micBtn.disabled = true;
if (state.recording) stopRecording();
if (state.pendingStart) {
state.abortController?.abort();
state.pendingStart = false;
micBtn.classList.remove("recording");
}
setStatus("disconnected", "已断开");
scheduleReconnect();
};
ws.onerror = () => ws.close();
state.ws = ws;
}
@@ -199,25 +198,14 @@ function setPreview(text: string, isFinal: boolean): void {
previewBox.classList.toggle("active", !isFinal);
}
function showToast(msg: string): void {
let toast = document.getElementById("toast");
if (!toast) {
toast = document.createElement("div");
toast.id = "toast";
toast.style.cssText =
"position:fixed;bottom:calc(100px + var(--safe-bottom,0px));left:50%;" +
"transform:translateX(-50%);background:#222;color:#eee;padding:8px 18px;" +
"border-radius:20px;font-size:14px;z-index:999;opacity:0;transition:opacity .3s;";
document.body.appendChild(toast);
}
const toast = q("#toast");
toast.textContent = msg;
toast.style.opacity = "1";
clearTimeout(
(toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer,
);
(toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer =
setTimeout(() => {
toast.style.opacity = "0";
}, 2000);
toast.classList.add("show");
const timer = (toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer;
if (timer) clearTimeout(timer);
(toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer = setTimeout(() => {
toast.classList.remove("show");
}, 2000);
}
// ── Audio pipeline ──
async function initAudio(): Promise<void> {
@@ -234,19 +222,19 @@ async function initAudio(): Promise<void> {
async function startRecording(): Promise<void> {
if (state.recording || state.pendingStart) return;
state.pendingStart = true;
state.startCancelled = false;
const abortController = new AbortController();
state.abortController = abortController;
try {
await initAudio();
if (state.startCancelled) {
if (abortController.signal.aborted) {
state.pendingStart = false;
return;
}
const audioCtx = state.audioCtx as AudioContext;
// Ensure AudioContext is running (may suspend between recordings)
if (audioCtx.state === "suspended") {
await audioCtx.resume();
}
if (state.startCancelled) {
if (abortController.signal.aborted) {
state.pendingStart = false;
return;
}
@@ -257,7 +245,7 @@ async function startRecording(): Promise<void> {
channelCount: 1,
},
});
if (state.startCancelled) {
if (abortController.signal.aborted) {
stream.getTracks().forEach((t) => {
t.stop();
});
@@ -275,34 +263,33 @@ async function startRecording(): Promise<void> {
};
source.connect(worklet);
worklet.port.postMessage({ command: "start" });
// Don't connect worklet to destination (no playback)
state.workletNode = worklet;
state.pendingStart = false;
state.abortController = null;
state.recording = true;
sendJSON({ type: "start" });
micBtn.classList.add("recording");
setPreview("", false);
} catch (err) {
state.pendingStart = false;
state.abortController = null;
showToast(`麦克风错误: ${(err as Error).message}`);
}
}
function stopRecording(): void {
// Cancel pending async start if still initializing
if (state.pendingStart) {
state.startCancelled = true;
state.abortController?.abort();
state.abortController = null;
micBtn.classList.remove("recording");
return;
}
if (!state.recording) return;
state.recording = false;
// Stop worklet
if (state.workletNode) {
state.workletNode.port.postMessage({ command: "stop" });
state.workletNode.disconnect();
state.workletNode = null;
}
// Stop mic stream
if (state.stream) {
state.stream.getTracks().forEach((t) => {
t.stop();