chore(integration): snapshot local main worktree

This commit is contained in:
inman
2026-08-31 10:23:21 +08:00
parent 48a9189939
commit 33fb31fb28
116 changed files with 8108 additions and 3026 deletions

View File

@@ -0,0 +1,239 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
import { blobToBase64, convertAudioBlobTo16kMonoWav } from '@/lib/voice-recording';
import { transcribeWorksSpeech, WorksSquareApiError } from '@/lib/works-square';
import { useAuthStore } from '@/stores/auth';
import { getProfileAccountKey } from '@/stores/user-profile';
export type WorksVoiceInputState = 'idle' | 'recording' | 'transcribing';
interface UseWorksVoiceInputOptions {
enabled: boolean;
scopeKey: string | null;
onTranscript(text: string): void;
}
function isUnauthorizedWorksError(error: unknown): boolean {
if (error instanceof WorksSquareApiError && error.statusCode === 401) return true;
if (!(error instanceof Error)) return false;
const record = error as Error & { code?: unknown; details?: { status?: unknown } };
const message = error.message.toLowerCase();
return record.details?.status === 401
|| record.code === 'AUTH_INVALID'
|| message.includes('401')
|| message.includes('unauthorized')
|| message.includes('invalid access token');
}
function currentAuthAccountKey(): string | null {
const user = useAuthStore.getState().user;
return getProfileAccountKey(user?.userId ?? user?.username);
}
export function useWorksVoiceInput({
enabled,
scopeKey,
onTranscript,
}: UseWorksVoiceInputOptions) {
const accountKey = useAuthStore((state) => (
getProfileAccountKey(state.user?.userId ?? state.user?.username)
));
const accessToken = useAuthStore((state) => state.accessToken);
const getValidAccessToken = useAuthStore((state) => state.getValidAccessToken);
const refreshSession = useAuthStore((state) => state.refreshSession);
const [state, setState] = useState<WorksVoiceInputState>('idle');
const recorderRef = useRef<MediaRecorder | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const chunksRef = useRef<Blob[]>([]);
const operationRef = useRef(0);
const mountedRef = useRef(true);
const accountKeyRef = useRef(accountKey);
const scopeKeyRef = useRef(scopeKey);
const enabledRef = useRef(enabled);
const onTranscriptRef = useRef(onTranscript);
accountKeyRef.current = accountKey;
scopeKeyRef.current = scopeKey;
enabledRef.current = enabled;
onTranscriptRef.current = onTranscript;
const stopStream = useCallback(() => {
for (const track of streamRef.current?.getTracks() ?? []) track.stop();
streamRef.current = null;
}, []);
const cancel = useCallback(() => {
operationRef.current += 1;
const recorder = recorderRef.current;
if (recorder) {
recorder.ondataavailable = null;
recorder.onstop = null;
if (recorder.state !== 'inactive') recorder.stop();
recorderRef.current = null;
}
chunksRef.current = [];
stopStream();
if (mountedRef.current) setState('idle');
}, [stopStream]);
useEffect(() => () => {
mountedRef.current = false;
}, []);
useEffect(() => () => {
cancel();
}, [accountKey, cancel, enabled, scopeKey]);
const finishTranscription = useCallback(async (
mimeType: string,
operationId: number,
requestedAccountKey: string,
requestedScopeKey: string,
) => {
const isActive = () => mountedRef.current
&& operationRef.current === operationId
&& enabledRef.current
&& accountKeyRef.current === requestedAccountKey
&& scopeKeyRef.current === requestedScopeKey
&& currentAuthAccountKey() === requestedAccountKey;
const chunks = chunksRef.current;
chunksRef.current = [];
stopStream();
if (chunks.length === 0) {
if (isActive()) {
setState('idle');
toast.warning('没有录到声音');
}
return;
}
if (isActive()) setState('transcribing');
try {
if (!isActive()) return;
const validAccessToken = await getValidAccessToken();
if (!isActive()) return;
if (!validAccessToken) {
toast.error('请先登录后再使用语音输入');
return;
}
const audioBlob = new Blob(chunks, { type: mimeType || chunks[0]?.type || 'audio/webm' });
const wavBlob = await convertAudioBlobTo16kMonoWav(audioBlob);
if (!isActive()) return;
const audioBase64 = await blobToBase64(wavBlob);
if (!isActive()) return;
const transcribeWithToken = (token: string) => transcribeWorksSpeech({
accessToken: token,
audioBase64,
fileName: 'voice.wav',
mimeType: 'audio/wav',
language: 'zh',
});
let transcription;
try {
transcription = await transcribeWithToken(validAccessToken);
if (!isActive()) return;
} catch (error) {
if (!isUnauthorizedWorksError(error)) throw error;
if (!isActive()) return;
const refreshedAccessToken = await refreshSession();
if (!isActive()) return;
if (!refreshedAccessToken) throw error;
transcription = await transcribeWithToken(refreshedAccessToken);
if (!isActive()) return;
}
const transcript = transcription.text.trim();
if (transcript) onTranscriptRef.current(transcript);
} catch (error) {
if (isActive()) {
toast.error('语音识别失败', {
description: error instanceof Error ? error.message : String(error),
});
}
} finally {
if (isActive()) {
recorderRef.current = null;
setState('idle');
}
}
}, [getValidAccessToken, refreshSession, stopStream]);
const toggle = useCallback(async () => {
if (state === 'recording') {
const recorder = recorderRef.current;
if (!recorder || recorder.state === 'inactive') {
cancel();
return;
}
setState('transcribing');
recorder.stop();
return;
}
if (!enabled || !scopeKey || state !== 'idle') return;
if (!accountKey || !accessToken) {
toast.error('请先登录后再使用语音输入');
return;
}
if (!navigator.mediaDevices?.getUserMedia || typeof window.MediaRecorder === 'undefined') {
toast.error('当前环境不支持语音输入');
return;
}
const operationId = operationRef.current + 1;
operationRef.current = operationId;
const requestedAccountKey = accountKey;
const requestedScopeKey = scopeKey;
const isActive = () => mountedRef.current
&& operationRef.current === operationId
&& enabledRef.current
&& accountKeyRef.current === requestedAccountKey
&& scopeKeyRef.current === requestedScopeKey
&& currentAuthAccountKey() === requestedAccountKey;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
if (!isActive()) {
for (const track of stream.getTracks()) track.stop();
return;
}
streamRef.current = stream;
const preferredMimeType = window.MediaRecorder.isTypeSupported?.('audio/webm')
? 'audio/webm'
: '';
const recorder = new window.MediaRecorder(
stream,
preferredMimeType ? { mimeType: preferredMimeType } : undefined,
);
chunksRef.current = [];
recorderRef.current = recorder;
recorder.ondataavailable = (event) => {
if (isActive() && event.data.size > 0) chunksRef.current.push(event.data);
};
recorder.onstop = () => {
void finishTranscription(
recorder.mimeType || preferredMimeType || 'audio/webm',
operationId,
requestedAccountKey,
requestedScopeKey,
);
};
recorder.start();
setState('recording');
} catch (error) {
stopStream();
recorderRef.current = null;
chunksRef.current = [];
if (isActive()) {
setState('idle');
toast.error('无法开始录音', {
description: error instanceof Error ? error.message : String(error),
});
}
}
}, [accessToken, accountKey, cancel, enabled, finishTranscription, scopeKey, state, stopStream]);
const supported = typeof window.MediaRecorder !== 'undefined'
&& Boolean(navigator.mediaDevices?.getUserMedia);
const canToggle = state === 'recording' || (enabled && Boolean(scopeKey) && state === 'idle');
return { canToggle, state, supported, toggle };
}