Skip to content

docs: add code quality cleanup roadmap (fixes #699)#778

Open
zsxh1990 wants to merge 10 commits into
Open-Less:mainfrom
zsxh1990:docs/code-quality-cleanup-roadmap
Open

docs: add code quality cleanup roadmap (fixes #699)#778
zsxh1990 wants to merge 10 commits into
Open-Less:mainfrom
zsxh1990:docs/code-quality-cleanup-roadmap

Conversation

@zsxh1990

@zsxh1990 zsxh1990 commented Jul 11, 2026

Copy link
Copy Markdown

User description

Summary

Document the code quality cleanup items from the 1-app audit (2026-06-17).

Changes

Items Documented

  1. [windows] 启动首屏被麦克风权限探测阻塞 #27: Remove blanket allow — Lowest risk, immediate value
  2. [infra/devex] Windows 真机回归缺少真实凭据和物理热键门禁 #32: Rename misleading stub — Quick win, improves clarity
  3. chore(release): rustfmt 后端 + 更新 CLAUDE.md #24: Extract shared builder — Most complex, do last

Next Steps

  • Install Rust toolchain for compilation verification
  • Execute cleanup in documented order
  • Each step includes verification checklist

Closes #699

/claim #699


PR Type

Enhancement, Bug fix, Tests, Documentation


Description

  • 主窗 UI 大改:聊天面板统一尺寸、macOS 非激活 NSPanel

  • Apple Speech ASR 改进:locale 支持、on-device、取消等待、流式简繁

  • 活动热力图与 Less Computer 缓冲同步

  • 按键事件配对修复(macOS 侧修饰键)及 Linux 拒绝

  • 持久化:样式偏好保留、活动计数写入


Diagram Walkthrough

flowchart LR
  lib["lib.rs 窗口管理重构"] --> panel["聊天面板统一尺寸 & 非激活 NSPanel"]
  lib --> hideAnim["退场动画 (epoch)"]
  lib --> capsule["胶囊尺寸统一 & 光效舞台"]
  apple["AppleSpeechAsr 改进"] --> locale["locale 支持"]
  apple --> onDevice["On-Device 识别"]
  apple --> cancelFlag["取消标志 + 轮询等待"]
  dict["dictation.rs"] --> buff["事件缓冲 (less_computer_sync)"]
  dict --> warming["乐观胶囊显示 (warming)"]
  dict --> streamingT2S["流式 t2s 转换"]
  volc["Volcengine ASR"] --> retry["连接重试 & 限流分类"]
  volc --> speaker["声纹过滤 (主说话人)"]
  combo["side_aware_combo"] --> pairFix["事件配对自愈"]
  combo --> linuxReject["Linux 拒绝"]
  settings["settings.rs"] --> preserveStyle["持久化时保留样式偏好"]
  coord["coordinator.rs"] --> activity["ActivityStore 热力图"]
  pref["preferences.rs"] --> preserveStyle
Loading

File Walkthrough

Relevant files
Enhancement
17 files
lib.rs
聊天面板窗口重构(尺寸、NSPanel、退场动画)                                                               
+156/-146
apple_speech_provider.rs
Apple Speech ASR 改进(locale、on-device、取消等待)                             
+311/-32
dictation.rs
Less Computer 缓冲、乐观胶囊、流式简繁转换                                                         
+173/-26
volcengine.rs
连接重试、限流分类、声纹过滤                                                                                     
+215/-33
coordinator.rs
添加 ActivityStore 与文本提交命令                                                                 
+49/-17 
preferences.rs
添加 set_preserving_current_style_preferences                           
+53/-1   
activity.rs
每日活动计数存储                                                                                                 
+69/-0   
SiriGL.tsx
语音光效舞台组件                                                                                                 
+471/-0 
Heatmap.tsx
活动热力图组件                                                                                                   
+222/-0 
avatars.tsx
聊天头像组件                                                                                                     
+148/-0 
input-group.tsx
聊天输入组样式更新                                                                                               
+155/-0 
bubble.tsx
聊天气泡样式更新                                                                                                 
+128/-0 
orbFeed.ts
语音光效动画逻辑                                                                                                 
+129/-0 
Tooltip.tsx
工具提示组件                                                                                                     
+135/-0 
message-scroller.tsx
消息滚动组件                                                                                                     
+128/-0 
button.tsx
聊天按钮组件                                                                                                     
+62/-0   
card.tsx
聊天卡片组件                                                                                                     
+100/-0 
Bug fix
2 files
side_aware_combo.rs
按键事件配对修复、Linux 拒绝                                                                               
+248/-7 
settings.rs
持久化时保留样式偏好                                                                                             
+141/-10
Documentation
1 files
CODE_QUALITY_CLEANUP.md
代码质量清理路线图文档                                                                                           
+98/-0   
Additional files
80 files
README.md +16/-1   
README.zh.md +16/-1   
package.json +20/-4   
Cargo.toml +4/-3     
bailian.rs +27/-0   
mod.rs +1/-1     
stream.rs +23/-0   
history.rs +12/-0   
mod.rs +2/-1     
providers.rs +15/-1   
qa.rs +73/-9   
asr_wiring.rs +11/-3   
capsule_focus.rs +33/-3   
hotkey_loops.rs +0/-1     
qa.rs +0/-5     
qa_session.rs +4/-0     
insertion.rs +11/-3   
credentials.rs +54/-5   
mod.rs +2/-0     
types.rs +33/-0   
unicode_keystroke.rs +90/-40 
windows_ime_profile.rs +83/-8   
tauri.conf.json +4/-4     
App.tsx +17/-11 
AutoUpdate.tsx +1/-1     
Capsule.tsx +223/-326
FloatingShell.tsx +197/-132
Kbd.tsx +45/-0   
MarketplaceModal.tsx +0/-125 
SettingsModal.tsx +9/-8     
ShortcutRecorder.tsx +4/-4     
ThinkingDots.tsx +75/-0   
chat.css +341/-0 
index.ts +64/-0   
utils.ts +7/-0     
lifecycle.ts +51/-0   
markdown.tsx +40/-0   
empty.tsx +105/-0 
input.tsx +20/-0   
marker.tsx +71/-0   
message.tsx +92/-0   
spinner.tsx +18/-0   
textarea.tsx +18/-0   
Modal.tsx +9/-2     
en.ts +33/-4   
ja.ts +33/-4   
ko.ts +33/-4   
zh-CN.ts +35/-6   
zh-TW.ts +35/-6   
audioCue.test.ts +36/-0   
audioCue.ts +82/-25 
capsuleLayout.test.ts +25/-23 
capsuleLayout.ts +18/-22 
hotkey.ts +9/-2     
chat-panel.ts +13/-0   
history.ts +7/-2     
index.ts +7/-5     
less-computer.ts +11/-4   
mock-data.ts +20/-0   
qa.ts +0/-4     
stylePrefs.test.ts +1/-0     
types.ts +23/-2   
LessComputerGlow.tsx +169/-96
LessComputerPanel.tsx +559/-469
Marketplace.tsx +28/-5   
Overview.tsx +66/-2   
QaPanel.tsx +368/-831
Style.tsx +9/-24   
LocalModelSection.tsx +1/-1     
PermissionsSection.tsx +8/-2     
ProvidersSection.tsx +3/-2     
RemoteInputSection.tsx +1/-1     
ThemeSection.tsx +12/-1   
tabs.tsx +34/-14 
useAppState.ts +1/-0     
global.css +79/-0   
tokens.css +113/-119
vite.config.ts +4/-1     
ipc_client.cpp +109/-40
ipc_client.h +12/-4   

appergb and others added 10 commits July 4, 2026 23:41
* fix(insertion): macOS 让「恢复剪贴板」开关真正生效,修复被回退的 Open-Less#525

Open-Less#525 的 macOS 剪贴板恢复(commit 4b4d219)被 Android 移植 commit 1ea467a
连同 cfg 门控一起回退:整套恢复机制被重新 #[cfg] 排除出 macOS,macOS insert()
退回「只写剪贴板 + Cmd+V」,于是设置里的「恢复剪贴板」开关在 macOS 上完全无效
——无论开关开还是关,剪贴板都被留成转写文字。

- 把共享的剪贴板恢复机制(ClipboardRestorePlan / schedule_clipboard_restore /
  restore_clipboard_after_delay / should_restore_clipboard 等)的 cfg 从
  not(any(macos,android,ios)) 放宽为 not(any(android,ios)),重新纳入 macOS
  (移动端仍排除;Windows/Linux 不变)。
- macOS insert() 改为:保存原剪贴板 → 写转写文字 → Cmd+V → 仅当「恢复剪贴板」
  开关开启且粘贴成功时按 CLIPBOARD_RESTORE_DELAY 延迟恢复;粘贴失败则保留转写
  文字供手动粘贴、不恢复。开 = 恢复,关 = 保持现状,开关说了算。
- 删除随之失效的 macos_insert_status_after_paste;恢复相关单测在 macOS 也运行。

Windows/Linux 专用项(insert_with_clipboard_restore / paste_keys /
simulate_paste(shortcut) / 其 insertion_success_status 变体)的 macOS 排除保持不变。

* fix(less-computer): 护栏 sudo/dd/chmod/chown 等设为不可批准,修批准注入畸形规则 (Open-Less#679)

旧审批流对所有命中 pattern 一律 format!("Bash({p}:*)") 生成 allow 规则:
- 对带空格/参数的子串("sudo " → "Bash(sudo :*)","dd if=" → "Bash(dd if=:*)",
  "chmod -r 777 /"、"chown -r")生成的串既不等于 deny "Bash(sudo:*)" 等(故 deny
  没被移除、批准静默失效),又把畸形规则注入了 allowed_tools。

改为:新增 guard::deny_rule_for_pattern(pattern),把「可批准」命令(rm -rf / git
push --force / git reset --hard / git clean 等)映射到其在 default_deny_rules 里的
**精确** deny 规则;提权/毁盘/系统级命令(sudo / dd / mkfs / chmod / chown / shutdown
/ reboot / 管道执行 / fork 炸弹)返回 None = 不可批准,即使被批准也保持拦截
(fail-closed),不再注入畸形 allow。审批流用 filter_map 取精确规则,移除的 deny 与
加入的 allow 严格一致。

附单测:每个可批准 pattern 的映射规则确实存在于 default_deny_rules;危险系统命令一律
不可批准。

* fix(ui): unify macOS shell top inset with sidebar/background surface

The 28px top inset that clears the macOS overlay traffic lights sat on the
content wrapper with no background, so it exposed the lighter WindowChrome
glass (--ol-window-bg) as a full-width band above the sidebar and panel —
reading as an extra top row that the Windows build (no inset) doesn't have.

Paint that wrapper with the same --ol-app-shell-bg the sidebar/background
already use, so the inset blends into one continuous surface with the panel
card floating on top. One property; token carries its own light + dark
values; no layout, token, or other UI changes.

* fix(capsule): 胶囊跟随鼠标光标所在屏,修复多屏/多 Space 只在第一块屏显示

多屏 + 多桌面下胶囊只出现在第一块屏、其他屏闪一下就消失。根因:定位
(position_capsule_bottom_center) 用 AX caret 选屏,多数 App 取不到 caret 矩形就
跌回胶囊自己的 current_monitor(第一块屏);而去重缓存 capsule_layout_snapshot 也
用 current_monitor,与定位看的不是同一块屏 → 胶囊一落第一块屏即被判“无变化”跳过
重定位,被锁死。两条路都没跟踪鼠标。

- 新增 mouse_cursor_point():CoreGraphics CGEventGetLocation 取光标,坐标系与 AX
  一致、零权限、永远可用
- focused_input_target_monitor → capsule_target_monitor,选屏改“光标优先”(caret
  仅兜底),选屏循环抽成 monitor_for_anchor_point
- 定位与去重快照统一走 capsule_target_monitor,彻底消除不一致
- 保留 CAN_JOIN_ALL_SPACES(Spaces 契约强制且本就正确:帧落在光标屏→只那块屏显示)
- 扩展 macos-capsule-spaces 契约测试,grep 守护“光标优先 + 两路共用同一函数”

Windows 路径未动(本就跟随输入 App 屏且快照一致)。
验证:cargo check + lib 纯函数单测 + check:macos-capsule-spaces 全过。

* feat(less-computer): 边缘发光改用 EdgeGlow 式四层模糊堆叠 + iridescent 配色

把 Less Computer 全屏彩虹边缘发光从原来「两条都带模糊的软光带」改成
EdgeGlow(github.com/vector4wang/EdgeGlow,MIT)的实现方式,让彩虹「实线」
更清晰、发光更有层次。

- 4 层模糊堆叠:最内一条 0.5px 锐利彩虹实线(对应 EdgeGlow blur=0 那层,
  定义边缘),外面叠 3 层逐渐变宽、变虚的光晕(blur 3/8/15px),得到
  霓虹灯管式发光。原生用 CAShapeLayer + 高斯模糊,这里用 CSS「边框遮罩环
  + filter:blur」等价复刻。
- 配色移植 EdgeGlow 默认的 iridescent(Apple Intelligence 高饱和霓虹:
  紫→蓝→青→绿→金→橙→红→粉),按 12 段均匀铺满 360°。
- 流光整圈 8s/圈通过 --lcg-angle 旋转(只动渐变角度不动元素形状,GPU 友好),
  光晕层各带不同周期呼吸;实线用 normal 保色准,光晕用 screen 叠加增亮。

保留 issue Open-Less#470 的隐藏即卸载发光层(零 GPU)逻辑、pointer-events:none、
prefers-reduced-motion 降级与 --lcg-radius 圆角变量。纯视觉、仅 macOS、单文件。

* fix(vault): Windows 凭据读取瞬时失败重试,修概率性「火山引擎未配置」

Windows Credential Manager(CredReadW)在登录后/并发下读 manifest+各 chunk 条目
时可能瞬时失败。任一条目读失败会让整组凭据看起来为空 → load_keyring_credentials
返回 Err → load_credentials 回退到空默认值 → 概览页显示「火山引擎未配置」,但下
一次听写重读成功,所以是「概率性」且「实际可正常使用」。凭据 chunk 越多,单次
load 读取次数越多,命中瞬时失败的概率越高。

修法:仅在 Windows 上对非 NoEntry 的读错误做几次短退避重试(4 次 / 60·n ms)。
NoEntry 是确定的「未存储」,立即返回不付重试延迟。macOS/Linux 保持原单次行为
——它们的读错误是 ACL 拒绝,重试无益,且未缓存的错误路径下次调用本就会重读,
加 sleep 只会拖慢 macOS 首次 Keychain 授权流程。纯后端、cfg 隔离,零 PC-macOS 影响。

* fix(asr): 火山引擎连接增加超时与重试,修弱网下无法语音输入

connect_async 此前既无超时也无重试:弱网下 TLS/WebSocket 握手可能挂到 OS 级
TCP 超时(几十秒),期间用户卡在「Starting」;而协调器全局超时只覆盖
await_final_result,不覆盖 open_session,所以握手挂起完全不受约束。单次网络
抖动(连接被重置/瞬时 DNS 失败)也会直接让整次听写失败。

修法:每次握手包 5s 超时,最多 3 次尝试、250·n ms 退避。AuthRejected(凭据被拒)
短路不重试——重试不会变好只会拖慢报错。把请求构造抽成 build_connect_request
(connect_async 消费 request 且 http::Request 非 Clone,需每次重建)。

桌面与安卓共享此路径,改动对两端等效;最坏耗时从「无界(~75s)」降到有界(~15s)
并新增抖动可恢复,对 PC 端只增不减。

* feat(coding-agent): OpenCode CLI 适配器,语音 Agent 可选 OpenCode 后端 (issue Open-Less#579)

把 OpenCode 当作与 Claude Code 同类的 coding agent CLI 接入,照既有 coding_agent
适配模式(不另造架构),复用同一套 CodingAgentRequest / CodingAgentEvent /
CodingAgentError / 审批护栏链路。

后端:
- coding_agent/opencode.rs(新):detect_opencode(opencode --version)、
  build_opencode_args(opencode run --format json --model --dir
  [--dangerously-skip-permissions],prompt 作为 argv)、parse_opencode_json_line
  (NDJSON:text/tool_use/error;text 块累计,EOF 合成 Completed)、
  run_opencode_agent(取消/超时 kill,护栏经 OPENCODE_CONFIG_CONTENT 注入)。
- coding_agent/args.rs:CodingAgentProvider 枚举(claude-code-cli/opencode-cli,
  未知回落 Claude)+ from_pref/default_exe。
- coding_agent/guard.rs:build_opencode_guard_config —— 把高风险 bash 前缀翻译成
  OpenCode permission.bash deny glob + webfetch deny,审批放行的前缀显式 allow。
- coordinator/dictation_voice_agent: run_less_computer_once 按 coding_agent_provider
  分派 Claude / OpenCode 运行器;两路都 fail-closed(护栏生成失败即中止,不裸跑)。
  审批拦截探测/重跑放行链路 provider 无关,自动复用。
- commands: coding_agent_detect_opencode 命令(已注册 lib.rs)。

前端:
- CodingAgentSection: OpenCode 选项从「即将支持」改为可用 + 选中后探测安装状态并提示。
- ipc: codingAgentDetectOpencode + OpenCodeDetection。
- i18n: opencodeReady / opencodeMissing(5 语言,替换 providerOpenCodeSoon)。

验证:cargo check + cargo test(492 通过)+ npm run build 均通过。

Refs Open-Less#579Open-Less#579 还含「语音润色模型配置」:prefs 字段已存在但全链路未实现/未接 UI,
转写直接进 agent 未经语音润色模型整理 —— 故用 Refs 不用 Closes。)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(coding-agent): OpenCode 防参数注入 + 可配置 Agent 可执行文件路径 (Open-Less#638)

注入风险(@H-Chris233 评审):`opencode run` 的 prompt 作为最后位置参数,但之前缺
end-of-options 分隔。以 `-` / `--` 开头的 prompt(语音转写或被 prompt 注入诱导)会被
OpenCode 当作 flag 解析,可绕过护栏(混入 --dangerously-skip-permissions、--dir 改写
工作目录等)。修复:build_opencode_args 末尾追加 `--`,运行器把 prompt 接在其后;新增单测。

可配置路径(@pr-agent 指出之前 hardcode "opencode"):新增 prefs.coding_agent_exe,
语音 Agent 路径据此取 exe(留空回退默认 claude / opencode),检测命令亦用配置路径;
设置「高级 → Less Computer」新增「可执行文件路径」输入;补 5 语言 i18n。

* fix(updater): 安装失败可见化 + 手动下载兜底 + 错误写日志 (Open-Less#706)

下载完成后若 install() 抛错,旧代码把 status 置成 'error',而 UpdateDialog 不渲染 error 态 → 弹框消失、错误只 console.error,用户表现为「下载后无反应、重启仍旧版」(Open-Less#706)。

- 新增独立状态 installError:仅下载/安装失败用它;'error' 仍表示「检查失败」,行为不变(CheckUpdateButton 按钮内轻提示、后台自动检查都不弹框,零回归)。
- 失败弹框留在原地显示真实错误 + 「手动下载」按钮直达 GitHub Releases 兜底。
- 新增 Rust IPC log_client_error,把前端更新错误写进 openless.log(webview console 不入文件日志),便于用户「导出日志」后定位真因。
- 5 个 i18n 文件补 updateDialog.installError / manualDownload。

注意:这是兜底(失败可见 + 可诊断),不解决 ad-hoc/quarantine 机器自动更新真正成功的问题——那需要 Apple Developer ID 签名 + 公证(本次暂缓)。

* 手动补:dictation/ipc 拆分后的 OpenCode + updater 改动迁移

* fix: AutoUpdate 兼容性修复(补回 CheckUpdateOptions + Android 参数类型)

* fix(audio-cue): 重建/唤醒退化的 AudioContext,修「用久了提示音消失」

长时间使用后「开始录音」提示音与设置页「试听」双双静音,且重启才恢复。

根因在 audioCue.ts 的 AudioContext 单例 sharedCtx 从不检查/恢复运行期状态:
- WKWebView/WebView2 在频繁录音抢占音频会话后,ctx 可能转入 WebKit 非标准的
  `interrupted` 态。旧代码只判断 `suspended`,interrupted 被当作可直接排期 →
  排到非 running 的 ctx 上,无声且不尝试 resume → 永久静默(macOS 最可能根因)。
- ctx 也可能被系统 `closed`。closed 无法复活(resume 必拒、createOscillator 必抛、
  被 catch 静默吞掉),而 getContext 从不重建 → 永久静默。
- 胶囊窗口与设置窗口各持一份 ctx,都会这样退化 → 两处提示音同时失效。

修复:
- 新增纯函数 audioContextActionForState(state):closed→recreate / running→ready /
  其余(suspended/interrupted/未知非运行态)→resume,单一真相、可单测。
- getContext 在 ctx 为 closed 时丢弃重建(并清空挂在旧 ctx 上的 activeVoices)。
- playRecordStartCue / primeAudioCue 改用该分类器,把 interrupted 一并按 resume 唤醒。

公共 API 签名不变,仅 AudioCue.tsx / RecordingInputSection.tsx 复用,行为向后兼容。
tsc --noEmit 通过;audioCue.test.ts 新增 5 条状态分类断言,全部通过。

* fix(macos): onboarding Accessibility authorization page stuck (Open-Less#703 follow-up)

Root cause: permissions::request_accessibility() was called in setup(),
which runs BEFORE the AppKit event loop is live. On some macOS versions,
AXIsProcessTrustedWithOptions depends on the run loop for XPC to tccd.
When the event loop isn't ready, TCC may not show the dialog but still
consumes the one-time prompt. Later calls from the onboarding page's
'grant accessibility' button silently return Denied, trapping users on
the onboarding page. Recovery requires System Settings grant + app
restart, but users had no clear restart action — only a text hint.

Changes:

- lib.rs: Remove setup() TCC prompt. The prompt is now exclusively
  triggered by user action on the onboarding page, aligning with
  Apple HIG (explain why → then prompt).

- Onboarding.tsx:
  * onGrantAccessibility now checks the TCC result: skip
    openSystemSettings when already granted (previously always opened)
  * New tccPromptShown state distinguishes 'never attempted' vs
    'already denied' for correct button labels
  * Denied state now shows a prominent 'Restart OpenLess' button
    calling restartApp()

- PermissionsSection.tsx: Same fix for settings page accessibility
  row — check TCC result, guide to System Settings when denied,
  show 'Restart App' button

- i18n: Add onboarding.actionRestart + settings.permissions.restart
  for zh-CN / zh-TW / en / ja / ko

Closes: macos-onboarding-accessibility-stuck

* chore(release): publish 1.3.13

* fix(audio-cue): recover when resume() is rejected or wakes non-running

Long sessions park the shared AudioContext in 'suspended' / WebKit
'interrupted' after the system audio session is repeatedly preempted by
ASR and recording. The earlier fix only recreated a 'closed' context: a
rejected resume() was swallowed and a resume() that resolved without
reaching 'running' scheduled voices on a frozen currentTime, both of
which leave the start cue permanently silent until the app restarts.

Treat both "won't wake" states as recoverable — discard the dead context
and retry once on a fresh one, bounded to avoid infinite recreation. The
decision is extracted into the pure, unit-tested cueActionAfterResume.

* chore(release): integrate 1.3.14

* fix(ipc): 去除 coding-agent.ts/index.ts 中 OpenCodeDetection 和 codingAgentDetectOpencode 的重复声明

integrate/cloud-sync-20260621 合并后 IPC barrel 出现重复 export(beta 已含 PR Open-Less#638 的
OpenCode 适配器,integrate 分支又加了一份)。合并去重,npm build 通过。

* style(less-computer): 气泡/审批/发送按钮改用 accent 主题 token

把 LessComputerPanel 里三处硬编码渐变(#3d6bff→#7a55f2)替换为
--ol-accent-solid-bg / --ol-accent-solid-ink,跟随浅/深主题。

* feat(capsule/asr/ui): 胶囊即时显示与预测展开、Apple 语音修复、弹窗全局遮罩

胶囊入场(延迟出现 / 无入场动画 / 亮起→卡→展开):
- 按下热键即「乐观显示」胶囊并播入场动画,不等麦克风就绪;level_handler 首帧 PCM
  到达后从预备态翻正式态、光条点亮(新增 warming 标志贯穿 emit_capsule payload)
- 修入场动画被吞:入场帧把发给前端的事件挪到 window.show 之后再 emit
- 预测式展开:光条按历史平均加载时长(EMA + localStorage 跨会话学习)从按下就平滑
  展开,就绪早则加速补满、未就绪则慢爬至 90%,消除「亮起→卡→突然展开」

Apple 语音(说中文识别成英文 / 偶发不可用):
- initWithLocale 跟随用户工作语言主语言,修 init 无参落系统首选语言(英文)导致
  中文被英文引擎误识别
- on-device 优先(requiresOnDeviceRecognition):离线可用 + 隐私 + 不受网络波动/限流
- isAvailable 就绪轮询等待,修刚 init 时瞬时不可用就报错的竞态

弹窗遮罩:Modal 用 createPortal 到 body,修被 WindowChrome/FloatingShell 常驻
will-change: transform 祖先困住 fixed 遮罩(登录弹窗浮在亮着的设置页上、无全局压暗)

服务页 Apple 语音文案优化(zh-CN/zh-TW)

---------

Co-authored-by: 吕柏青 <lubaiqing9@gmail.com>
Co-authored-by: sim <sim@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ess#763)

@tauri-apps/plugin-shell (npm) 与 tauri-plugin-shell (crate) 的声明下限
——npm `^2.0.1`、crate `"2"`——都落在 GHSA-c9pr-q8gx-3mgp 影响范围
(`< 2.2.1`,CRITICAL:`open` 端点作用域校验绕过,可致 RCE)。本项目
src/lib/ipc/utils.ts 正是通过该 `open` 端点打开外链,属受影响用法。

两个 lockfile 实际已解析到 2.3.5,但清单声明的下限仍是漏洞版本,供应链
扫描(SkillSpector SC4)因此读取到 2.0.1 报警。此处仅把两侧声明下限抬到
最新安全版 2.3.5,使清单与锁定版本一致、扫描不再命中;解析版本不变,无
运行时行为变化。

Closes Open-Less#668

Co-authored-by: sim <sim@local>
…pen-Less#760)

Open volcengine streaming ASR's `enable_speaker_info` flag so each
returned utterance carries a speaker label. Then in the result
assembly, group utterances by speaker, pick the one with the longest
total speaking time as the "primary speaker", and only concatenate
their text — dropping utterances from other speakers (e.g. a
colleague interjecting while you dictate).

This is a heuristic quick-win for the "only transcribe my voice"
request; it does not yet do enrolled voiceprint verification. If no
speaker labels are present, behavior is unchanged (all utterances
concatenated).

Co-authored-by: Shicongcoding <Shicongcoding@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@owtffssent.com>
- 新增「从工具到基础设施 / From tool to infrastructure」专章:把 OpenLess 定位为
  消灭「想法 → 干净文字」之间每一次协调的基础设施——语气沉降成风格包、专有名词
  沉降成词典热词、文字落点沉降成插入协议、模型与凭据沉降进系统保管库,开屏授权
  一次即为默认协议。
- 改写「更新亮点(增强)」与「当前状态(能力)」两节开场,从功能罗列改为「你授权
  一次之后就不必再操心的基础设施层」;其余条目与版本号(v1.3.6)保留不动。
- 中英双 README 同步(README.md + README.zh.md),纯文档改动,不涉及代码/版本门禁。

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Open-Less#757)

Long sessions park the shared AudioContext in 'suspended' / WebKit
'interrupted' after the system audio session is repeatedly preempted by
ASR and recording. The earlier fix only recreated a 'closed' context: a
rejected resume() was swallowed and a resume() that resolved without
reaching 'running' scheduled voices on a frozen currentTime, both of
which leave the start cue permanently silent until the app restarts.

Treat both "won't wake" states as recoverable — discard the dead context
and retry once on a fresh one, bounded to avoid infinite recreation. The
decision is extracted into the pure, unit-tested cueActionAfterResume.

Co-authored-by: sim <sim@local>
…down (Open-Less#764)

OpenLessIme.dll is a system-wide TSF keyboard TIP, so CTF loads it into
every process that has text input (explorer.exe, the taskbar, ...) and
runs it on that process's UI thread. The per-process pipe server blocked
on a synchronous ConnectNamedPipe, and Deactivate() -> Stop() joined that
worker from the host UI thread, relying on a racy CancelSynchronousIo +
self-connect WakePipe() to unblock it. When both lost the race the worker
re-entered ConnectNamedPipe forever and join() froze the host UI thread
indefinitely -> explorer.exe AppHangB1 (taskbar unresponsive, Explorer
restarts). WER reports for the reported hangs show OpenLessIme.dll loaded
in explorer.exe, and disabling the TSF profile stops the hang.

Make every pipe wait deterministically cancelable: open the pipe with
FILE_FLAG_OVERLAPPED and wait on {io_event, stop_event} via
WaitForMultipleObjects. Stop() now just signals stop_event and joins, so
it returns at once, never blocks the host UI thread, and leaks no worker
thread. Connect/read/write go through WaitForClient / RunOverlapped, which
CancelIoEx + drain the overlapped op before the stack OVERLAPPED / buffer
go out of scope. Removes the racy WakePipe / CancelSynchronousIo /
pipe_handle_ / pipe_mutex_.

Pipe name, JSON wire protocol and the Rust client (windows_ime_ipc.rs)
are unchanged.

Fixes Open-Less#665
Fixes Open-Less#707

Co-authored-by: appergb <lubaiqing9@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(deps): patch tauri-plugin-shell CVE (GHSA-c9pr-q8gx-3mgp)

@tauri-apps/plugin-shell (npm) 与 tauri-plugin-shell (crate) 的声明下限
——npm `^2.0.1`、crate `"2"`——都落在 GHSA-c9pr-q8gx-3mgp 影响范围
(`< 2.2.1`,CRITICAL:`open` 端点作用域校验绕过,可致 RCE)。本项目
src/lib/ipc/utils.ts 正是通过该 `open` 端点打开外链,属受影响用法。

两个 lockfile 实际已解析到 2.3.5,但清单声明的下限仍是漏洞版本,供应链
扫描(SkillSpector SC4)因此读取到 2.0.1 报警。此处仅把两侧声明下限抬到
最新安全版 2.3.5,使清单与锁定版本一致、扫描不再命中;解析版本不变,无
运行时行为变化。

Closes Open-Less#668

* fix(audit): 1.3.14 审计修复一批(Apple Speech 取消/IME 保存锁死/侧向键卡死/火山429 等)

- CRIT Apple Speech 取消/超时不终止 SFSpeechRecognitionTask(SendableTask+cancel_flag+[task cancel]+100ms 轮询)
- HIGH windows_ime 未安装分支反致设置保存事务锁死(抽纯函数 keyboard_list_pref_short_circuit)
- HIGH 侧向键 Released 卡死自愈 + macOS flags 类位竞态 + Linux 显式拒绝
- HIGH coordinator/hotkey_loops unused_assignments clippy 回归
- MED 火山 429 限流不重试(RateLimited)/ Heatmap clamp / 授权失败反馈 / SendInput 分类单一真相 / SendInputOptions 收敛
- 清理 删死码 ?? '重启' / side_aware 死字段 / 孤儿 MarketplaceModal

cargo test 552-0 / npm build 绿。windows cfg 部分需 CI 四平台验证。

* feat(chat): 统一聊天面板组件库——官方 shadcn base 组件按 CLI 规则内嵌

@shadcn/react 官方 primitive + base-rhea registry 原文内嵌 components/chat/ui/
(message-scroller/marker/bubble/message/card/empty/input-group/button/input/
textarea/spinner,仅按 CLI 规则改 import 路径;button 保留 3 行 React18 inert
归一)。Tailwind v4 作用域隔离:utilities source(none) + @source 只扫聊天文件、
无层导入压过 global.css;官方主题 shadcn/tailwind.css + tw-animate-css。
胶囊同款思考动画(orbFeed 单 GL 源镜像多头像)、GitHub 头像(CSP img-src 放行)、
出现/消失生命周期 hook。平台门控:非 darwin 构建 DCE 掉 Less Computer chunk。
删除旧自制 Marker/MessageScroller 组件。

* feat(panels): QA/LC 浮窗重写为官方 message-scroller-demo 同款骨架

Card + Empty + InputGroup + MessageScroller 官方组合;QA=Message 带头像形态
(右 GitHub / 左旋转 orb),LC=Marker 工具行形态(无头像)。
语音状态只在输入区:录音红光/转译黑光绕输入组跑圈,问题落定即停;
思考头像行门控在用户消息落定之后(修「AI 头像先出现、动画抽搐」)。
LC 事件空轮自愈 + mount 后 less_computer_sync 重放(webview 冷加载丢
user 事件导致「后端在跑前端空白/用户的话不显示」)。纯白背景、官方
24px 圆角、出现/消失动画(chat-panel:shown/closing)。

* fix(backend): 聊天面板后端配套——命令注册/非激活面板/事件缓冲/生命周期

- 恢复桌面 invoke_handler 的 qa_submit_text / qa_toggle_recording(此前删
  qa_window_pin 时误删相邻项,真机报 Command qa_submit_text not found)
- QA/LC 窗口转非激活 NSPanel(胶囊同管线:NonactivatingPanelMask +
  CanJoinAllSpaces|FullScreenAuxiliary),chat_panel_focus_keyboard 改主线程
  makeKeyAndOrderFront:打字不激活 app、主窗口不再被带出、frontmost 保持
  用户原 app(划词选区在多轮期间仍可读)
- Less Computer 事件按全局单调 seq 写入环形缓冲(fresh user 清空、cap 2048
  丢最旧),新命令 less_computer_sync 供前端 mount 重放(仅 less-computer
  窗口可调);附 2 个单测
- show/hide 生命周期:show 发 chat-panel:shown 并作废挂起 hide;hide 先发
  chat-panel:closing、240ms 后真正隐藏(AtomicU64 世代防快速关-开竞态)
- QA/LC 窗口统一固定 420×540;删除 qa_window_pin / less_computer_window_resize
  整链(无消费方)

---------

Co-authored-by: sim <sim@local>
三个缺陷叠加导致「阿里云百炼实时 ASR」的连接检查完全不可用:

1. 「验证」必然失败:只发 1 个 100ms 静音块就 finish-task,DashScope 对
   <200ms 音频必返回 task-failed: EmptyAudio(2026-07 实测边界:100ms 拒、
   200ms 起收)——有效凭据也永远显示「操作失败」。改为发送 500ms 静音。
2. 「拉取模型」假成功:直接返回硬编码列表、不发任何网络请求,Key/endpoint
   全错也显示成功。现在先执行与「验证」相同的 WebSocket 连通性检查
   (百炼无模型列表接口,列表本身仍为静态)。
3. 接口地址误填 https://(百炼控制台兼容模式/专属域名地址)时,WS 握手报的
   "URL scheme not supported" 被前端兜底成笼统「操作失败」。现在验证入口
   校验协议头必须为 ws:// / wss://,返回专用错误码
   bailianEndpointSchemeInvalid,前端映射为含默认 wss 地址的明确提示
   (en / zh-CN / zh-TW / ja / ko)。

测试:cargo test 555 passed;npm run build (tsc + vite) 通过;
DashScope 线上协议行为已用独立脚本实测确认。
Co-authored-by: Chris233 <h-chris233@outlook.com>
Document the cleanup items from 1-app audit:
- Open-Less#27: Remove blanket allow(dead_code) and clean incrementally
- Open-Less#24: Extract shared request builder in polish.rs
- Open-Less#32: Rename misleading mobile_stubs/selection.rs

Includes execution order, risk assessment, and verification steps.
@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

699 - Partially compliant

Compliant requirements:

  • None of the three requirements are fulfilled by the changes shown in the diff.

Non-compliant requirements:

  • Blanket allow removal not addressed.
  • Stub rename not addressed.
  • Shared builder extraction not addressed.

Requires further human verification:

27 - Partially compliant

Compliant requirements:

  • None of the Windows startup changes are present in this diff.

Non-compliant requirements:

  • Windows startup behavior unchanged.
  • Microphone permission gate still blocks main UI.

Requires further human verification:

32 - Partially compliant

Compliant requirements:

  • None of these items appear in the diff.

Non-compliant requirements:

  • No credentials requirement checks added.
  • No regression script provided.

Requires further human verification:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Incorrect NSPanel Style Mask and Level

The nonactivating NSPanel conversion uses 1 << 7 (128) as the style mask. Apple's NSNonactivatingPanelMask is defined as 1 << 8 (256). Using the wrong bit may cause the panel to still activate the app, defeating the purpose. Additionally, panel.set_level(3) is used, but NSFloatingWindowLevel is 5, not 3. Level 3 (NSTornOffMenuWindowLevel) sits below floating windows, which could cause the chat panel to be hidden behind other floating windows in practice. This affects both make_chat_window_panel_macos and make_chat_window_draggable_macos usage.

fn make_chat_window_panel_macos<R: tauri::Runtime>(window: &tauri::WebviewWindow<R>, tag: &str) {
    use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior;
    use tauri_nspanel::WebviewWindowExt;
    match window.to_panel() {
        Ok(panel) => {
            const NS_NONACTIVATING_PANEL_MASK: i32 = 1 << 7;
            panel.set_style_mask(NS_NONACTIVATING_PANEL_MASK);
            // 浮层级别(NSFloatingWindowLevel):盖普通窗口,不盖菜单栏/胶囊(25)。
            panel.set_level(3);
            panel.set_collection_behaviour(
                NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
                    | NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces,
            );
            log::info!("[{tag}] converted to nonactivating NSPanel");
        }
        Err(e) => log::warn!("[{tag}] to_panel failed: {e:?}"),
    }
Missing NSException catch in ObjC msg_send! for setMovable

The make_chat_window_draggable_macos function wraps the msg_send! calls in an objc2::exception::catch block, which is good. However, there is a potential issue: if ns_window is retrieved successfully but setMovableByWindowBackground: raises an exception, the catch block captures it. But the setMovable: call is also inside the same catch. If the first call succeeds but the second fails, the exception is caught and logged. This is acceptable, but the safety comment claims that the closure has no Rust values needing drop. This is correct, but the comment could be clearer that the catch does not prevent all memory unsafety if an exception is raised (e.g., if a Rust value with a destructor were present). For now, it's safe.

/// 解法是把 NSWindow 的 `movableByWindowBackground` 打开——这条路径不依赖窗口是否成为
/// key window,跟 Spotlight / Raycast 的浮窗是同一手法。设一次就够,整个生命周期保持。
#[cfg(target_os = "macos")]
fn make_chat_window_draggable_macos<R: tauri::Runtime>(window: &tauri::WebviewWindow<R>, tag: &str) {
    use objc2::msg_send;
    use objc2::runtime::{AnyObject, Bool};
    let Ok(handle) = window.ns_window() else {
        log::warn!("[{tag}] ns_window unavailable; drag fix skipped");
        return;
    };
    let ns_window = handle as *mut AnyObject;
    if ns_window.is_null() {
        log::warn!("[{tag}] ns_window null; drag fix skipped");
        return;
    }
    // 异常兜底:AppKit 在 drag margins 状态不一致时会从 setMovable 内部 raise
    // NSException;不捕获的话异常穿过 Rust 边界直接 abort 整个 app。捕获后降级为
    // 日志(拖动失效但 app 活着),日志会指认具体异常便于追根因。
    // SAFETY: 闭包内只有两个无返回值的 ObjC 消息发送,没有需要运行析构的 Rust 值,
    // 异常展开跳过闭包帧不会破坏内存安全。
    let result = unsafe {
        objc2::exception::catch(std::panic::AssertUnwindSafe(|| {
            let _: () = msg_send![ns_window, setMovableByWindowBackground: Bool::YES];
            let _: () = msg_send![ns_window, setMovable: Bool::YES];
        }))
    };
    match result {
        Ok(()) => log::info!("[{tag}] NSWindow movableByWindowBackground=YES"),
        Err(e) => {
            log::error!("[{tag}] drag setup raised ObjC exception (caught, drag disabled): {e:?}")
        }
    }
Potential Data Race on Global Event Log

LESS_COMPUTER_EVENT_LOG is a global OnceLock<Mutex<LessComputerEventLog>>. The log_less_computer_event function holds the mutex lock while pushing to the VecDeque. However, the emit_less_computer function only locks when the log is available; if the lock is poisoned (e.g., panic while holding the lock), every subsequent call will skip the log and fall back to emitting without seq, which is a degraded behavior. This is an acceptable trade-off but could lead to silent data loss if the poison persists, and unwrap_or_default() in less_computer_event_backlog() swallows the error. Consider using std::sync::PoisonError::into_inner to recover or at least log the poisoning.

static LESS_COMPUTER_EVENT_LOG: std::sync::OnceLock<std::sync::Mutex<LessComputerEventLog>> =
    std::sync::OnceLock::new();

fn less_computer_event_log() -> &'static std::sync::Mutex<LessComputerEventLog> {
    LESS_COMPUTER_EVENT_LOG.get_or_init(|| {
        std::sync::Mutex::new(LessComputerEventLog {
            next_seq: 0,
            events: std::collections::VecDeque::new(),
        })
    })
}

/// 纯逻辑:给 payload 编 seq 并写入缓冲(fresh user 先清空,超限丢最旧)。
fn log_less_computer_event(log: &mut LessComputerEventLog, payload: &mut serde_json::Value) {
    let fresh_user = payload.get("kind").and_then(|k| k.as_str()) == Some("user")
        && payload.get("fresh").and_then(|f| f.as_bool()) == Some(true);
    if fresh_user {
        log.events.clear();
    }
    log.next_seq += 1;
    payload["seq"] = serde_json::json!(log.next_seq);
    log.events.push_back(payload.clone());
    while log.events.len() > LESS_COMPUTER_EVENT_LOG_CAP {
        log.events.pop_front();
    }
}

/// `less_computer_sync` 命令的数据源:当前会话已发生的事件(seq 升序)。
pub(crate) fn less_computer_event_backlog() -> Vec<serde_json::Value> {
    less_computer_event_log()
        .lock()
        .map(|log| log.events.iter().cloned().collect())
        .unwrap_or_default()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants