diff --git a/CHANGELOG.md b/CHANGELOG.md index f09e6d2d8cb..a6638cf93a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Onboarding/CLI: restore terminal state without resuming paused `stdin`, so onboarding exits cleanly after choosing Web UI and the installer returns instead of appearing stuck. +- macOS Voice Wake: fix a crash in trigger trimming for CJK/Unicode transcripts by matching and slicing on original-string ranges instead of transformed-string indices. (#11052) Thanks @Flash-LHR. ## 2026.2.12 diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift b/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift index 5035357c870..7ef86c28507 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift @@ -735,12 +735,13 @@ actor VoiceWakeRuntime { } private static func trimmedAfterTrigger(_ text: String, triggers: [String]) -> String { - let lower = text.lowercased() for trigger in triggers { - let token = trigger.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) - guard !token.isEmpty, let range = lower.range(of: token) else { continue } - let after = range.upperBound - let trimmed = text[after...].trimmingCharacters(in: .whitespacesAndNewlines) + let token = trigger.trimmingCharacters(in: .whitespacesAndNewlines) + guard !token.isEmpty else { continue } + guard let range = text.range( + of: token, + options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive]) else { continue } + let trimmed = text[range.upperBound...].trimmingCharacters(in: .whitespacesAndNewlines) return String(trimmed) } return text diff --git a/apps/macos/Tests/OpenClawIPCTests/VoiceWakeRuntimeTests.swift b/apps/macos/Tests/OpenClawIPCTests/VoiceWakeRuntimeTests.swift index 3d92a32e095..89345914df6 100644 --- a/apps/macos/Tests/OpenClawIPCTests/VoiceWakeRuntimeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/VoiceWakeRuntimeTests.swift @@ -35,6 +35,18 @@ import Testing #expect(VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers)) } + @Test func trimsAfterChineseTriggerKeepsPostSpeech() { + let triggers = ["小爪", "openclaw"] + let text = "嘿 小爪 帮我打开设置" + #expect(VoiceWakeRuntime._testTrimmedAfterTrigger(text, triggers: triggers) == "帮我打开设置") + } + + @Test func trimsAfterTriggerHandlesWidthInsensitiveForms() { + let triggers = ["openclaw"] + let text = "OpenClaw 请帮我" + #expect(VoiceWakeRuntime._testTrimmedAfterTrigger(text, triggers: triggers) == "请帮我") + } + @Test func gateRequiresGapBetweenTriggerAndCommand() { let transcript = "hey openclaw do thing" let segments = makeSegments(