add: 增加任务完成的提示音

This commit is contained in:
2026-04-26 13:10:14 +08:00
parent ce10295e89
commit 3c0d98f4cc
20 changed files with 265 additions and 22 deletions

View File

@@ -3,11 +3,13 @@
import { useCallback, useEffect, useMemo, useState } from "react"
import {
ArrowUpCircle,
Bell,
CheckCircle2,
Languages,
Loader2,
MonitorCog,
RefreshCw,
Volume2,
Wifi,
} from "lucide-react"
import { Github } from "@lobehub/icons"
@@ -31,9 +33,11 @@ import {
import {
getSystemProxySettings,
getSystemRenderingSettings,
getSystemNotificationSettings,
updateSystemLanguageSettings,
updateSystemProxySettings,
updateSystemRenderingSettings,
updateSystemNotificationSettings,
} from "@/lib/api"
import { isDesktop, openUrl } from "@/lib/platform"
import type { AppLocale } from "@/lib/types"
@@ -96,6 +100,9 @@ export function SystemNetworkSettings() {
)
const renderingDirty =
processStartLoaded && persistedDisableHwAccel !== processStartDisableHwAccel
const [notifTaskCompletion, setNotifTaskCompletion] = useState(true)
const [notifSoundEnabled, setNotifSoundEnabled] = useState(true)
const [savingNotification, setSavingNotification] = useState(false)
const [currentVersion, setCurrentVersion] = useState<string>("")
const [availableUpdate, setAvailableUpdate] = useState<Update | null>(null)
const [checkingUpdate, setCheckingUpdate] = useState(false)
@@ -171,13 +178,15 @@ export function SystemNetworkSettings() {
setLoadError(null)
try {
const [proxySettings, version, renderingSettings] = await Promise.all([
getSystemProxySettings(),
getCurrentAppVersion(),
renderingSettingsLoadable
? getSystemRenderingSettings()
: Promise.resolve(null),
])
const [proxySettings, version, renderingSettings, notificationSettings] =
await Promise.all([
getSystemProxySettings(),
getCurrentAppVersion(),
renderingSettingsLoadable
? getSystemRenderingSettings()
: Promise.resolve(null),
getSystemNotificationSettings(),
])
setEnabled(proxySettings.enabled)
setProxyUrl(proxySettings.proxy_url ?? "")
@@ -191,6 +200,10 @@ export function SystemNetworkSettings() {
setProcessStartLoaded(true)
}
}
if (notificationSettings) {
setNotifTaskCompletion(notificationSettings.task_completion)
setNotifSoundEnabled(notificationSettings.sound_enabled)
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setLoadError(message)
@@ -291,6 +304,26 @@ export function SystemNetworkSettings() {
[languageSettings.language, setLanguageSettings, t]
)
const saveNotificationSettings = useCallback(
async (taskCompletion: boolean, soundEnabled: boolean) => {
setSavingNotification(true)
try {
const result = await updateSystemNotificationSettings({
task_completion: taskCompletion,
sound_enabled: soundEnabled,
})
setNotifTaskCompletion(result.task_completion)
setNotifSoundEnabled(result.sound_enabled)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("notificationSaveFailed", { message }))
} finally {
setSavingNotification(false)
}
},
[t]
)
const formatUpdateError = useCallback(
(error: unknown, action: UpdateAction): string => {
const { kind, rawMessage } = normalizeAppUpdateError(error)
@@ -716,6 +749,46 @@ export function SystemNetworkSettings() {
</Select>
</div>
</section>
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Bell className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("notificationTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("notificationDescription")}
</p>
<label className="inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={notifTaskCompletion}
disabled={savingNotification}
onChange={(event) => {
const next = event.target.checked
setNotifTaskCompletion(next)
saveNotificationSettings(next, notifSoundEnabled)
}}
/>
{t("notificationTaskCompletion")}
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={notifSoundEnabled}
disabled={savingNotification || !notifTaskCompletion}
onChange={(event) => {
const next = event.target.checked
setNotifSoundEnabled(next)
saveNotificationSettings(notifTaskCompletion, next)
}}
/>
<Volume2 className="h-3.5 w-3.5 text-muted-foreground" />
{t("notificationSoundEnabled")}
</label>
</section>
</div>
</ScrollArea>
)

View File

@@ -22,6 +22,7 @@ import {
acpCancel,
acpRespondPermission,
acpDisconnect,
getSystemNotificationSettings,
} from "@/lib/api"
import type {
AgentType,
@@ -1364,6 +1365,19 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
pushAlertRef.current = pushAlert
}, [pushAlert])
// Notification settings (loaded once, refreshed lazily)
const notificationSettingsRef = useRef({
task_completion: true,
sound_enabled: true,
})
useEffect(() => {
getSystemNotificationSettings()
.then((s) => {
notificationSettingsRef.current = s
})
.catch(() => {})
}, [])
// Ref-based store — mutations don't trigger React state updates
const storeRef = useRef<InternalStore>({
connections: new Map(),
@@ -1948,14 +1962,16 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
}
// Send OS notification when window is not focused
{
const ns = notificationSettingsRef.current
const nc = storeRef.current.connections.get(contextKey)
if (nc) {
if (nc && ns.task_completion) {
const agentLabel = AGENT_LABELS[nc.agentType]
const fn = folderNameRef.current
const title = fn ? `${fn} - Codeg` : "Codeg"
sendSystemNotification(
title,
t("notificationTurnComplete", { agent: agentLabel })
t("notificationTurnComplete", { agent: agentLabel }),
{ sound: ns.sound_enabled }
).catch(() => {})
}
}

View File

@@ -145,7 +145,12 @@
"downloadFailed": "فشل تنزيل حزمة التحديث. يرجى المحاولة مرة أخرى لاحقًا.",
"installFailed": "فشل تثبيت التحديث. يرجى إغلاق التطبيق ثم إعادة المحاولة.",
"unknown": "فشل التحديث. يرجى المحاولة مرة أخرى لاحقًا."
}
},
"notificationTitle": "الإشعارات",
"notificationDescription": "تكوين إشعارات إتمام المهام وتنبيهات الصوت.",
"notificationTaskCompletion": "إشعار عند إتمام المهمة",
"notificationSoundEnabled": "تشغيل صوت الإشعار",
"notificationSaveFailed": "فشل حفظ إعدادات الإشعارات: {message}"
},
"VersionControlSettings": {
"loading": "جارٍ التحميل...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "Das Update-Paket konnte nicht heruntergeladen werden. Bitte später erneut versuchen.",
"installFailed": "Das Update konnte nicht installiert werden. Bitte App schließen und erneut versuchen.",
"unknown": "Update fehlgeschlagen. Bitte später erneut versuchen."
}
},
"notificationTitle": "Benachrichtigungen",
"notificationDescription": "Benachrichtigungen bei Aufgabenerledigung und Soundalarme konfigurieren.",
"notificationTaskCompletion": "Bei Aufgabenerledigung benachrichtigen",
"notificationSoundEnabled": "Benachrichtigungston abspielen",
"notificationSaveFailed": "Fehler beim Speichern der Benachrichtigungseinstellungen: {message}"
},
"VersionControlSettings": {
"loading": "Laden...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "Failed to download update package. Please try again later.",
"installFailed": "Failed to install update. Please close the app and try again.",
"unknown": "Update failed. Please try again later."
}
},
"notificationTitle": "Notifications",
"notificationDescription": "Configure task completion notifications and sound alerts.",
"notificationTaskCompletion": "Notify on task completion",
"notificationSoundEnabled": "Play notification sound",
"notificationSaveFailed": "Failed to save notification settings: {message}"
},
"VersionControlSettings": {
"loading": "Loading...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "No se pudo descargar el paquete de actualización. Inténtalo más tarde.",
"installFailed": "No se pudo instalar la actualización. Cierra la app e inténtalo de nuevo.",
"unknown": "La actualización falló. Inténtalo más tarde."
}
},
"notificationTitle": "Notificaciones",
"notificationDescription": "Configurar notificaciones de finalización de tareas y alertas de sonido.",
"notificationTaskCompletion": "Notificar al completar tarea",
"notificationSoundEnabled": "Reproducir sonido de notificación",
"notificationSaveFailed": "Error al guardar configuración de notificaciones: {message}"
},
"VersionControlSettings": {
"loading": "Cargando...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "Impossible de télécharger le paquet de mise à jour. Veuillez réessayer plus tard.",
"installFailed": "Impossible dinstaller la mise à jour. Fermez lapp puis réessayez.",
"unknown": "La mise à jour a échoué. Veuillez réessayer plus tard."
}
},
"notificationTitle": "Notifications",
"notificationDescription": "Configurer les notifications de fin de tâche et les alertes sonores.",
"notificationTaskCompletion": "Notifier à la fin de la tâche",
"notificationSoundEnabled": "Jouer un son de notification",
"notificationSaveFailed": "Échec de l'enregistrement des paramètres de notification : {message}"
},
"VersionControlSettings": {
"loading": "Chargement...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "更新パッケージのダウンロードに失敗しました。しばらくしてから再試行してください。",
"installFailed": "更新のインストールに失敗しました。アプリを閉じて再試行してください。",
"unknown": "更新に失敗しました。しばらくしてから再試行してください。"
}
},
"notificationTitle": "通知",
"notificationDescription": "タスク完了通知とサウンドアラートを設定します。",
"notificationTaskCompletion": "タスク完了時に通知する",
"notificationSoundEnabled": "通知音を再生する",
"notificationSaveFailed": "通知設定の保存に失敗しました:{message}"
},
"VersionControlSettings": {
"loading": "読み込み中...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "업데이트 패키지 다운로드에 실패했습니다. 잠시 후 다시 시도하세요.",
"installFailed": "업데이트 설치에 실패했습니다. 앱을 종료한 뒤 다시 시도하세요.",
"unknown": "업데이트에 실패했습니다. 잠시 후 다시 시도하세요."
}
},
"notificationTitle": "알림",
"notificationDescription": "작업 완료 알림 및 소리 알림을 설정합니다.",
"notificationTaskCompletion": "작업 완료 시 알림",
"notificationSoundEnabled": "알림 소리 재생",
"notificationSaveFailed": "알림 설정 저장 실패: {message}"
},
"VersionControlSettings": {
"loading": "로딩 중...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "Falha ao baixar o pacote de atualização. Tente novamente mais tarde.",
"installFailed": "Falha ao instalar a atualização. Feche o app e tente novamente.",
"unknown": "Falha na atualização. Tente novamente mais tarde."
}
},
"notificationTitle": "Notificações",
"notificationDescription": "Configurar notificações de conclusão de tarefas e alertas sonoros.",
"notificationTaskCompletion": "Notificar ao concluir tarefa",
"notificationSoundEnabled": "Reproduzir som de notificação",
"notificationSaveFailed": "Falha ao salvar configurações de notificação: {message}"
},
"VersionControlSettings": {
"loading": "Carregando...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "下载更新包失败,请稍后重试。",
"installFailed": "安装更新失败,请关闭应用后重试。",
"unknown": "更新失败,请稍后重试。"
}
},
"notificationTitle": "通知",
"notificationDescription": "配置任务完成通知和声音提醒。",
"notificationTaskCompletion": "任务完成时通知",
"notificationSoundEnabled": "播放通知声音",
"notificationSaveFailed": "保存通知设置失败:{message}"
},
"VersionControlSettings": {
"loading": "加载中...",

View File

@@ -145,7 +145,12 @@
"downloadFailed": "下載更新包失敗,請稍後再試。",
"installFailed": "安裝更新失敗,請關閉應用後重試。",
"unknown": "更新失敗,請稍後再試。"
}
},
"notificationTitle": "通知",
"notificationDescription": "設定任務完成通知和聲音提醒。",
"notificationTaskCompletion": "任務完成時通知",
"notificationSoundEnabled": "播放通知聲音",
"notificationSaveFailed": "儲存通知設定失敗:{message}"
},
"VersionControlSettings": {
"loading": "載入中...",

View File

@@ -48,6 +48,7 @@ import type {
SystemLanguageSettings,
SystemProxySettings,
SystemRenderingSettings,
SystemNotificationSettings,
GitCredentials,
GitDetectResult,
PackageManagerInfo,
@@ -469,6 +470,16 @@ export async function updateSystemRenderingSettings(
return getTransport().call("update_system_rendering_settings", { settings })
}
export async function getSystemNotificationSettings(): Promise<SystemNotificationSettings> {
return getTransport().call("get_system_notification_settings")
}
export async function updateSystemNotificationSettings(
settings: SystemNotificationSettings
): Promise<SystemNotificationSettings> {
return getTransport().call("update_system_notification_settings", { settings })
}
// --- Version Control ---
export async function detectGit(): Promise<GitDetectResult> {

View File

@@ -1,9 +1,31 @@
import { getTransport } from "./transport"
import { isDesktop } from "./transport"
let cachedAudio: HTMLAudioElement | null = null
function getNotificationAudio(): HTMLAudioElement {
if (!cachedAudio) {
cachedAudio = new Audio("/sounds/notification.wav")
}
return cachedAudio
}
export function playNotificationSound(): void {
try {
const audio = getNotificationAudio()
audio.currentTime = 0
audio.play().catch(() => {
// Autoplay may be blocked; silently ignore
})
} catch {
// Ignore audio playback errors
}
}
export async function sendSystemNotification(
title: string,
body: string
body: string,
options?: { sound?: boolean }
): Promise<void> {
if (!document.hidden) return
if (isDesktop()) {
@@ -19,4 +41,8 @@ export async function sendSystemNotification(
}
}
}
if (options?.sound !== false) {
playNotificationSound()
}
}

View File

@@ -638,6 +638,11 @@ export interface SystemRenderingSettings {
disable_hardware_acceleration: boolean
}
export interface SystemNotificationSettings {
task_completion: boolean
sound_enabled: boolean
}
// --- Version Control ---
export interface GitCredentials {