Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 25 additions & 38 deletions openless-all/app/scripts/windows-ime-install-smoke.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ $KeyboardCategoryGuid = "{34745C63-B2F0-4784-8B67-5E12C8701A31}"
$ImmersiveCategoryGuid = "{13A016DF-560B-46CD-947A-4C3AF1E0E35D}"
$SystrayCategoryGuid = "{25504FB4-7BAB-4BC1-9C69-CF81890F0EF5}"

# Keep this script aligned with the backend status check and the TSF IPC path
# used by OpenLessImeSubmit-* named pipes.
$ExpectedBackendKeys = @(
# Keep this script aligned with the default Windows backend: OpenLess ships the
# TSF DLLs for optional diagnostics, but installers must not register the TIP.
$ExpectedUnregisteredKeys = @(
"Software\Classes\CLSID\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\InprocServer32",
"Software\WOW6432Node\Classes\CLSID\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\InprocServer32",
"Software\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\LanguageProfile\0x00000804\{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E}",
Expand Down Expand Up @@ -103,7 +103,7 @@ function Assert-RegistryKey {
Write-Host "[ok] $Label registry key present ($View)"
}

function Get-DefaultRegistryValue {
function Assert-RegistryKeyAbsent {
param(
[Parameter(Mandatory = $true)]
[Microsoft.Win32.RegistryView]$View,
Expand All @@ -114,54 +114,41 @@ function Get-DefaultRegistryValue {
)

$key = Open-LocalMachineSubKey -View $View -SubKey $SubKey
if ($null -eq $key) {
throw "Missing $Label registry key ($View): HKLM\$SubKey"
}
try {
$value = [string]$key.GetValue("")
if ([string]::IsNullOrWhiteSpace($value)) {
throw "$Label default registry value is empty ($View): HKLM\$SubKey"
}
return $value
} finally {
if ($null -ne $key) {
$key.Close()
throw "Unexpected $Label registry key ($View): HKLM\$SubKey"
}
Write-Host "[ok] $Label registry key absent ($View)"
}

function Assert-OpenLessImeInstalled {
$comKey = "Software\Classes\CLSID\$TextServiceClsid\InprocServer32"
$x64Dll = Get-DefaultRegistryValue -View Registry64 -SubKey $comKey -Label "x64 COM"
$x86Dll = Get-DefaultRegistryValue -View Registry32 -SubKey $comKey -Label "x86 COM"
$installRootCandidates = @(
(Join-Path $env:ProgramFiles "OpenLess")
)
if ($env:ProgramFiles -ne ${env:ProgramFiles(x86)}) {
$installRootCandidates += (Join-Path ${env:ProgramFiles(x86)} "OpenLess")
}

foreach ($dll in @($x64Dll, $x86Dll)) {
if (-not (Test-Path -LiteralPath $dll -PathType Leaf)) {
throw "Registered IME DLL path does not exist: $dll"
}
$installRoot = $installRootCandidates |
Where-Object { Test-Path -LiteralPath (Join-Path $_ "openless.exe") -PathType Leaf } |
Select-Object -First 1
if ([string]::IsNullOrWhiteSpace($installRoot)) {
throw "Installed OpenLess executable not found under: $($installRootCandidates -join ', ')"
}

$installRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $x64Dll))
$expectedX64 = Join-Path $installRoot "windows-ime\x64\OpenLessIme.dll"
$expectedX86 = Join-Path $installRoot "windows-ime\x86\OpenLessIme.dll"
if ($x64Dll -ne $expectedX64) {
throw "x64 COM DLL path points outside the installed IME directory. Expected '$expectedX64', got '$x64Dll'"
}
if ($x86Dll -ne $expectedX86) {
throw "x86 COM DLL path points outside the installed IME directory. Expected '$expectedX86', got '$x86Dll'"
}
if (-not (Test-Path -LiteralPath (Join-Path $installRoot "openless.exe") -PathType Leaf)) {
throw "Installed OpenLess executable not found under $installRoot"
foreach ($dll in @($expectedX64, $expectedX86)) {
if (-not (Test-Path -LiteralPath $dll -PathType Leaf)) {
throw "Packaged optional IME DLL path does not exist: $dll"
}
}

Assert-RegistryKey -View Registry64 -SubKey "Software\Microsoft\CTF\TIP\$TextServiceClsid\LanguageProfile\$LangId\$ProfileGuid" -Label "TSF language profile"
Assert-RegistryKey -View Registry64 -SubKey "Software\Microsoft\CTF\TIP\$TextServiceClsid\Category\Category\$KeyboardCategoryGuid\$TextServiceClsid" -Label "TSF keyboard category"
Assert-RegistryKey -View Registry64 -SubKey "Software\Microsoft\CTF\TIP\$TextServiceClsid\Category\Category\$ImmersiveCategoryGuid\$TextServiceClsid" -Label "TSF immersive category"
Assert-RegistryKey -View Registry64 -SubKey "Software\Microsoft\CTF\TIP\$TextServiceClsid\Category\Category\$SystrayCategoryGuid\$TextServiceClsid" -Label "TSF systray category"

foreach ($key in $ExpectedBackendKeys) {
Assert-RegistryKey -View Registry64 -SubKey $key -Label "backend-required"
foreach ($key in $ExpectedUnregisteredKeys) {
Assert-RegistryKeyAbsent -View Registry64 -SubKey $key -Label "default-disabled TSF"
}

Write-Host "[ok] Windows IME backend would report installed"
Write-Host "[ok] OpenLess installed with TSF registration disabled by default"
return $installRoot
}

Expand Down
38 changes: 18 additions & 20 deletions openless-all/app/scripts/windows-package-msvc.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ assert.deepEqual(tauriConfig.bundle.windows.wix.componentRefs, [
"OpenLessImeDllX86Component",
]);
assert.equal(tauriConfig.bundle.windows.nsis.installMode, "perMachine", "NSIS must force a machine-wide install because TSF registration is machine-wide");
assert.equal(tauriConfig.bundle.windows.nsis.installerHooks, "nsis/openless-ime-hooks.nsh", "NSIS must install and register the TSF DLLs");
assert.equal(tauriConfig.bundle.windows.nsis.installerHooks, "nsis/openless-ime-hooks.nsh", "NSIS must stage the optional TSF DLLs and clean legacy registration");

assert.match(imeSolution, /Release\|Win32/, "IME solution should include a Win32 Release configuration");
assert.match(imeProject, /Release\|Win32/, "IME project should include a Win32 Release configuration");
Expand All @@ -113,16 +113,16 @@ assert.match(wixFragment, /Component Id="OpenLessImeDllX64Component"/, "WiX frag
assert.match(wixFragment, /Component Id="OpenLessImeDllX86Component"/, "WiX fragment should define the x86 TSF DLL component");
assert.match(wixFragment, /Source="src-tauri\\target\\windows-ime-msvc\\x64\\Release\\OpenLessIme\.dll"/, "WiX fragment should consume the package-built x64 IME DLL");
assert.match(wixFragment, /Source="src-tauri\\target\\windows-ime-msvc\\x86\\Release\\OpenLessIme\.dll"/, "WiX fragment should consume the package-built x86 IME DLL");
assert.match(wixFragment, /regsvr32\.exe/, "MSI should register and unregister the TSF DLL");
assert.match(wixFragment, /\[System64Folder\]regsvr32\.exe/, "MSI should register the x64 IME with 64-bit regsvr32");
assert.match(wixFragment, /\[WindowsFolder\]SysWOW64\\regsvr32\.exe/, "MSI should register the x86 IME with 32-bit regsvr32");
assert.match(wixFragment, /RegisterOpenLessImeX64/, "MSI should register x64 OpenLess IME during install");
assert.match(wixFragment, /RegisterOpenLessImeX86/, "MSI should register x86 OpenLess IME during install");
assert.match(wixFragment, /UnregisterOpenLessImeX64/, "MSI should unregister x64 OpenLess IME during uninstall");
assert.match(wixFragment, /UnregisterOpenLessImeX86/, "MSI should unregister x86 OpenLess IME during uninstall");
assert.match(wixFragment, /regsvr32\.exe/, "MSI should retain regsvr32 custom actions for cleanup/diagnostics");
assert.match(wixFragment, /\[System64Folder\]regsvr32\.exe/, "MSI should know how to unregister the x64 IME with 64-bit regsvr32");
assert.match(wixFragment, /\[WindowsFolder\]SysWOW64\\regsvr32\.exe/, "MSI should know how to unregister the x86 IME with 32-bit regsvr32");
assert.doesNotMatch(wixFragment, /<Custom Action="RegisterOpenLessImeX64"/, "MSI should not register x64 OpenLess IME during install by default");
assert.doesNotMatch(wixFragment, /<Custom Action="RegisterOpenLessImeX86"/, "MSI should not register x86 OpenLess IME during install by default");
assert.match(wixFragment, /<Custom Action="UnregisterOpenLessImeX64"/, "MSI should clean legacy x64 OpenLess IME registration during upgrade");
assert.match(wixFragment, /<Custom Action="UnregisterOpenLessImeX86"/, "MSI should clean legacy x86 OpenLess IME registration during upgrade");

assert.match(nsisHook, /NSIS_HOOK_PREINSTALL/, "NSIS should copy IME DLLs before install completes");
assert.match(nsisHook, /NSIS_HOOK_POSTINSTALL/, "NSIS should register IME DLLs after files are installed");
assert.match(nsisHook, /NSIS_HOOK_POSTINSTALL/, "NSIS should define a postinstall hook");
assert.match(nsisHook, /NSIS_HOOK_PREUNINSTALL/, "NSIS should unregister IME DLLs before uninstall removes them");
assert.match(nsisHook, /OPENLESS_IME_STAGE_AND_REPLACE "x64" "OPENLESS_IME_DLL_X64"/, "NSIS should consume the CI-built x64 IME DLL");
assert.match(nsisHook, /OPENLESS_IME_STAGE_AND_REPLACE "x86" "OPENLESS_IME_DLL_X86"/, "NSIS should consume the CI-built x86 IME DLL");
Expand All @@ -131,10 +131,9 @@ assert.match(nsisHook, /File \/oname=OpenLessIme\.dll\.new "\$%\$\{ENV_VAR\}%"/,
assert.match(nsisHook, /Sysnative\\regsvr32\.exe/, "NSIS should use 64-bit regsvr32 for the x64 IME");
assert.match(nsisHook, /SysWOW64\\regsvr32\.exe/, "NSIS should use 32-bit regsvr32 for the x86 IME");
assert.match(nsisHook, /System32\\regsvr32\.exe[\s\S]*windows-ime\\x86\\OpenLessIme\.dll/, "NSIS should use System32 regsvr32 for the x86 IME on 32-bit Windows");
assert.match(nsisHook, /Abort/, "NSIS install should fail if TSF registration fails");
assert.match(nsisHook, /OPENLESS_IME_ABORT_IF_FAILED \$0 "x64 registration"/, "NSIS install should fail if x64 TSF registration fails");
assert.match(nsisHook, /OPENLESS_IME_ABORT_IF_FAILED \$0 "x86 registration"/, "NSIS install should fail if x86 TSF registration fails");
assert.match(nsisHook, /OPENLESS_IME_REGISTER_X86[\s\S]*\$\{If\} \$0 != 0[\s\S]*StrCpy \$1 \$0[\s\S]*OPENLESS_IME_UNREGISTER_X64[\s\S]*StrCpy \$0 \$1[\s\S]*OPENLESS_IME_ABORT_IF_FAILED \$0 "x86 registration"/, "NSIS install should roll back x64 registration before aborting on x86 registration failure");
assert.match(nsisHook, /TSF IME registration skipped/, "NSIS should skip default TSF registration");
assert.doesNotMatch(nsisHook, /NSIS_HOOK_POSTINSTALL[\s\S]*OPENLESS_IME_REGISTER_X64[\s\S]*!macroend/, "NSIS postinstall should not register the x64 TSF IME");
assert.doesNotMatch(nsisHook, /NSIS_HOOK_POSTINSTALL[\s\S]*OPENLESS_IME_REGISTER_X86[\s\S]*!macroend/, "NSIS postinstall should not register the x86 TSF IME");
assert.doesNotMatch(nsisHook, /OPENLESS_IME_ABORT_IF_FAILED \$0 "x64 unregistration"/, "NSIS uninstall should not fail if x64 TSF unregistration fails");
assert.doesNotMatch(nsisHook, /OPENLESS_IME_ABORT_IF_FAILED \$0 "x86 unregistration"/, "NSIS uninstall should not fail if x86 TSF unregistration fails");
assert.match(nsisHook, /OpenLess x64 TSF IME unregister exit code \$0/, "NSIS uninstall should log x64 TSF unregistration failures");
Expand All @@ -144,13 +143,12 @@ assert.match(imeInstallSmoke, /\[ValidateSet\("nsis", "msi"\)\]/, "install smoke
assert.match(imeInstallSmoke, /Join-ProcessArguments/, "install smoke should quote process arguments before Start-Process");
assert.match(imeInstallSmoke, /\$commandLine = Join-ProcessArguments \$ArgumentList/, "install smoke should build a single quoted command line");
assert.match(imeInstallSmoke, /Start-Process -FilePath \$FilePath -ArgumentList \$commandLine/, "install smoke should pass a single quoted command line to Start-Process");
assert.match(imeInstallSmoke, /OpenLessImeSubmit/, "install smoke should preserve TSF backend context");
assert.match(imeInstallSmoke, /Software\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/, "install smoke should check x64 COM registration");
assert.match(imeInstallSmoke, /Software\\WOW6432Node\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/, "install smoke should check x86 COM registration");
assert.match(imeInstallSmoke, /LanguageProfile\\0x00000804\\\{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E\}/, "install smoke should check the TSF language profile");
assert.match(imeInstallSmoke, /Category\\Category\\\{34745C63-B2F0-4784-8B67-5E12C8701A31\}/, "install smoke should check the keyboard TSF category");
assert.match(imeInstallSmoke, /foreach \(\$key in \$ExpectedBackendKeys\) \{[\s\S]*Assert-RegistryKey -View Registry64 -SubKey \$key[\s\S]*\}/, "install smoke should assert every backend-required registry key exists");
assert.doesNotMatch(imeInstallSmoke, /foreach \(\$key in \$ExpectedBackendKeys\) \{[\s\S]*Write-Host "\[trace\] backend-required key: HKLM\\\$key"[\s\S]*\}/, "install smoke must not only trace backend-required registry keys");
assert.match(imeInstallSmoke, /ExpectedUnregisteredKeys/, "install smoke should track TSF keys that must stay absent by default");
assert.match(imeInstallSmoke, /Software\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/, "install smoke should check x64 COM registration absence");
assert.match(imeInstallSmoke, /Software\\WOW6432Node\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/, "install smoke should check x86 COM registration absence");
assert.match(imeInstallSmoke, /LanguageProfile\\0x00000804\\\{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E\}/, "install smoke should check the TSF language profile absence");
assert.match(imeInstallSmoke, /Category\\Category\\\{34745C63-B2F0-4784-8B67-5E12C8701A31\}/, "install smoke should check the keyboard TSF category absence");
assert.match(imeInstallSmoke, /Assert-RegistryKeyAbsent -View Registry64 -SubKey \$key/, "install smoke should assert every default-disabled TSF registry key is absent");
assert.match(ciWorkflow, /windows-ime-install-smoke\.ps1[\s\S]*-InstallerKind nsis/, "CI should install and verify the NSIS artifact");
assert.match(ciWorkflow, /windows-ime-install-smoke\.ps1[\s\S]*-InstallerKind msi/, "CI should install and verify the MSI artifact");
assert.match(ciWorkflow, /InstallerKind nsis[\s\S]*\$LASTEXITCODE -ne 0[\s\S]*NSIS installer smoke failed/, "CI should fail immediately when the NSIS smoke run fails");
Expand Down
3 changes: 1 addition & 2 deletions openless-all/app/src-tauri/nsis/openless-ime-hooks.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,7 @@
!macroend

!macro NSIS_HOOK_POSTINSTALL
!insertmacro OPENLESS_IME_REGISTER_X64
!insertmacro OPENLESS_IME_REGISTER_X86
DetailPrint "OpenLess TSF IME registration skipped; Windows uses Unicode SendInput by default"
!macroend

!macro NSIS_HOOK_PREUNINSTALL
Expand Down
16 changes: 3 additions & 13 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1765,19 +1765,13 @@ fn insert_via_non_tsf_fallback(

match status {
InsertStatus::Inserted => {
log::warn!(
"[windows-ime] TSF unavailable; inserted via paced Unicode SendInput fallback"
);
log::info!("[windows-ime] inserted via paced Unicode SendInput");
}
InsertStatus::CopiedFallback => {
log::warn!(
"[windows-ime] TSF unavailable; Unicode SendInput failed, left text on clipboard"
);
log::warn!("[windows-ime] Unicode SendInput failed, left text on clipboard");
}
InsertStatus::PasteSent | InsertStatus::Failed => {
log::warn!(
"[windows-ime] TSF unavailable; Unicode SendInput fallback failed and copy fallback failed"
);
log::warn!("[windows-ime] Unicode SendInput failed and copy fallback failed");
}
}

Expand Down Expand Up @@ -1861,8 +1855,6 @@ mod non_tsf_fallback_tests {

// ─────────────────────────── helpers ───────────────────────────



fn read_whisper_credentials() -> (String, String, String) {
let api_key = CredentialsVault::get(CredentialAccount::AsrApiKey)
.ok()
Expand Down Expand Up @@ -1959,7 +1951,6 @@ fn enabled_hotwords(inner: &Arc<Inner>) -> Vec<DictionaryHotword> {
.collect()
}


/// 读 Gemini 凭据。所有 LLM provider 共用 ark.* 槽位(persistence 没做 per-provider
/// 隔离),所以这里也是从 `ArkApiKey` / `ArkModelId` / `ArkEndpoint` 三个槽读,
/// 但回退默认值改成谷歌的:base_url 默认 `https://generativelanguage.googleapis.com/v1beta`,
Expand Down Expand Up @@ -3037,7 +3028,6 @@ fn schedule_capsule_idle(inner: &Arc<Inner>, delay_ms: u64) {
});
}


// ─────────────────────────── audio bridge ───────────────────────────

struct DeferredAsrBridge {
Expand Down
47 changes: 29 additions & 18 deletions openless-all/app/src-tauri/src/coordinator/dictation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,10 +1004,7 @@ pub(super) async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {

/// begin_session 的带参版本,voice_agent=true 时在 Starting 阶段就标记好,
/// 防止 finish_starting_session 处理 pending_stop 时丢失标志。
pub(super) async fn begin_session_as(
inner: &Arc<Inner>,
voice_agent: bool,
) -> Result<(), String> {
pub(super) async fn begin_session_as(inner: &Arc<Inner>, voice_agent: bool) -> Result<(), String> {
let current_session_id = {
let mut state = inner.state.lock();
let Some(session_id) =
Expand All @@ -1025,9 +1022,13 @@ pub(super) async fn begin_session_as(
};
#[cfg(target_os = "windows")]
{
let prepared = inner.windows_ime.prepare_session();
let mut slots = inner.prepared_windows_ime_session.lock();
store_prepared_windows_ime_session(&mut slots, current_session_id, prepared);
if inner.prefs.get().windows_tsf_backend_enabled {
let prepared = inner.windows_ime.prepare_session();
let mut slots = inner.prepared_windows_ime_session.lock();
store_prepared_windows_ime_session(&mut slots, current_session_id, prepared);
} else {
log::debug!("[windows-ime] TSF backend disabled; using Unicode SendInput insertion");
}
}
// 翻译模式标志重置;hotkey 监听器在 Shift down 时再 set true。
inner
Expand Down Expand Up @@ -2301,6 +2302,7 @@ pub(super) async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
let prefs = inner.prefs.get();
let restore_clipboard = prefs.restore_clipboard_after_paste;
let allow_non_tsf_insertion_fallback = prefs.allow_non_tsf_insertion_fallback;
let windows_tsf_backend_enabled = prefs.windows_tsf_backend_enabled;
let paste_shortcut = prefs.paste_shortcut;
// 流式路径下,字符已经通过 Unicode keystroke 落到光标处,跳过 inserter.insert。
let status = if already_streamed {
Expand All @@ -2323,17 +2325,26 @@ pub(super) async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
if focus_ready_for_paste {
#[cfg(target_os = "windows")]
{
let ime_target = capture_ime_submit_target();
insert_with_windows_ime_first(
inner,
current_session_id,
&polished,
restore_clipboard,
allow_non_tsf_insertion_fallback,
paste_shortcut,
ime_target,
)
.await
if windows_tsf_backend_enabled {
let ime_target = capture_ime_submit_target();
insert_with_windows_ime_first(
inner,
current_session_id,
&polished,
restore_clipboard,
allow_non_tsf_insertion_fallback,
paste_shortcut,
ime_target,
)
.await
} else if allow_non_tsf_insertion_fallback {
insert_via_non_tsf_fallback(inner, &polished, restore_clipboard, paste_shortcut)
} else {
log::warn!(
"[windows-ime] TSF backend disabled and Unicode insertion fallback disabled"
);
InsertStatus::Failed
}
}
#[cfg(not(target_os = "windows"))]
{
Expand Down
Loading