("zh");
const { resetOnboarding } = useOnboardingState();
const { setLanguage: setI18nLanguage } = useI18nPatch();
+ const { soundEnabled, setSoundEnabled, playToolcallSound } =
+ useSoundContext();
// 重新运行引导
const handleResetOnboarding = useCallback(() => {
@@ -217,6 +228,32 @@ export function GeneralSettings() {
+ {/* 音效 */}
+
+
+
+
+
+
音效
+
+ 工具调用和打字时播放提示音
+
+
+
+
{
+ setSoundEnabled(e.target.checked);
+ if (e.target.checked) {
+ playToolcallSound();
+ }
+ }}
+ className="w-4 h-4 rounded border-gray-300"
+ />
+
+
+
{/* 启动行为 */}
启动行为
diff --git a/src/contexts/SoundProvider.tsx b/src/contexts/SoundProvider.tsx
new file mode 100644
index 000000000..11349f75f
--- /dev/null
+++ b/src/contexts/SoundProvider.tsx
@@ -0,0 +1,16 @@
+/**
+ * @file SoundProvider.tsx
+ * @description 音效 Provider 组件
+ * @module contexts/SoundProvider
+ */
+
+import { ReactNode } from "react";
+import { useSound } from "../hooks/useSound";
+import { SoundContext } from "./soundContext";
+
+export function SoundProvider({ children }: { children: ReactNode }) {
+ const sound = useSound();
+ return (
+ {children}
+ );
+}
diff --git a/src/contexts/soundContext.ts b/src/contexts/soundContext.ts
new file mode 100644
index 000000000..7aa74b814
--- /dev/null
+++ b/src/contexts/soundContext.ts
@@ -0,0 +1,10 @@
+/**
+ * @file soundContext.ts
+ * @description 音效上下文定义
+ * @module contexts/soundContext
+ */
+
+import { createContext } from "react";
+import type { UseSoundReturn } from "../hooks/useSound";
+
+export const SoundContext = createContext(null);
diff --git a/src/contexts/useSoundContext.ts b/src/contexts/useSoundContext.ts
new file mode 100644
index 000000000..4a9b062e8
--- /dev/null
+++ b/src/contexts/useSoundContext.ts
@@ -0,0 +1,17 @@
+/**
+ * @file useSoundContext.ts
+ * @description 音效上下文 Hook
+ * @module contexts/useSoundContext
+ */
+
+import { useContext } from "react";
+import { SoundContext } from "./soundContext";
+import type { UseSoundReturn } from "../hooks/useSound";
+
+export function useSoundContext(): UseSoundReturn {
+ const context = useContext(SoundContext);
+ if (!context) {
+ throw new Error("useSoundContext must be used within a SoundProvider");
+ }
+ return context;
+}
diff --git a/src/hooks/README.md b/src/hooks/README.md
index 0fe14ef73..ca1ef6a1c 100644
--- a/src/hooks/README.md
+++ b/src/hooks/README.md
@@ -25,6 +25,7 @@ React 自定义 Hooks,封装业务逻辑和状态管理。
- `useProviderState.ts` - Provider 状态 Hook
- `useRelayRegistry.ts` - Relay Registry 管理 Hook(Requirements 2.1, 7.2, 7.3)
- `useSkills.ts` - 技能管理 Hook
+- `useSound.ts` - 音效管理 Hook(工具调用和打字机音效)
- `useSwitch.ts` - 开关状态 Hook
- `useTauri.ts` - Tauri 通用 Hook
- `useWindowResize.ts` - 窗口大小 Hook
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index cc6289054..db8444814 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -3,6 +3,8 @@ export { useConfigEvents } from "./useConfigEvents";
export { useOAuthPlugins, useSingleOAuthPlugin } from "./useOAuthPlugins";
export { useDeepLink } from "./useDeepLink";
export { useModelRegistry } from "./useModelRegistry";
+export { useSound } from "./useSound";
+export type { UseSoundReturn } from "./useSound";
export type {
ConnectPayload,
RelayInfo,
diff --git a/src/hooks/useSound.ts b/src/hooks/useSound.ts
new file mode 100644
index 000000000..a29d7dd47
--- /dev/null
+++ b/src/hooks/useSound.ts
@@ -0,0 +1,71 @@
+/**
+ * @file useSound.ts
+ * @description 音效管理 Hook,提供工具调用和打字机音效播放功能
+ * @module hooks/useSound
+ * @requires react
+ */
+
+import { useState, useEffect, useCallback, useRef } from "react";
+
+const STORAGE_KEY = "proxycast_sound_enabled";
+const SOUND_INTERVAL = 120; // 打字音效间隔 120ms
+
+export interface UseSoundReturn {
+ soundEnabled: boolean;
+ setSoundEnabled: (enabled: boolean) => void;
+ playToolcallSound: () => void;
+ playTypewriterSound: () => void;
+}
+
+export function useSound(): UseSoundReturn {
+ const [soundEnabled, setSoundEnabledState] = useState(() => {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ return stored === "true";
+ });
+
+ const toolcallAudioRef = useRef(null);
+ const typewriterAudioRef = useRef(null);
+ const lastSoundTimeRef = useRef(0);
+
+ // 初始化音频
+ useEffect(() => {
+ if (!toolcallAudioRef.current) {
+ toolcallAudioRef.current = new Audio("/sounds/tool-call.mp3");
+ toolcallAudioRef.current.volume = 1;
+ toolcallAudioRef.current.load();
+ }
+ if (!typewriterAudioRef.current) {
+ typewriterAudioRef.current = new Audio("/sounds/typing.mp3");
+ typewriterAudioRef.current.volume = 0.6;
+ typewriterAudioRef.current.load();
+ }
+ }, []);
+
+ const setSoundEnabled = useCallback((enabled: boolean) => {
+ setSoundEnabledState(enabled);
+ localStorage.setItem(STORAGE_KEY, String(enabled));
+ }, []);
+
+ const playToolcallSound = useCallback(() => {
+ if (!soundEnabled || !toolcallAudioRef.current) return;
+ toolcallAudioRef.current.currentTime = 0;
+ toolcallAudioRef.current.play().catch(console.error);
+ }, [soundEnabled]);
+
+ const playTypewriterSound = useCallback(() => {
+ const now = Date.now();
+ if (!soundEnabled || !typewriterAudioRef.current) return;
+ if (now - lastSoundTimeRef.current > SOUND_INTERVAL) {
+ typewriterAudioRef.current.currentTime = 0;
+ typewriterAudioRef.current.play().catch(console.error);
+ lastSoundTimeRef.current = now;
+ }
+ }, [soundEnabled]);
+
+ return {
+ soundEnabled,
+ setSoundEnabled,
+ playToolcallSound,
+ playTypewriterSound,
+ };
+}