Commit Graph
275 Commits
Author SHA1 Message Date
baiqing 81fa80da69 fix(windows): NSIS x64 dll 用 Sysnative 绕过 WOW64 重定向
NSIS installer 是 32-bit 进程,进程内的 $SYSDIR / $WINDIR\System32 都因
WOW64 文件系统重定向变成 $WINDIR\SysWOW64,跑的是 32-bit regsvr32 → x64 dll
被注册到 HKLM\Software\Wow6432Node\Classes\CLSID(KEY_WOW64_32KEY 视图)。
但 windows_ime_profile.rs 用 KEY_WOW64_64KEY 校验时只看 HKLM\Software\Classes
\CLSID,永远查不到 → 仍判 RegistrationBroken("OpenLess COM registration is
missing")。

修复:x64 改用 $WINDIR\Sysnative\regsvr32.exe — 这是 32-bit 进程访问真
64-bit System32 的专用 alias,写入会落在 KEY_WOW64_64KEY 视图。x86 保持
SysWOW64 不变(确实需要 32-bit regsvr32 写 Wow6432Node)。
2026-05-06 10:52:12 +08:00
baiqing c52658b08b fix(windows): NSIS 也注册 32-bit TSF DLL — 修 P1 review
Codex review 发现:windows_ime_profile.rs::inspect_windows_ime_registration() 同时
用 KEY_WOW64_64KEY 和 KEY_WOW64_32KEY 两次检查 HKLM\Software\Classes\CLSID\{...}\
InprocServer32,少了任一边就返回 RegistrationBroken("OpenLess 32-bit COM
registration is missing")。MSI 流程通过 wxs 同时跑了 64-bit 和 SysWOW64\regsvr32
注册两份;之前的 NSIS hook 只注册了 x64,所以 NSIS / updater 用户依然在 Settings
页看到「不可用」。

修复:
- bundle.resources 拆出 x64 / x86 两份 dll target;x64 → tsf-ime\x64,x86 → tsf-ime\x86
- installer.nsh PostInstall/PreUninstall 增加 x86 分支,
  x86 用 $WINDIR\SysWOW64\regsvr32.exe(写 KEY_WOW64_32KEY 必需)
- CI Build Windows IME native DLLs 步骤把复制 dll 移到循环里,x64 + x86 都覆盖
  仓库占位
- 仓库新增 0 字节 x86 占位 dll,让本地 mac/linux 构建也能 resolve resources
2026-05-06 10:47:03 +08:00
baiqing 5aec74653b fix(windows): NSIS 安装包注册 OpenLess TSF DLL — 修"输入法后端:不可用"
## 现象
1.2.15 起在 Settings 页"Windows 输入法后端"出现 Pill「不可用」。`get_windows_ime_status()`
查 HKLM 注册表四件套(COM CLSID + TSF LanguageProfile + 三个 Category),缺一即报 NotInstalled。

## 根因
- MSI 通过 wix/openless-ime.wxs 的 RegisterOpenLessIme* CustomAction 跑 regsvr32 → 注册齐全。
- NSIS 包既没把 OpenLessIme.dll 打进去(wxs.componentRefs 是 MSI-only),也没 hook 跑
  regsvr32 → 用 setup.exe 安装的用户、走 updater 自动升级的存量用户全部命中"不可用"。
- 由于 updater latest-windows-x86_64.json 是 NSIS pass 写的,老 MSI 用户每次更新后也
  会被替换为没 TSF 的 NSIS 版本。

## 修复
- 加 src-tauri/installer.nsh:NSIS_HOOK_POSTINSTALL/PREUNINSTALL 跑 regsvr32 注册/反注册。
- bundle.resources map 把 OpenLessIme.dll 拷贝到 $INSTDIR\tsf-ime\OpenLessIme.dll
  (target 路径避开 wxs File 已声明的 windows-ime\x64\,避免 MSI component 路径冲突)。
- bundle.windows.nsis.installMode = "perMachine",让 NSIS 以管理员身份运行(写 HKLM 必需)。
- CI 在 build native dll 后把真 dll 复制到 src-tauri/openless-ime-payload/x64/OpenLessIme.dll;
  仓库 commit 0 字节占位文件,让本地 mac 端 tauri build 也能 resolve resources 路径。
- build-mac.sh 与 Linux build 步骤加 --config '{"bundle":{"resources":{}}}' 跳过 Windows-only
  的 dll 占位,避免污染 mac .app / Linux .deb/.rpm/.AppImage。

## Trade-off
- NSIS perMachine 升级需要 UAC,老 currentUser 用户首次升级时多一次 UAC 提示。
- MSI 包里 dll 会有两份(wxs File + bundle.resources),磁盘冗余 ~几百 KB,无功能影响。
2026-05-06 10:41:07 +08:00
baiqing 463a6d9716 fix(local-asr): 切走/删模型/手动释放时真正归还 RAM
三个问题合修:

1. 用户点"释放"按钮后活动监视器看 RSS 不下降。原因:encoder f32 weights
   是 malloc 出来的几百 MB,free 后 macOS libmalloc 不会立即把页归还内核。
   修:drop QwenAsrEngine 后调 malloc_zone_pressure_relief(NULL, 0),主动
   归还。decoder bf16 走 mmap 不依赖这个。

2. 切回云端 ASR 后本地引擎仍驻留。set_active_asr_provider 切到非 local
   时未释放,1.2GB+ RAM 一直留到进程退出。

3. 删除已下载的模型时未释放内存里加载的同 id 引擎,留下 mmap 指向已
   unlink 文件 + RAM 不归还的双重问题。

Windows 端本期不上线(cache.rs macOS cfg 块包住改动)。
2026-05-05 20:20:46 +08:00
baiqing cd00ebc584 fix(local-asr): 修 PR #262 review 提的两个 bug
(1) 小文件路径 try_download_range_append 的 retry 污染:
  原实现用 append 模式打开 .partial。若上一次 attempt 已写了部分字节后失败,
  retry 拿到的还是完整 chunk → append → 文件比应有大小多 N 字节,永久损坏。
  改成 truncate(effective_start) + seek 写入,每次 attempt 从干净起点重写。
  小文件(≤ 32MB)每个 chunk 是整个文件,重写无成本。

(2) 多文件并发一个失败不 abort 其它 worker:
  原代码只 log "signaling others to stop",没真 set cancel flag。剩下的
  workers 会继续吃带宽直到全部完成才把错误冒上来。
  现在错误时主动 cancel.store(true),再用 self_aborted 标记区分这是"我们
  因错误 abort"还是"用户主动 cancel"——前者 emit Failed,后者 emit Cancelled,
  避免错误被误报成用户取消。

跟踪 PR: #262 review by github-actions PR Reviewer Guide
2026-05-05 12:59:01 +08:00
TRIP 438fce26ab Merge pull request #262 from appergb/feat/local-asr-qwen3
feat(local-asr): 加本地 Qwen3-ASR 引擎(macOS 优先)
2026-05-05 12:53:35 +08:00
baiqing 8c3e4aca0d Merge remote-tracking branch 'origin/main' into feat/local-asr-qwen3
# Conflicts:
#	openless-all/app/src-tauri/src/coordinator.rs
2026-05-05 12:48:13 +08:00
Chris233 a0cc88d2ac Merge pull request #260 from weikeyi/capsule-confirm-overflow
fix(capsule): keep confirm icon inside pill on done/error states
2026-05-05 12:46:39 +08:00
Chris233 f984dec84b Merge pull request #261 from sora520/fix/windows-hotkey-trigger-leak
fix(windows): suppress consumed hotkey trigger events
2026-05-05 12:42:27 +08:00
Chris233 3e1ba69bfc Merge pull request #252 from Cooper-X-Oak/fix/recorder-timeout-238
fix(recorder): 添加 liveness watchdog 和全局超时保护以修复 #238
2026-05-05 12:32:37 +08:00
kaku.ki 9c0ae1495a fix(windows): suppress consumed hotkey trigger events 2026-05-05 13:17:50 +09:00
baiqing 520787b0f7 feat(local-asr): 引擎内存常驻 + 切到本地时预加载 + 释放时机可配
用户痛点:之前每次按 hotkey 都重加载 1.2GB 模型 → 首句词延迟 3-5s。

新流程:
- 新 LocalAsrCache:模型一次 load 后驻留内存,跨多次会话复用
- LocalQwenAsr::new 改成接受 Arc<QwenAsrEngine>(引擎所有权挪到 cache)
- build_local_qwen3 改成 async + spawn_blocking 走 cache.get_or_load()
- 切换 active model 时自动 drop 旧引擎再加载新的
- end_session 后调 cache.touch() + schedule_local_asr_release
  - keep_loaded_secs == 0 → 立即释放
  - keep_loaded_secs > 0 → spawn 一个 sleep+check 任务,到点查 last_used,
    若中间又被使用过则跳过释放

预加载:
- commands::set_active_asr_provider("local-qwen3") 触发 preload_in_background
- 用户在「模型设置」页可手动「立即加载」/「立即释放」
- 实时显示「内存中的引擎: <model id>(约占 1.2-3.4 GB 内存)」
- 5 秒轮询刷新状态

新设置项:
- prefs.local_asr_keep_loaded_secs (u32,默认 300)
- UI 选项:说完话立即释放 / 1 min / 5 min(默认)/ 30 min / 始终保留
- 4 个新 IPC:local_asr_engine_status / release_engine / preload /
  set_keep_loaded_secs
2026-05-05 12:09:55 +08:00
baiqing 8ed674ecaf fix(local-asr): 多文件并发 + 8MB 小块 + aria2 UA — 对齐 hfd/hf_xet 实测
参考 hf-mirror 官方推荐工具 hfd(aria2 包装)+ hf_xet 默认 16 并发:
- PARALLEL_FILES = 3:原本顺序下文件,大 .safetensors 在跑时 6 个小文件
  全在排队;改成最多 3 个文件并发,让小文件不阻塞、大文件 throttle
  时剩余带宽喂别的文件
- PARALLEL chunks per file = 4 → 8:贴 hf_xet 默认 (16) 的折中
- CHUNK_SIZE = 32MB → 8MB:单连接寿命缩到 5-20s(CDN throttle 临界点之下),
  失败重做成本降 4 倍
- User-Agent: openless/x → aria2/1.36.0:实测 aria2 UA 在 HF / hf-mirror
  反滥用规则里走白名单;自定义 UA 在 sustained 传输后会被切流
  (日志里大量 "end of file before message length reached" 就是这个)

进度计算改成多文件汇总:每个文件 in_flight bytes 用 AtomicU64,全模型
总进度 = sum(in_flight) + already_done。失败时 cancel 标志传播给其它
spawn task 早退。
2026-05-05 11:33:46 +08:00
weikeyi dab04fc345 fix(capsule): keep confirm icon inside pill on done/error states 2026-05-05 11:17:49 +08:00
baiqing 209dc9fece fix(local-asr): 4 并发分块下载 + 取消正确传播 + 残留进度可见可删
四个用户报的问题,逐个修:

(1) 慢——单连接顺序拉,HF CDN 单连接限速明显
  → 4 并发 chunk + sparse 文件 seek+write,每完成一块原子追加 .partial.idx;
    与 huggingface_hub / aria2 / hf_transfer 同款思路;下次启动只下未完成块
  → 走严格 206 协议(并发 seek 模式不能容忍服务端忽略 Range 返 200,否则会
    把整个文件写到 chunk 偏移导致灾难);非 206 直接 fail 让 retry 接手

(2) 取消按钮没反应
  → 原因:旧 download_one 的 chunk loop 在 chunk Err 后 "若有进度就 continue";
    用户取消时也属于"有进度"路径 → cancel 标志被吞
  → 新版每次 spawn task 都拿 Arc<AtomicBool>,每个 chunk + 每个流块都查
    cancel;run_download 主线在 download_one 返回后立刻再查 cancel emit
    Cancelled 事件
  → DownloadManager::cancel 加日志方便诊断

(3) 继续下载丢进度条
  → showProgress 原来只在有 progress 事件时为 true → 重启 app 后看不到 partial
  → 加 hasPartial 判断:downloadedBytes>0 && !isDownloaded 也算 showProgress
  → 按钮文案对应改 "继续下载" / "Resume"

(4) 残留无法删
  → 原本只有 isDownloaded 状态才出 删除按钮
  → 加 hasPartial 状态显示 [继续下载] [删除] 两个按钮
2026-05-05 11:13:41 +08:00
Chris233 c5aa3fef4a Merge pull request #259 from H-Chris233/main
feat(i18n): add full Traditional Chinese (zh-TW) UI support
2026-05-05 11:07:37 +08:00
kaku.ki 23d7f44a96 fix(windows): stabilize capsule visuals and audio meter 2026-05-05 11:51:57 +09:00
H-Chris233 e50bc17a15 Add complete Traditional Chinese UI locale support
Add zh-TW locale resources and wire it through language selection,
persistence, and system-locale detection. Also add static shape typing
for zh-TW to prevent future key drift from zh-CN.

Constraint: Keep changes minimal and localized to i18n + language pickers
Rejected: Partial zh-TW translation only for language section | fails full locale support goal
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep zh-TW key shape locked to zh-CN when adding new translation keys
Tested: npm run -s build (openless-all/app)
Not-tested: Manual runtime walkthrough on all platform shells
2026-05-05 10:51:46 +08:00
baiqing 4a3531083b feat(local-asr): HTTP Range 分块下载 + 加载测试按钮
下载彻底重写 (huggingface_hub / aria2 同款思路):
- 之前是"整文件 GET,失败重试整文件"——HF CDN 对长连接不友好,到某个固定时间
  点会强制断流,导致看到"卡 5% 就死"。用户对比别家工具发现这点,确实如此。
- 现在 download_one 内按 32MB chunk 跑 Range,每块 4 次指数退避 retry,每次都
  从 .partial 真实长度续传;CDN 中途断流只丢一个 chunk
- 防御服务端忽略 Range 返回 200 + 全文件:检测到就 truncate .partial 重头来,
  避免 append 污染
- client 加 User-Agent (HF 把 no-UA 流量算异常 → 限速)
- pool_idle_timeout 缩短,避免复用已被 CDN 关掉的连接

加载测试按钮(用户:「做好加载模型和测试按钮」):
- 内嵌 antirez 自带的 samples/test_speech.wav(120KB,"Hello. This is a test
  of the Voxtrail speech-to-text system.")
- 严格按 RIFF chunk 链解析(fmt 后面带 LIST/INFO,不能 +44 硬偏移)
- 新 IPC local_asr_test_model:spawn_blocking 跑 qwen_load + transcribe_audio
  分别计时;返回 backend / 原文 / 识别 / load_ms / transcribe_ms
- LocalAsr 页每个已下载模型行加「加载并测试」按钮 + 结果卡片

Backend 标识改为 "Apple Accelerate (AMX/NEON, CPU)"——antirez 引擎不走 Metal
GPU(作者明确选择跳过 MPS),Apple Silicon 上靠 AMX 矩阵协处理器 + NEON 加速。
不撒谎说支持 Metal。
2026-05-05 10:27:48 +08:00
baiqing 25d9e9f3da fix(local-asr): 下载 retry 升级 4 次指数退避 + 已下载列表/删除/性能警告
下载失败修:
- HF 大文件被 CDN 中途断流(end of file before message length reached);
  原 1 次 retry 不够 + 间隔太短两次都失败
- 升到 4 次:1s → 4s → 16s 指数退避;每次都从 .partial 当前长度断点续传
- reqwest client 加 connect_timeout(30s) + pool_idle_timeout(20s) 避免
  连接被复用到已被 CDN 关掉的 socket
- 不设整体 timeout(1GB 文件没意义),只防卡死

UI 补「已下载管理 + 性能预期警告」(用户两条新需求):
- Settings 的 LocalAsrProviderHint 现在列出所有已下载模型 + 每行删除按钮
  (原来只显示当前激活模型 ready/not 状态,没看到其它模型也没法删)
- Settings 与 LocalAsr 页顶部都加 ⚠️ 性能警告卡片:明确"比云端慢/中文准确率
  通常不如火山+turbo/适用于离线/隐私场景",避免用户误判
- i18n 新增 zh+en:localAsrPerformanceWarning / localAsrDownloadedTitle /
  localAsrDelete + LocalAsr.performanceWarning
2026-05-05 10:03:26 +08:00
baiqing 5a58f74c4a fix(local-asr): HF 下载 TLS unexpected_eof + Settings 引导改下载入口
下载层修两件事:
1. 改用 native-tls (macOS = SecureTransport) 而非 rustls:
   HF LFS CDN 经常不发 TLS close_notify 直接关 TCP,rustls 0.22+ 把这当
   致命 unexpected_eof,实测多个 .safetensors 失败。SecureTransport 容错。
   Volcengine WebSocket 不动,继续走 rustls。
2. 单文件失败自动 retry 1 次(带 1s 退避,且尊重 cancel)。
   HF 大文件偶发瞬断很常见,不让用户手点重试。

Settings 引导(按用户反馈):
- ASR 选 'local-qwen3' 时**不**渲染 API key / Base URL / Model 三个填空
- 改为 LocalAsrProviderHint 卡片:
  * 文案"本地 Qwen3-ASR 在本机运行,无需 API Key"
  * 已下载/未下载 Pill 显示当前激活模型状态
  * 一键按钮 → 派发 NAVIGATE_LOCAL_ASR_EVENT
- FloatingShell 监听同事件,关 Settings modal + 切到 'localAsr' tab
- i18n 同步 zh + en
2026-05-05 09:56:54 +08:00
baiqing 839ad76c5b fix(local-asr): 用 tauri::async_runtime 取代 tokio::spawn — 修点击下载即崩
崩溃栈:local_asr_download_model → DownloadManager::start → tokio::spawn
→ panic("there is no reactor running")。

根因:Tauri 同步 command 在 main thread 上下文跑,没进 tokio runtime;
tokio::spawn 直接 panic。换 tauri::async_runtime::spawn —— 它走 Tauri
持有的 runtime handle,不依赖调用方上下文。

同步把 local_provider.rs 的 spawn_blocking 也换成 tauri::async_runtime
版本(虽然现在在 async 路径上调没事,保持一致防回归)。

附带把 qwen_ffi.rs 的 `unsafe extern "C"` block 改回经典 extern block,
匹配 CLAUDE.md 声明的 rust-version = "1.77"(前者要 1.82+)。
2026-05-05 09:49:33 +08:00
baiqing dd43a7801d feat(local-asr): 前端「模型设置」页 + Settings 预设 + 反硬编码重构 — P5
后端反硬编码(按用户反馈):
- models.rs 删 files()/approx_bytes(),改用哨兵文件 .openless-asr-ready
  判 is_downloaded、walk_dir 求和算 downloaded_bytes
- download.rs 加 fetch_remote_info(repo, mirror):实时 GET HF
  /api/models/<repo>/tree/main 拿真实文件清单 + 尺寸;过滤 .md/.png/.git*
  等非权重文件(白名单:json/safetensors/txt/bin/model/tiktoken)
- 新 IPC: local_asr_fetch_remote_info(modelId, mirror)
  → RemoteInfo { files: [{path,size}], totalBytes }

前端:
- LocalAsr.tsx 新页:状态卡 / 镜像源切换 / 模型列表 (下载/取消/删除/设为默认)
  + 进度条订阅 local-asr-download-progress 事件实时刷新
  + 真实尺寸通过 fetchLocalAsrRemoteInfo 拉取,**不硬编码**
- localAsr.ts: 全套 IPC 包装 + dev mock 分支
- FloatingShell.tsx + useAppState.ts: 加 'localAsr' tab,icon=archive
- Settings.tsx ASR_PRESETS 加 { id: 'local-qwen3', nameKey: 'asrLocalQwen3' }
- i18n zh+en 同步:nav.localAsr / asrLocalQwen3 / 整块 localAsr.* 文案
- types.ts UserPreferences 加 localAsrActiveModel + localAsrMirror
2026-05-05 09:39:26 +08:00
baiqing 4656ecc7c7 feat(local-asr): coordinator 接 ActiveAsr::Local 第三分支 — P4
- local_provider.rs (LocalQwenAsr):实现 AudioConsumer 缓 i16 PCM →
  stop 时转 f32 → 跑 qwen_transcribe_stream,token 经
  `local-asr-token` 事件实时推到前端胶囊;spawn_blocking 防止占住 tokio runtime
- coordinator.rs:
  * ActiveAsr::Local(Arc<LocalQwenAsr>) cfg(macos)
  * start_dictation_for_session 优先走 local 分支再回落 whisper/volc
  * end_session 加 Local arm,失败静默回退(CLAUDE.md 不变量)
  * cancel_active_asr / ensure_asr_credentials 都补 Local 分支
  * build_local_qwen3 helper:从 prefs 拿 model id → models::model_dir → 加载
  * ensure_local_qwen3_model_ready:模型未下载时友好提示用户去模型设置页
- 非 macOS 选择 local-qwen3 时 ensure_asr_credentials 直接返回错误
2026-05-05 09:29:12 +08:00
baiqing 51b638a52e feat(local-asr): IPC commands + 偏好持久化 — P3
- types.rs UserPreferences 加 local_asr_active_model + local_asr_mirror,
  serde 默认值保证向后兼容(老 prefs.json 不需手工迁移)
- commands.rs 加 7 个 local_asr_* IPC:
  get_settings / set_active_model / set_mirror /
  list_models / download_model / cancel_download / delete_model
- lib.rs .manage(Arc<DownloadManager>) + invoke_handler 注册全部新命令
- 引擎可用性通过 cfg!(target_os = "macos") 暴露给前端,Win UI 据此灰按钮
2026-05-05 09:26:46 +08:00
baiqing 7b38e26473 feat(local-asr): 加模型注册表 + HF/HF-Mirror 下载管理 — P2
- models.rs: ModelId (0.6B / 1.7B) + 文件清单复刻 antirez download_model.sh
  + is_downloaded / downloaded_bytes / list_status / delete_model
- download.rs: DownloadManager (per-model AtomicBool 取消) + reqwest stream
  写 .partial 然后原子 rename;支持 Range 续传;Tauri 事件
  `local-asr-download-progress` 上报五种 phase
- Mirror enum:Huggingface(默认)/ HfMirror(hf-mirror.com 国内镜像)
- persistence.rs 加 pub fn local_models_root() —— 路径
  ~/Library/Application Support/OpenLess/models/qwen3-asr/<id>/
2026-05-05 09:24:27 +08:00
baiqing bd3bac3ca6 feat(local-asr): 接入 antirez/qwen-asr 编译链路 — 阶段 1 脚手架
- vendor antirez/qwen-asr 作为 git submodule (commit b00b789)
- build.rs 用 cc 编 10 个 C 源 + 链 Accelerate (仅 macOS)
- src/asr/local/{mod,qwen_ffi,qwen_engine}.rs 包装最小 FFI 面:
  load / transcribe_audio / transcribe_stream + token 回调蹦床
- libqwen_asr.a (~434KB) 已链通,nm 验证 qwen_* 符号齐全
- Windows 端不编入,路径见 issue #256

下一阶段:模型下载管理 + IPC commands + 新页 LocalAsr.tsx + coordinator 路由
2026-05-05 09:17:32 +08:00
baiqing 149a6b8eca chore(release): 1.2.19 — Windows updater 修 NSIS+MSI 拆两轮跑 2026-05-04 23:12:55 +08:00
baiqing 429780b5e4 chore(release): 1.2.18 — Windows bundle 顺序锁 nsis,msi 让 updater 资产先落盘
历史链:
  1.2.13 ✓  无 IME wxs,MSI+NSIS 全过
  1.2.15 ✗  加 IME wxs;CI pwsh 把 JSON 引号吃掉,tauri 连 candle 都没跑
  1.2.16 ✗  shell 切 bash 后到 light 阶段,但 ICE80 触发,Repair 没 -sice
  1.2.17 ✗  Repair 加了 -sice:ICE80 修了 MSI;但 Tauri 字母序 msi→nsis 让
            MSI 先挂掉,NSIS 永远没机会跑,updater .exe(.sig) 缺失
  1.2.18    显式 --bundles nsis,msi:NSIS 先写盘产 updater 资产,MSI 再失败
            由 Repair 兜底 light.exe 重链

唯一改动:Build (Windows) 步骤两个分支都加 --bundles nsis,msi。
预留:NSIS 不读 wix.fragmentPaths,所以 .exe 暂不带 IME DLL;MSI 仍带,
后续单独 issue 跟进 NSIS IME 集成,不挂当前发版。
2026-05-04 22:56:33 +08:00
baiqing a5f1a8f16d chore(release): 1.2.17 — Windows MSI 修 light.exe ICE80 报错
x86/x64 IME DLL 都装进 INSTALLDIR\windows-ime\,32-bit 组件落在 64-bit
Directory 是 ICE80 的硬性报错。本场景路径绝对指向、不依赖 SysWOW64
重定向,按 Microsoft 文档是合法用法,给手动 light.exe 重链加 -sice:ICE80
即可放行。改动同步覆盖 CI workflow 和本地打包脚本。
2026-05-04 22:21:50 +08:00
baiqing 49210363d6 chore(release): 1.2.16 — Windows MSI 修 PowerShell 引号吞双引号 bug
1.2.15 第 4 轮 Windows job 在 wix 阶段之前就 exit 2,根因是上一版我把
Build (Windows) 整步切成 shell:pwsh 后,& npm run tauri --- build --config
'{"bundle":{"createUpdaterArtifacts":true}}' 这条命令 PowerShell 调外部
进程时把 JSON 内的双引号剥光,tauri 收到 {bundle:{createUpdaterArtifacts:true}}
直接拒绝。log 第 586 行能看到剥过引号的 commandline。

修法:
- Build (Windows) 切回 shell:bash(单引号能保留内部双引号)。set +e 抓
  exit code 写到 GITHUB_ENV,让本步不直接失败,把判断权交给下一步。
- 新增独立 step "Repair Windows MSI if Tauri failed at WiX link"(pwsh),
  仅在 tauri 非零退出且 MSI 不存在时手动从 appRoot 跑 light.exe 重链。
  本地 windows-package-msvc.ps1::Repair-TauriMsiBundle 同款手术。
2026-05-04 21:53:23 +08:00
cooper a8741dd568 fix(recorder): watchdog 从播放开始时计时,避免慢启动误报
- 移除 StreamState::stream_start_time 字段
- 在 watchdog 线程内部记录启动时间
- 确保首次回调截止时间从 stream.play() 成功后开始计时
- 修复慢速后端可能导致的过早超时问题
2026-05-04 21:46:07 +08:00
Chris233 88452cb2ca Merge pull request #251 from H-Chris233/feat/asr-e2e-validation
feat(settings): add end-to-end ASR provider validation
2026-05-04 21:45:45 +08:00
cooperandClaude Sonnet 4.6 87838617f8 fix(recorder): 添加首次回调截止时间检测
根据 code review 反馈,修复 watchdog 的行为缺口:

## 问题
修复 Finding 3 后,虽然避免了误报慢启动设备,但也失去了对
"第一个回调永远不到达"这种失效模式的检测能力。

如果 stream.play() 成功但 CPAL 从未发送任何回调,watchdog
的 `if let Some(last_time)` 分支永远不会执行,session 仍然
可能无限期挂起。

## 修复方案
添加"首次回调截止时间"机制,watchdog 现在检查两种情况:

1. **首次回调未到达**(last_callback_time = None)
   - 检查是否超过 FIRST_CALLBACK_DEADLINE_SECS (5秒)
   - 如果超过,报错:"录音启动后 X 秒内未收到回调"

2. **回调中途停止**(last_callback_time = Some)
   - 检查是否超过 CALLBACK_TIMEOUT_SECS (3秒)
   - 如果超过,报错:"录音回调静默停止 X 秒"

## 技术细节
- 在 StreamState 中添加 stream_start_time 字段
- 在 watchdog 中使用 match 语句区分两种情况
- 5 秒首次回调截止时间足够慢启动设备,但能捕获永不回调的失败

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 21:39:52 +08:00
H-Chris233 cb2eb8e9ce Preserve query parameters when building ASR transcription validation URL
ASR validation URL normalization now edits the parsed URL path instead of string-appending to the raw endpoint, so endpoints with query parameters (e.g. api-version) remain valid after adding /audio/transcriptions.

Constraint: Keep endpoint normalization minimal and avoid changing query/fragment semantics

Rejected: Drop query parameters during validation | would break Azure-style OpenAI-compatible deployments

Confidence: high

Scope-risk: narrow

Tested: cargo test -q
2026-05-04 21:35:14 +08:00
H-Chris233 df2d51e014 Normalize ASR transcription endpoint and enforce streaming response cap
Addresses review feedback: ASR validation now recognizes OpenAI-style /chat/completions endpoints when building the /audio/transcriptions URL, and response-size protection is enforced while streaming the body instead of after buffering it all.

Constraint: Keep ASR validation behavior provider-agnostic and minimal in scope

Rejected: Heuristic URL rewrites beyond known suffixes | risk of breaking custom reverse proxies

Confidence: high

Scope-risk: narrow

Directive: If changing reqwest features, keep Cargo.lock in sync and re-run tests

Tested: cargo test -q
2026-05-04 21:16:46 +08:00
H-Chris233 fd60f815c0 Accept /audio base URLs when validating ASR transcription endpoint
Some OpenAI-compatible deployments expose ASR under a base URL that already ends with /audio. ASR validation now treats that as a valid prefix and appends only /transcriptions to avoid generating /audio/audio/transcriptions.

Constraint: Keep ASR validation URL normalization conservative and backwards-compatible

Rejected: Full URL path rewrite heuristics | risk of breaking custom reverse-proxy layouts

Confidence: high

Scope-risk: narrow

Tested: cargo test -q
2026-05-04 20:51:40 +08:00
baiqing dcfeb405bf chore(release): 1.2.15 — Windows MSI 修复 IME bundle 集成
1.2.14 三轮 release pipeline Windows job 全挂在 WiX 集成上:
  1. 没构建 IME DLL → light.exe 找不到文件
  2. 加了 build 步骤后用相对路径 src-tauri\target\... → Tauri wix bundler
     cwd 和本地脚本不同,相对路径 resolve 错位
  3. 改 $(env.OPENLESS_IME_DLL_X64) 想用绝对路径 → candle 处理 env var
     substitution 时 abort(具体原因被 Tauri 吞掉)

1.2.15 切换到本地 windows-package-msvc.ps1 已经验证可行的 Repair 思路:
  - wxs 维持字面相对路径 src-tauri\target\... (让 candle 正常吃过)
  - workflow 接住 tauri build 失败:从 appRoot (openless-all/app/) 手动跑
    light.exe 重链 main.wixobj + openless-ime.wixobj。这是 light 唯一能
    resolve 该相对路径的 cwd。
2026-05-04 20:39:22 +08:00
H-Chris233 73dbd4cef7 Harden ASR validation path against endpoint and response regressions
Follow-up fixes from dual review keep the new end-to-end ASR check while removing compatibility and robustness regressions: normalize transcription endpoint URL, enforce HTTPS except localhost, cap response size, and switch to stable backend error codes mapped in i18n.

Constraint: Preserve issue #242 full-chain validation behavior with minimal surface change

Rejected: Revert to model-list-only validation | no longer verifies real transcription route

Confidence: high

Scope-risk: narrow

Directive: Keep providerHttpStatus:* and stable error-code contract synchronized with Settings error mapping

Tested: npm run build; cargo check -q

Not-tested: Manual validation against all third-party ASR endpoints
2026-05-04 20:27:08 +08:00
H-Chris233 2ae3bdf68a Use real transcription call for ASR connection validation
ASR validation now runs one end-to-end /audio/transcriptions request with the currently selected model and a tiny bundled silence WAV payload. This verifies endpoint, auth, model routing, and response schema in one pass instead of model-list reachability only.

Constraint: Keep UI/API contract unchanged and avoid touching recording pipeline

Rejected: Reuse live microphone capture in settings validation | invasive and non-deterministic for quick checks

Confidence: high

Scope-risk: narrow

Directive: Preserve providerHttpStatus:* error marker so frontend can keep status-specific guidance

Tested: npm run build; cargo check -q

Not-tested: Live validation against every third-party ASR provider implementation
2026-05-04 20:27:08 +08:00
H-Chris233 2438c901eb Ensure ASR connection check validates the configured model
Issue #242 asks ASR to reach parity with LLM availability checks. Validation now requires a configured ASR model and verifies it exists in the provider model list, instead of only checking endpoint/auth reachability.

Constraint: Keep existing settings flow and IPC shape with minimal UI change

Rejected: Add a new ASR test-audio transcription request | adds latency and provider-specific request complexity

Confidence: high

Scope-risk: narrow

Directive: Keep validate and fetch-models semantics aligned; validate should fail when configured model cannot actually be selected

Tested: npm run build; cargo check -q

Not-tested: Manual Settings UI interaction against live third-party ASR endpoints
2026-05-04 20:27:08 +08:00
baiqing ac2edbe961 ci(release): wxs 用 env 绝对路径定位 IME DLL
之前 light.exe 失败的根因:openless-ime.wxs 的 Source 写的相对路径
src-tauri\target\windows-ime-msvc\... ,要从 openless-all/app/ 才能
resolve;但 Tauri 内置 wix bundler 的 candle/light cwd 是 src-tauri/,
路径错位,文件找不到 → light 失败。

本地 windows-package-msvc.ps1 通过 Repair-TauriMsiBundle 从 appRoot 手动
跑 light 兜底,CI 没有这个兜底所以挂掉。

修法:wxs Source 改用 $(env.OPENLESS_IME_DLL_X64) / _X86 绝对路径变量;
工作流的 IME build 步骤把绝对路径写到 $GITHUB_ENV。这样无论 candle/light
cwd 在哪,都能拿到正确的 DLL 路径。

本地脚本本就 set 这两个 env var(windows-package-msvc.ps1:213),改后
本地 + CI 共用同一条 absolute-path 路径,Repair fallback 不再必需。
2026-05-04 20:04:18 +08:00
cooperandClaude Sonnet 4.6 2050524cbf fix: 根据 code review 修复三个问题
根据 code review 反馈,修复以下问题:

## 1. High: 超时后未清理 ASR session
- 在全局超时分支中添加 asr.cancel() 调用
- 避免 websocket/read tasks 继续运行造成资源泄漏
- 影响文件:coordinator.rs (dictation 和 QA 路径)

## 2. Medium: QA 路径缺少超时保护
- 为 QA 路径的 await_final_result() 添加相同的超时保护
- 保持 dictation 和 QA 路径的一致性
- 影响文件:coordinator.rs:2288-2298

## 3. Medium: Watchdog 误报慢启动设备
- 将 last_callback_time 初始化从 Some(Instant::now()) 改为 None
- 只有在第一次回调后才开始计时
- 避免慢启动设备被误判为"静默停止"
- 影响文件:recorder.rs:333

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 19:43:34 +08:00
cooperandClaude Sonnet 4.6 e0d459bbd0 fix(recorder): 添加 liveness watchdog 和全局超时保护以修复 #238
根据 Issue #238 的分析,录音器异常停止后会导致胶囊卡在 processing 状态。
本次修复实施了两层防护机制:

## 修复 1: Recorder Liveness Watchdog
- 在 StreamState 中添加 last_callback_time 字段跟踪最后一次回调时间
- 在 process_callback 中更新时间戳
- 在 run_audio_thread 中启动 watchdog 线程,每秒检查一次
- 如果 3 秒没有回调,主动触发 runtime_error 通知 coordinator
- 解决了"录音回调静默停止"的根本问题

## 修复 2: Coordinator 全局超时保护
- 添加 COORDINATOR_GLOBAL_TIMEOUT_SECS = 15 秒常量
- 在 end_session() 中为 await_final_result() 和 transcribe() 添加超时保护
- 超时时强制恢复到 Idle 状态,显示错误胶囊
- 作为最后的防线,即使 ASR 超时机制失效也能恢复

## 技术细节
- Watchdog 使用独立线程,避免阻塞主线程
- 超时时间设置为 15 秒(比 ASR 的 12 秒 FINAL_RESULT_TIMEOUT 稍长)
- 保持与现有错误处理机制的一致性

Fixes #238

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 19:06:08 +08:00
baiqing 72ebe76be0 chore(release): 1.2.14
包含自 1.2.13 以来的修复与新功能:
- feat(autostart): 跨端开机自启 (#194)
- feat(asr): SiliconFlow / GLM-ASR / Groq / OpenAI Whisper preset (#212)
- fix(qa): Windows / Linux 划词追问浮窗 Esc + 拖拽 + 文案 (#205 / #206)
- fix(settings): preset 切换 race + per-provider 凭据隔离 (#219 / #220)
- fix(overview): 概览整屏适配 + 嵌套 scroller 细滚动条 (#243 / #248)
- 一系列 Windows IME (TSF) 模块 (#233 / #240 等)
2026-05-04 18:58:03 +08:00
Chris233 5d15f1d2f2 Merge pull request #241 from H-Chris233/main
security(qa): sanitize assistant markdown rendering in QA panel
2026-05-04 18:52:05 +08:00
baiqing fd0b2fdf02 fix(overview): Codex P1 + P2 — 保留 fallback scroll + 嵌套 scroller 同步细化
Codex P1:之前 displayTab==='overview' 时 wrapper 用 overflow:hidden,
窗口被压缩或文案变长(i18n / 字体放大)后底部 Recent Card 整片不可达,
没有 scroll 兜底路径。

修法:所有 tab 一律 overflow:auto。Overview.tsx 内部已经用 flex 把底部
行 grow 到撑满 → 正常尺寸下内容刚好占满,浏览器不显示 scrollbar;真挤
不下了 fallback 出细 scrollbar。"整屏不滚"的视觉效果保留,但有 scroll
保险。

Codex P2:::-webkit-scrollbar 不级联到子节点,所以 .ol-thinscroll 加
在 wrapper 上对 History / SettingsModal 等内层 overflow:auto 容器无效。
把 className 显式补到这些嵌套 scroller:
- History 左侧列表 + 右侧详情 Card(Card 组件加 className prop 透传)
- Settings.tsx 内嵌模式右栏
- SettingsModal personalize/about 块
QaPanel 是独立窗口,不在主壳 thinscroll 范围内,保留默认。
2026-05-04 18:26:34 +08:00
baiqing 8b435b4844 fix(overview): 底部行 flex:1 + 仅最近识别区域滚动
#243 第一版给外层 wrapper 加了 overflow:hidden 让概览不滚,但底部「近 7 天 +
最近识别」grid 仍按自然高度展开 → 内容超出 wrapper 时被硬裁,看不到底部
圆角;最近识别第 5 行只露半截。

修法(Overview.tsx 底部 grid):
- grid 自身 flex:1, minHeight:0,撑满父 flex:column 的剩余高度
- 两张 Card 都加 display:flex/column + minHeight:0;最近识别 Card 还加
  overflow:hidden,让内层行严格剪裁到 Card 的圆角
- 最近识别的 list <div> 加 .ol-thinscroll + flex:1 + overflow:auto,自己
  独立滚动;其他区域按需求保持不滚

视觉效果:底部圆角现在完整显示并与上方 metrics / providers 对齐;只有最
近识别区域出现细滚动条,整体仍是 dashboard「一眼看完」节奏。
2026-05-04 18:19:22 +08:00
H-Chris233 806fa26693 Keep QA code snippets readable while preserving markdown XSS guard
Review feedback found that pre-escaping the entire markdown payload caused double-encoding in inline/fenced code, making HTML/JSON/XML examples unreadable. Switch to raw markdown parsing with renderer-level HTML token escaping so code formatting stays readable while raw HTML remains inert.

Constraint: Maintain issue #221 security boundary without broad sanitizer rewrite

Rejected: Keep full-string pre-escape | breaks code snippet readability

Confidence: high

Scope-risk: narrow

Directive: Escape only raw HTML tokens, not the whole markdown source

Tested: npm run build (openless-all/app)

Not-tested: Manual QA panel rendering across all markdown edge cases
2026-05-04 18:06:51 +08:00
baiqing b41c904104 fix(ui): 概览页整屏不滚 + 其他页 scrollbar 变细 (#243)
FloatingShell.tsx 的 page wrapper:
- displayTab === 'overview' 时 overflow:hidden,让 Overview 真"一眼看完"
- 其他 tab 维持 overflow:auto,加上 .ol-thinscroll 让 scrollbar 细而可见

tokens.css 新增 .ol-thinscroll —— 与既有 .ol-noscrollbar 同位置;区别是
"细而仍可见",保留可滚动的视觉 affordance(不像 noscrollbar 完全隐藏)。

Closes #243
2026-05-04 18:02:25 +08:00