beta
66
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6f9af89583 |
release: OpenLess 2.0.0-Beta.4+build.20260930 (#1126)
Integrate ASR/Omni fixes, API Route, Android IME and confirmation-based vocabulary learning. Complete settings transitions and macOS single-item credential storage. Synchronize all application versions to 2.0.0-Beta.4+build.20260930. Integrated PRs: #1116, #1120, #1121, #1123, #1124, #1067 and #1128. Linux #1117 remains independent and is excluded from automatic release. Validated: full cross-platform CI, 18 UI motion cases, 112 frontend test files, 1245 Core tests (1 ignored) and 572 macOS native tests. Co-authored-by: HKLHaoBin <we3q@qq.com> |
||
|
|
913279c049 |
feat(linux-egui): native EGUI parity host (#1060)
* refactor(linux): move shared runtime assets out of Tauri
* feat(linux-egui): adopt redesigned native shell
* feat(linux-egui): add typed popup process bridge
* feat(linux-egui): add native desktop services
* feat(linux-egui): add verified AppImage replacement
* feat(linux-egui): render native popup processes
* feat(linux-egui): bind vocabulary corrections and styles
* feat(linux-egui): add real history actions
* feat(linux-egui): wire native popup supervisors
* feat(linux-egui): bind marketplace and device login
* fix(linux-egui): do not overreport update capability
* feat(linux-egui): honor minimized startup
* test(linux-egui): lock Tauri-free parity contracts
* fix(core): keep provider URL validation clippy-clean
* refactor(linux-egui): keep view data clippy-clean
* feat(settings): transact launch at login effects
* feat(linux-egui): complete verified AppImage updater
* feat(linux-egui): restore native desktop lifecycle
* feat(linux-egui): restore history audio workflows
* feat(linux-egui): complete local model controls
* feat(linux-egui): add native style pack editor
* feat(linux-egui): complete marketplace workflows
* feat(linux-egui): merge dirty settings on revision conflict
* feat(linux-egui): complete fcitx5 desktop hotkeys
* feat(linux-egui): harden and restore native popups
* feat(linux-egui): bind shared vocabulary presets
* feat(linux-egui): edit style hotkeys without blocking frames
* feat(linux-egui): persist and export diagnostic logs
* feat(linux-egui): edit all fcitx5 hotkeys
* fix(linux-egui): follow terminal capture cancellation
* docs(linux-egui): record parity and device limits
* docs(linux-egui): track latest 1019 head
* fix(linux-egui): reload fcitx5 after addon update
* feat(linux-egui): restore recording and appearance settings
* feat(linux-egui): restore Core-backed overview
* feat(linux-egui): restore native recording feedback
* ci(linux-egui): lock release artifacts and checksums
* feat(linux-egui): localize native interface
* fix(linux-egui): tolerate legacy fcitx addon methods
* [pi] Work in progress
* [pi] Work in progress
* [pi] All tasks are complete. Here's a summary of what w...
* fix(linux-egui): preserve original frontend content geometry
* fix(linux-egui): align beta contracts and slim release
* ci: make Linux PR and release gates reliable
* fix(linux): remove dead frontend artifacts and restore autostart
* fix(settings): handle launch-at-login in Tauri restore
* fix(ci): follow relocated Linux remote input assets
* fix(ci): normalize Windows contract paths
* refactor(linux): rely on packaged fcitx addon
* feat(linux): add audio backend fallback and fcitx clipboard
* ci(linux): use native deb and rpm packaging
* fix(linux): bound addon reload and use app version
* fix(linux-egui): restore input and CI build
* fix(linux-egui): align window input regions
* fix(linux-egui): restore frontend input routing
* wip(linux-egui): 2.0 parity checkpoint before parallel page rewrites
* chore: parity agent orchestration script
* [pi] 我负责的三个功能性 bug 全部修完,全量测试通过。报告如下。
* [pi] 全绿(24 个 test binary 全 ok;`coding_agent` 那条是已知时序 fl...
* [pi] 快捷键与选区做完了,全量验证通过。
* [pi] 三个浮窗按 Tauri 重做完了,旧的臆造实现已整段删除。
* [pi] 四项遗留全部做完,并回答了 Vulkan 的问题。
* [pi] Siri 光效 GPU 化(GLSL 移植)+ 圆角矩形扫光着色器,接胶囊与思考态;Vulkan 结论附证据
* [pi] 实现完成,验证全绿并打好包。
* [pi] Work in progress
* [pi] Work in progress
* [pi] **任务 1(胶囊 wlr-layer-shell 主路径)完成并出包。**
* [pi] 任务 2 完成。**开工时先核对了前任的产物,结果发现任务 1 其实从未真正生效**——两个阻塞性 ...
* [pi] 三个问题里属于我这个工单的(③ 吞键、④ 选区助手)已经修完并出包。**其中「吞键」我现在就把你机器...
* [pi] 任务2:静音失败归一化 + 胶囊终态自动收起
① 不说话报 InvalidArgument:
- 根因 Core `mark_dictation_failed` 把错误码名当 message(format!("{:?}", code)),
宿主原样送进胶囊/状态栏;空音频在 Core 里就是 InvalidArgument。
- 新增 linux-egui/src/dictation_feedback.rs(纯函数):capsule_outcome 分类相位+message,
内部错误码永不作为用户文案;normalize_stop_result 把 InvalidArgument/InvalidState/
Busy/Cancelled 这类预期内停止错误归一为「没有结果」,真失败仍上报。
- 三个停止/取消入口(胶囊 ✕/✓、Core 自动停止)改走 normalize_stop_result。
② 弹窗不自动收回:
- 按 Tauri coordinator::core_capsule_hide_delay 契约落地 capsule_hide_delay:
成功/失败 2s、取消立即、进行中不收;schedule_capsule_dismissal 延时后经
UiResult::CapsuleDismissDue 回主线程,capsule_hide_is_still_current 用会话+相位
双守卫,避免误关用户中途新开的录音。
- 收起=结束弹窗进程(layer surface 只能随进程销毁),并在进行中的相位按需
ensure_popup 重新拉起(phase_shows_capsule 排除终态,避免迟到事件把药丸喊回来)。
- 新增进程级测试:request_shutdown 必须真的让子进程退出且不算 crash。
验证:全量 cargo test 全绿(含 localization 契约)、i18n --check 干净(未新增 key)、
两个发布契约通过;包 2.0.0-Beta.1-28。
* [pi] 修复热键:修饰键触发不再吞键、Shift+符号键匹配(Ctrl+Shift+;),插件更新后自动重启 fcitx5
- 插件侧新增 hotkey_match.h 纯函数:字母大小写折叠 + US 布局 base/shifted 视为同一物理键
(实测 fcitx 的 Key::normalize 对符号键只改 states 不改 sym,所以注册 ';' 永远匹配不到到达的 ':')。
21 条断言的 hotkey_match_contract 覆盖 Ctrl+Shift+; / Ctrl+; 不误触发 / 字母 / 裸修饰键。
- 取消「裸修饰键一律拒绝注册」:改由插件保证不吞修饰键(shouldConsume),
按住修饰键当热键期间若按下其它键则判定组合键并放弃触发。
- 录制器支持录裸修饰键(跨帧看 Modifiers:按住→无其它键→松开),记为 LeftControl 等。
- 新增 /proc 时间戳比较:安装包替换 libopenless.so 后,若插件比运行中的 fcitx5 新,
启动时自动 fcitx5 -r,否则新匹配规则不会生效。
- 诊断日志:命中与「修饰位一致但键不同」的近似未命中(限速 1 次/秒)。
验证:cargo test 全绿;ctest 2/2(input_target_contract + hotkey_match_contract);
deb 内 libopenless.so 与新构建 sha256 一致(66d534f0…)。
* [pi] 任务2:报错路径胶囊收回(Core reset 时序 + 宿主兜底 + 宿主断开自退场)
* [pi] Work in progress
* [pi] 执行完了。取证结论很明确,而且**deb 打包没问题**。
* [pi] Work in progress
* [pi] 修复:启动不再被插件探测/重载阻塞(主窗口必现)+ /usr/local 手工残留不再冒充随包插件 + 指纹持久化(消除每次启动重载);热键注册摘要与取证日志
* fix(linux-egui): 胶囊去掉自造外圈扫光(Tauri 经典药丸只有 1px 中性描边)+ 风格包草稿行不再误改已有行 + 打包 postinst 改 D-Bus 重载
- 胶囊:录音/思考不再画红/黑外圈(用户报「语音输入弹窗有一个红边」),
改成 Tauri 的「1px 中性描边 + 随音量放大 1.8%」;中心声波/流体圆点保留。
两条新测试锁死:录音相位不得出现偏红涂色、GPU 回调数固定 1/1/0。
- 风格直达快捷键:草稿行选择器曾把 draft_pack 当行号下发 StyleHotkeyRepack,
会把已有行的包改掉,且 StyleHotkeyDraftPack 从未构造(编译告警)。
新增 style_pack_pick_action(row: Option<usize>, next) + 测试。
- popup_layer:去掉不必要的 unsafe 与未被使用的 FrameCallback 别名。
- 打包脚本:postinst/postrm 不再 `fcitx5 -r`(会替换 daemon 并前台常驻、装包卡 5s),
改用 D-Bus controller Restart(实测 0.002s 返回)。
* fix(linux-egui): Ctrl+Shift+; 热键被 CapsLock 状态位吃掉——插件按 fcitx 的 normalize 语义只比 ctrl/alt/shift/super;近失日志降到 Info 并加可开关逐键 trace;宿主逐项记录注册的 (sym,states)
* fix(linux-egui): fcitx5 缺席/插件过旧不再拖死后端——启动链对 fcitx5 失败改为警告降级(heatkey listener 变 Option)
* ci(release): 共用一条跨平台发布流水线(-tauri/-egui 按后缀分流)+ 末端聚合 zip
Linux egui 的发布此前是一条从没被调用、也从没跑成功过的独立工作流:
fork 里 dispatch 时它带了一个不以 v 开头的 release_tag,在第 8 步
(依赖安装 + fcitx5 插件构建 + 全量测试之后)报
"release_tag must name an existing v* release",白烧 4.5 分钟 runner。
- release-tauri.yml 变成唯一一条发版流水线,按 tag 后缀决定构建哪些平台:
v*-tauri → macOS/Windows 的 Tauri 包;v*-egui → Linux deb/rpm(egui 原生宿主,
通过 workflow_call 复用 release-linux-egui.yml);手动 dispatch → 两侧都构建。
- 新增 bundle-all-artifacts:把本次 run 的全部 artifact 收成一个
OpenLess-<tag>-all-artifacts.zip(zip 名自带 -tauri/-egui 后缀),
根目录附覆盖全部文件的 SHA256SUMS,tag 推送时挂到同一个 release。
与 build job 同一套渠道判定,beta release 不会被这个只挂 zip 的 job 升格。
- release-linux-egui.yml:tag 校验提到编译之前(几秒出结论);release target
从「release 必须先存在」改为「tag 必须存在」,让 -egui 这个新命名空间
首次推送时能自己建 release;版本推导先剥 -egui(-tauri 仅作历史兼容,
保持 release 契约测试要求的写法);首次为 -egui tag 建 release 时正确标 prerelease。
* fix(linux-egui): 关闭键改为退回托盘(不退出进程)+ 托盘唤起按 最小化恢复→显示→聚焦 回任务栏;QA 热键主键被输入法引擎占用时释放(pinyin QuickPhraseKey,带备份)
* ci(release): Linux egui 发布 job 换到 ubuntu-24.04(与 ci.yml 同因)
实测 run 35120587777:22.04 上冷缓存重编 libspa 0.10.1 直接编译失败
(spa_video_info_raw has no field named flags / missing spa_meta_first),
整条发布流程卡在第 8 步。ci.yml 的 linux-core-contract 早就因为同一个原因
换到 ubuntu-24.04 并留了注释,这条工作流从没跑过所以一直没跟上。
注意:deb/rpm 的 glibc 基线随之抬高到 24.04(glibc 2.39);若需要覆盖
22.04 用户,得在依赖层解决 libspa/PipeWire 版本,而不是换 runner。
* ci(release): 单一 -egui tag 触发同一 run 内四平台并发(macOS arm64/x64 + Windows + Linux egui)
按用户拍板的方案 B 收口 tag 约定:v<version>-egui(例 v2.0.0-Beta.1-egui)
一次推送 → 同一个 run 里四平台并发构建 → 所有产物挂同一个 Release
→ 末尾 bundle-all-artifacts 打成 OpenLess-<tag>-all-artifacts.zip。
- 后缀不再是平台分发开关:-egui tag 下 build(Tauri 三腿)与
release-linux-egui(workflow_call,tag 由编排层传入)都跑,zip job needs 全四者。
- build 里所有 release 相关步骤(签名可用性检查、release body prelude、
Create/update release)从「仅 -tauri」放宽到任何 v* tag,否则 -egui 发布时
dmg/msi/exe 只进 artifacts、不会挂到 Release。
- 历史 v*-tauri tag 兼容保留:仍只跑 Tauri 三平台(旧行为),文件头注释写清
两者关系与迁移注意(Cask URL/README/bump-version.sh/android-apk.yml)。
- Linux 产物命名统一带 egui 标识:deb/rpm 已是
OpenLess-Linux-egui-<ver>-<arch>(脚本原有),校验和文件改名 SHA256SUMS-egui,
避免与其它平台/聚合层那份混淆;版本串仍由传入 tag 推导(剥 -egui)。
- update-homebrew-cask 保持仅 -tauri:cask 下载 URL 写死 v#{version}-tauri,
迁移到 -egui 前不能动,已作为待办写进文件头注释。
* fix(linux-egui): 关闭键真正隐藏(Wayland 塌缩 1x1,不能 unmap/最小化)+ 事件泵用真线程保活 + Ctrl+Shift+; 匹配「Shift 折进符号」
三个实测问题(-40 真机反馈):
1. 点关闭键没反应:winit 在原生 Wayland 上 `set_visible` 是空实现
(winit-0.30 wayland/window/mod.rs「Not possible on Wayland」),
所以 -40 的 `Visible(false)` 等于什么都没做。最小化虽受支持,但最小化的
surface 拿不到 frame callback → eframe 的 update() 停摆 → 热键、托盘、
单实例唤起全部失效(实测心跳停、按热键无反应、重启也拉不回来)。
改为 Wayland 上隐藏 = 临时放开最小内尺寸 + 塌缩到 1x1 并保持映射:
窗口看不见了,事件循环照常跑(实测隐藏后 recording=true,热键仍生效;
再次启动能把窗口按原尺寸恢复)。X11 仍走真 unmap。
2. 事件泵保活必须用真线程:tokio 运行时是 current-thread,spawn 的任务只在
别处 block_on 时才推进,当后台泵用等于没保活。
3. Ctrl+Shift+; 到达插件时是 sym=0x3a(':') 且 states 里【没有 Shift 位】
(trace 实测;字母如 Ctrl+Shift+S 则是 sym=0x53 states=0x05)。注册值
0x3b/0x05 因此永远匹配不上。匹配规则改为:符号相同则严格比修饰位;符号
成 base/shifted 对时,只有注册值本身带 Shift 才接受、并忽略事件里的
Shift 位(Ctrl+; 与 Ctrl+Shift+; 双向仍可区分)。近失日志同步放宽到
「除 Shift 外的修饰位一致」,否则这类失败永远没有日志。
* ci(release): 四平台真正并发——egui job 去掉 needs: build,Linux 资产的 release 写入交给聚合 job
needs: build 会让 Linux 腿等 macOS/Windows 编完才启动,那不是四平台并发。
去掉它之后两个 job 可能同时建同一个 release,所以:
- release-linux-egui.yml 新增 attach_release 输入(默认 true 保持单独 dispatch 行为)
- 编排层传 attach_release: false,Linux 资产改由 bundle-all-artifacts(needs 四者、
最后一个 job)统一挂到 release,顺带和 zip 一起写,写入点唯一。
bundle job 里 Linux 资产那步用 hashFiles 守卫:egui 腿失败时不会让整步失败、
也不会把 zip 一起拖下水(zip 自己有 test -s 硬门禁)。
* ci(release): 聚合 zip 只装 Linux 平台产物(用户口径:all 指 Linux 各产物)
- bundle-all-artifacts 只下载 openless-linux-egui-x86_64 这一个 artifact,
zip 根部就是打包脚本的原始文件名(.deb/.rpm/SHA256SUMS),不再塞 dmg/msi/exe,
也不再另造一层汇总校验和——打包脚本自带的 SHA256SUMS 就是权威。
- Linux 校验和文件命名改回 SHA256SUMS(撤销上一轮的 SHA256SUMS-egui)。
- needs 仍为 [build, release-linux-egui]:它仍是 Linux 资产的唯一 release 写入点
(egui job 保持 attach_release: false)。
- ubuntu-24.04 的分发基线写进注释:glibc ≥ 2.39,支持范围 Ubuntu 24.04+,
22.04 / Debian 12 不在支持范围内(用户已确认,防止被改回去)。
- Homebrew、Cask、README、bump-version.sh、android-apk.yml、macOS/Windows 构建步骤均未动。
* fix(linux-egui): 最小化键改为退回托盘(Wayland 无法取消最小化,最小化的窗口会永久停掉 egui 重绘循环 → 热键消费与弹窗拉起一起失效)
- 有托盘:最小化 → 与关闭键同一条 hide 路径(Wayland 塌缩隐藏,事件循环继续跑)
- 无托盘:保持真正的窗口管理器最小化,否则窗口找不回来
- 新增 minimize_action() 决策函数 + 测试锁死该语义
* fix(linux-egui): 发布门禁单测不再依赖宿主是否装了 deb
a_per_user_copy_only_shadows_when_the_package_has_the_addon 之前直接断言
/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so 存在:装了 deb 的开发机绿,
干净的 CI runner 上 is_file()=false → 该断言必挂,于是 Linux 发布腿在第 8 步
「Verify framework-independent Linux contract」失败,后续 upload/checksums
与聚合 zip job 级联失败。
改为 tempfile 自建 per-user 文件,覆盖「无文件」与「有文件 + 包内有 addon」两个
分支,并保留「无包内 addon 时不 shadow」的反例断言。用 unshare + tmpfs 隐藏系统
插件目录后该测试仍通过,证明不再依赖宿主文件系统。
* fix(linux-egui): 删除自动改写输入法配置的逻辑,并用契约测试锁死
已实测证伪「拼音的 QuickPhraseKey=semicolon 会吃掉 Ctrl+Shift+;」:
fcitx 的 Key::check 要求修饰位精确相等(semicolon 是 states=0,而
Ctrl+Shift+; 到达时是 states=Ctrl),两者本就能共存。用户真机验证:
保持 QuickPhraseKey=semicolon 时按 Ctrl+Shift+; 面板照常弹出。
- settings.rs: 去掉 apply_hotkeys 里对 release_engine_key_conflict 的调用
- fcitx5.rs: 删除 release_engine_key_conflict{,_in}、release_engine_reserved_key、
engine_reserved_keys、fcitx_key_aliases、fcitx_punctuation_name、
reload_addon_config 及 4 个相关单元测试(-259 行)
- tests/fcitx5_config_contract.rs: 新增不变量契约,断言宿主源码里不出现
输入法配置目录与引擎保留键;已用注入违规验证它非空转
- 真机 A/B:旧二进制会把临时 HOME 的 QuickPhraseKey 清空并留日志,
新二进制字节不变、0 条日志,且启动路径确实跑过(有热键注册日志)
* [pi] Work in progress
* fix(linux-egui): 恢复被清理删掉的 provider/凭据编辑能力,发布门禁不再被拦
`1157fe51` 的界面重写把旧 monolith 的 provider 编辑器一起删了,新设置页只留住
「渠道列表 + 新建 + 启停 + 校验 + 删除」,于是 Linux 版既不能录入/修改/删除云服务
凭据,也不能改名、换 provider 类型、重排渠道、选当前渠道 —— 发布流水线第 8 步的
`check-linux-public-surface.ps1` 正是在拦这个(它的注释写明:只有状态、或只有本地
模型捷径的界面不算可用安装路径)。
- 宿主重新接上 Core 的 provider/凭据 API:rename_channel、set_channel_provider_type、
reorder_channels、set_active_provider、set_credential、remove_credential,以及
provider.validate / provider.list_models(模型目录属于 Core)。
- 编辑器草稿放在宿主侧(ProviderEditorForm)并每帧镜像给 view model:重新拉取渠道
列表不会冲掉正在输入的内容;密钥字段只写,打开编辑器不会把已存密钥读进 egui。
- 界面补回:渠道行的编辑/上移下移/换类型/设为当前,以及按 AuthRequirement 渲染的
编辑器(Endpoint/Model/Resource ID/密钥/鉴权模式 + 读取模型 + 保存 + 清空密钥)。
- 顺手修掉「新建渠道」名称框推的是编辑前的值:宿主会把旧值写回,每敲一个字被回灌。
- 新增本地契约测试 provider_surface_contract.rs,把这组 Core 调用钉住,避免下次合并
或清理再把实现删掉(历史上正是这样丢的)。
* ci(release): 聚合 zip 改成「手动安装」散装包(不再装 deb/rpm)+ 附 install.sh
用户澄清了这个 zip 的用途:不是把安装包再打一层,而是给装不了 deb/rpm 的发行版
手动安装用。所以:
- payload 用 `dpkg-deb -x` 解开 deb 得到精确安装树(usr/bin/openless、
usr/lib/*/fcitx5/libopenless.so、addon conf、desktop、metainfo、icon),
并新增硬门禁:散装清单必须与 `dpkg-deb -c` 的文件清单逐项一致(漏文件最难发现)。
- 新增 `openless-all/app/scripts/linux-egui-manual-install.sh`,随包发布为
install.sh:检查 root、先验 SHA256SUMS、按用途给权限铺到 /、用 D-Bus controller
Restart 重载 fcitx5(与 deb postinst 一致,不用会卡住的 `fcitx5 -r`),最后打印
重启与卸载提示。
- SHA256SUMS 改为对散装文件 + install.sh 重算;zip 内硬门禁断言不得出现 deb/rpm。
- zip 名自解释:OpenLess-Linux-egui-<tag 或 run>-x86_64-manual.zip;job 名/artifact
名与注释一并从 all-artifacts 改为 manual install,避免语义继续误导。
- deb/rpm 与它们自己的 SHA256SUMS 仍作为独立 release 资产保留(那条 attach 步骤没动)。
- bundle job 补 checkout(脚本随仓库版本走,不内联进 workflow)。
* ci(release): Linux 腿内联成平级 job(不再折叠)+ deb/rpm 与 manual zip 合并到一个 job + 升 action 到 node24 版本消掉 node 警告
- 去掉 workflow_call 调用:release-linux-egui.yml 的步骤内联为 release-tauri.yml 里与
build 平级的 linux-egui job,Actions 页面上不再被渲染成嵌套折叠条目
- 同一个 job 内完成 deb/rpm 打包与手动安装 zip 组装(删掉单独的 bundle job)
- release 竞态:内联后与 Tauri 三层并发写同一 release,先用 gh release view||create
幂等地建出来(吞掉并发 422),softprops 只做 attach/update
- manual zip 语义:散装 usr/ 树 + install.sh + 重算 SHA256SUMS,不含 deb/rpm;
deb/rpm 仍作为独立 release 资产
- action 升版(仅这两个发布工作流):checkout v4→v6、setup-node v4→v7、
upload-artifact v4→v7,消除 GitHub action 自身 node20 运行时带来的
DEP0040 punycode / DEP0169 url.parse 弃用警告(用户点名的 node 警告)
- 契约测试加锁:断言 Linux 腿必须是内联普通 job、manual zip 必须在同一 job 产出、
两份副本的关键门禁不能漂移
* ci(release): 统一发布语义——任何 v* 标签都出全平台(Tauri 三平台 + Linux egui + 安卓)
- release-tauri.yml: push.tags 收敛为 'v*';build 与 linux-egui 的 if 不再按 -tauri/-egui
后缀分流(后缀降级为命名约定);Homebrew cask 仍只跟稳定的 -tauri 正式版
(注释写明该边界与影响,避免被顺手放开)
- android-apk.yml: 触发从 'v*-tauri' 扩展为 'v*';is_tag_release 判定不再要求 -tauri;
OPENLESS_RELEASE_CHANNEL 判定与桌面统一(含 -beta-egui);未签名 debug fallback 语义不变;
注释说明为何 Android 是独立 workflow(另一套 JDK/SDK/Gradle 工具链,塞进 matrix 会让
每步都长满 if)以及它的资产如何落到同一个 release
- 顺带把这几个 action 升到 node24 版本(checkout v6 / setup-node v7 / upload-artifact v7),
与发布工作流一致,消掉 action 自身运行时带出的弃用警告
- 契约测试新增:两份工作流都必须对 'v*' 触发、安卓必须挂到同一个 release、
build/linux 两个 job 必须无条件运行、Homebrew 仍锁 -tauri
* feat(linux-egui): 关窗后切到无窗口宿主(窗口进程真退出)+ 事件泵不再依赖窗口绘制
- 抽出与渲染无关的 tick():原生事件、托盘命令、自动更新检查、心跳日志;
eframe 的 update() 与无窗口宿主的 run_headless() 循环共用它,热键消费与
弹窗拉起不再依赖「窗口是否在绘制」(最小化/隐藏/无窗口都照常工作)。
- 关闭键/最小化键:有托盘且没有进行中的会话时切到无窗口宿主。Wayland 下
winit 无法隐藏窗口(set_visible 是空实现),只有窗口进程真的退出,任务栏
与窗口列表里的条目才会消失;会话进行中退回托盘,避免新进程重开后端丢会话。
- 进程形态切换用同可执行文件的新进程 + --takeover:新进程等旧进程释放单实例
锁后接管(不在交接期被转发后直接退出),托盘在接管后重试一次;接管超时降级
为无锁启动,保证托盘/窗口仍可用。
- Core 的 ShowMain/FocusMain 提示在无窗口宿主里不拉起窗口进程(那只是「把已有
窗口推前面」),只有托盘「显示主窗口」才是明确的用户动作。
* feat(linux-egui): 常驻无窗口宿主 + 独立 UI 窗口进程(真实时 IPC,关窗不再重开后端)
形态:宿主进程(无窗口,长期存活)持有 Core 后端 / 数据目录 / 单实例锁 /
热键监听 / 托盘 / 弹窗监督器;UI 窗口进程(--ui-client)只渲染。
- 新增 `src/ui/bridge.rs`:unix socket + JSONL 双向协议。快照带单调递增序号,
UI 只采纳更大序号(重放/乱序/回退一律丢弃);快照可合并(UI 慢时跳帧,
绝不画过期状态),控制帧永远先于待发快照落盘。
- 关窗 = **只有窗口进程退出**(Wayland 下任务栏条目真的消失);托盘「显示主窗口」
或用户再次启动 = 宿主拉起新窗口进程,宿主 PID 与后端状态原地不动
(实测:宿主 PID 不变、开窗约 50ms,不再重开后端)。
- 时序:先 bind socket 再拉 UI(UI 连不上就直接退出);UI 断连立即作废窗口句柄,
会话/录音/弹窗不受影响;宿主退出先发 Shutdown 再释放单实例锁。
- 删除重启式交接:--headless / --takeover / HandoffMode / spawn_handoff_process /
塌缩 1x1 隐藏 / 最小化改道 / 事件泵后台唤醒线程全部移除。
- `FrontendViewModel` + `FrontendAction` 加最小 serde 集:Lang 按 tag 序列化
(i18n.rs 不允许手改),theme_mode 随快照下发,UI 进程据此配色。
- 事件消费与弹窗拉起改由宿主循环驱动(无窗口 ⇒ 必须线程化),另加
OPENLESS_UI_DEBUG=1 诊断开关记录 UI 侧指针点击与动作。
* feat(linux-egui): 设置页排版对齐 Tauri(行=200px 标签列+1fr 控件列/左对齐、卡片与分区标题 14px)+ 浮窗尺寸语义对齐(qa 420×540、preview 640×440 可缩放且抢焦点)
* feat(linux-egui): 补齐设置页剩余排版对齐(行标签 500 字重 / 输入框 32·13.5 / 分段选中片阴影 / 无热键后端隐藏快捷键分区)
* feat(linux-egui): 在 Linux 实现 Less Computer 浮窗(420×540 / 常驻宿主唯一所有者 / 事件序列经 IPC 呈现 + 审批与取消链路)
- 新增 PopupKind::LessComputer(--less-computer)与协议版本 4:HostToPopup::LessComputer
携带宿主事件序列(user/tool/compaction/assistant/error)、working、审批与错误;
PopupToHost 增加 Submit/Approve/Cancel/Dismiss 四条动作。
- 宿主是事件序列唯一所有者:Core 的 LessComputerEventKind 逐条转成面板条目,
流式增量合并为一条助手条目,终局正文替换它;窗口进程重连即收到完整序列。
- HostAction::ShowLessComputer 不再是空实现;面板尺寸取自共享常量(420×540),
位置与 qa 一致(居中卡片);设置里的开关此前只翻转 coding_agent_enabled,现在
有真实 UI 可看。
- 顺手修正 qa 弹窗视口尺寸的硬编码 520×520(应为共享常量 QA_WINDOW_SIZE=420×540)。
* [pi] # 选区读取为什么是「缓冲区里最后一次」——调查结论
* feat(linux-egui): 设置页顶栏/录音行/胶囊设置与试听按 Tauri 补齐 + 模态居中修正
用户实测反馈的七条:
1. 去掉设置页顶部那一层「设置」标题栏(桌面端左右两栏各自通到顶:左栏顶端是
搜索框,右栏顶端才是「分类标题 + 修改后自动保存 + 关闭」,对齐 SettingsModal
的桌面分支)。
2. 通用页第一行「录音快捷键」此前是只读文本,改成真正的 ShortcutRecorder
(行内展开 → 录制 / 停用),并补上行下方的录音方式提示语。
3. 补上「录音提示音」的试听按钮,走与录音开始同一段合成提示音。
4. 补上「录音胶囊」开关与「胶囊样式」选择(siri / classic / typeless),并让样式
真正改变胶囊渲染;关掉开关时胶囊立刻收起。协议 v4 → v5 增加 style 字段。
5. 默认录音方式一行补上可见提示语(Tauri modeDesc)。
6. 麦克风枚举过滤 cpal/PipeWire 混进来的输出监听与重复节点,并在枚举失败/无设备
时显式提示;每次打开设置重新枚举。
7. 设置模态此前稳定偏下 19.5px、卡片被内容撑宽 22px(横向偏 11px),且在窗口缩放
后要等下一帧才居中:改为由 body.center() 显式算位置并修正两栏宽度预算。
* fix(linux-egui): 补齐 P0 —— 窗口边缘拉伸光标 + 锁定胶囊按钮可点
P0(本轮核对结果):
* 胶囊退出/结束转录按钮可点:行为本来就是对的(layer-shell 只给指针输入,两个圆钮
靠 `ui.interact` 命中),但没有任何测试锁住它。新增 `the_capsule_buttons_report_
cancel_and_confirm`:把真实指针事件按到 ✕ / ✓ 的圆心,必须分别返回 Cancel/Confirm。
* resize 光标:**确实缺失** —— 八个拖拽区只发 BeginResize、不设光标,无边框窗口
完全没有「这里能拉伸」的提示。按方向设置 ResizeHorizontal/Vertical/NwSe/NeSw,
并新增 `the_window_edges_show_a_resize_cursor_on_hover` 四角四边逐一断言。
第 8 条(qa / Less Computer 避开任务栏)按决定**不改代码**:Wayland 没有
skip-taskbar 协议,winit 的该开关只在 Windows 生效;换成自研 layer-shell 就必须
自己补 xkb 键码 + zwp_text_input_v3 + wl_data_device,而 winit 在 Wayland 上已经
提供输入法支持(fcitx5 中文输入依赖它)—— 为一个任务栏条目牺牲追问面板的中文输入
不划算。
* test(linux-egui): 锁住两个被报为回归的交互(拖标题栏移窗口 / 分组开合)
用户报「拖标题栏移不动窗口」「设置里可展开分组点不开」。排查结果:**两者都不复现**。
- 无头:`the_titlebar_press_starts_a_window_drag`(标题栏按下必须发出
ViewportCommand::StartDrag)、`the_collapsible_group_header_toggles_on_click`
(点分组标题行后内容消失 = 真的点开了)—— 都通过。
- 真机(uinput 绝对坐标注入,源码版单实例):
* 点侧栏「历史」→ 截图对比 AE=21984,页面确实切到历史记录(点击被送达)
* 拖标题栏(+150,+100)→ KWin 读到的窗口几何 340,118 → **490,218**(窗口真被拖走)
- 结论:两条都不是代码回归。真机不clean 状态(陈旧单实例套接字 / 两个宿主并存)
会让人以为交互失灵 —— 本轮排查时我确实复现过这种假象。
把这两条固化成回归测试,以后真回归会被抓住。
* test(linux-egui): 补上分组开合测试用的 test-only 入口
上一个提交 d69642ac 里的 `the_collapsible_group_header_toggles_on_click` 依赖
settings.rs 里的 `test_group_toggle`(cfg(test) 包装 card_group),漏提交会让
干净检出编译不过。这里补上。
* [pi] Work in progress
* feat(linux-egui): 润色结果并入「选区助手」面板(收尾 9a109965 的半成品)
9a109965 把独立 preview 窗口拆掉了但停在中间态:面板里还留着「可编辑」的
TextEdit、`preview_focus_requested` 只为它服务的字段、以及指向已删窗口的注释与
常量。这次把它做完并定型:
* 润色结果模式 = 选区助手面板里的第二套 UI(`polish_result_mode`),签名收成
`&QaPolishState`:**只读展示**(原先的可编辑 TextEdit 换成可选可滚动的只读
文本区,用户要求不做就地编辑),底部按钮集与 Tauri `SelectionPolishPreview.tsx`
一致:✕ / 取消(`selectionPolishPreview.cancel`,line 72/125)+ ✓ 确认并替换
(`selectionPolishPreview.confirmReplace`,line 139)—— 后者就是把结果写回
原选区的「插入」(→ `PopupToHost::ConfirmPolish` → Core `selection.confirm`)。
* 副标题跟着语义改:Tauri 的 `selectionPolishPreview.subtitle` 原写「可直接编辑」,
与只读冲突,5 个语言一起改成「只读结果;点「确认并替换」才会写回原选区。」
(egui 目录由 `scripts/sync-egui-i18n.mjs` 写入)。
* `HostAction::HideSelectionPreview` 只撤掉「润色结果」模式:面板若正在提问对话
中,不能被润色流程的收尾动作关掉。
* 清掉半成品留下的尾巴:`preview_focus_requested` 字段、`PreviewAction` →
`PolishResultAction`、`selection_preview_visible` → `polish_result_visible`、
已删窗口的常量注释(`PREVIEW_WINDOW_SIZE`/`PREVIEW_MIN_SIZE`/`PREVIEW_PADDING`)。
验证:`cargo test --locked` 1257 passed / 0 failed;`sync-egui-i18n.mjs --check` 干净。
* [pi] Work in progress
* fix(linux-egui): 标题栏拖动补两点 —— 未聚焦时先要回焦点、最大化时先还原再拖
egui-winit 对 ViewportCommand::StartDrag 带 window.has_focus() 前置条件
(egui-winit-0.33.3/src/lib.rs:1403-1409),未聚焦那一帧会被整帧丢掉,而 Wayland
的 xdg_toplevel.move 只认按下那一帧的 serial;另外最大化窗口的 move 请求会被
合成器忽略。现在:未聚焦时同帧补发 Focus,最大化时先 Maximized(false) 再拖。
* fix(linux-egui): 设置弹窗降到 Order::Foreground,修「点开的控件被弹窗盖住」
egui 的 ComboBox 下拉/弹出菜单是 Order::Foreground(popup.rs:150),而同层后创建者
在上、跨 Order 是 Tooltip > Foreground。设置弹窗占着 Tooltip,于是里面所有
ComboBox(渠道 provider / 认证方式 / 新增渠道 / 通用下拉等)展开后都被弹窗挡住,
表现为需要点开的控件都打不开。降到 Foreground:先创建的遮罩输入层仍在其下,
弹窗内再打开的下拉因创建更晚而在其上。
* fix(linux-egui): 设置遮罩改用 Area 排序(不再盖在弹窗上)+ 去掉「模拟粘贴快捷键」选项
- 遮罩原先用 ctx.layer_painter() 建原生图层,同 Order 内相对位置不受控;弹窗从
Tooltip 降到 Foreground 后遮罩反而盖到弹窗上面(用户报「设置整体被阴影层压住」)。
合并进原本就存在的 backdrop-input Area 并在弹窗之前创建,遮罩稳定在下面。
- 按用户要求移除「模拟粘贴快捷键」设置行(Ctrl+V / Ctrl+Shift+V)。
* feat(linux-egui): 接入上游 #1082 的「稳定模式(先录音后识别)」
合并 origin/beta 后唯一的冲突是 linux-egui/src/main.rs:上游往旧的 settings_ui 里加了
一个复选框,而那一整块我们早已重写成前端模块架构 —— 逐块保留 HEAD 侧,并把该设置
按新架构接上(否则上游这个新开关在 Linux 侧等于不存在):
view model 增 stable_transcription、SettingsField::StableTranscription、
SettingsDirty 白名单与 merge、宿主回灌与写回偏好、录音卡片里的开关行;
i18n 两个 key 由 sync 脚本从 Tauri 目录写入(904 keys,--check 干净)。
* fix(linux-egui): 遮罩与卡片合并成同一个 Area(点遮罩不再盖住卡片)+ 条带提到 Tooltip
- 阴影上移的真因:egui 在 Area 被按下时把它抬到同层最上面
(egui-0.33.3/src/containers/area.rs:549 move_to_top)。遮罩原本是独立 Area,
于是点一下遮罩就盖住卡片、设置没法用。现在遮罩、点击拦截与卡片同属一个 Area
且遮罩先画 —— 结构上不可能压住卡片。
- 设置打开时改不了窗口大小:遮罩覆盖 body,而左/右/下三侧拉伸条带落在 body 范围内,
被遮罩吃掉。条带提到 Order::Tooltip(层级高一层,仍只占外圈 6px/18px)。
- 卡片矩形改写进 egui memory,供居中/宽度测试查询(Area 面积已不等于卡片)。
* refactor(tauri): 删除 Tauri Linux 专有文件(linux_fcitx.rs / tauri.linux.conf.json)
Linux 桌面不再由 Tauri 提供(已改用 egui 前端 openless-all/app/linux-egui),先删掉
两个只服务 Linux 的文件:
- src-tauri/src/linux_fcitx.rs(976 行):fcitx5 插件桥 —— 热键同步、CommitText 插入、
PRIMARY 选区读取、aux-down 跟随、AppImage 期插件自动安装。egui 侧有自己的
linux-egui/src/fcitx5.rs,不依赖它。
- src-tauri/tauri.linux.conf.json:Linux 平台覆盖配置。
调用点与编译期门禁在后续提交里一并清理(见 7b778d9b、68fe1442)。
* refactor(tauri): 删除 Tauri 的 Linux 桌面实现(Linux 已改用 egui 前端)
Linux 桌面不再由 Tauri 提供,相关实现整体删除:
- src-tauri/tauri.linux.conf.json(Linux 平台覆盖配置)
- src-tauri/src/linux_fcitx.rs(976 行的 fcitx5 插件桥:热键同步、CommitText 插入、
PRIMARY 选区读取、aux-down 跟随、插件自动安装)
- hotkey.rs 的 Linux 平台 adapter 模块(fcitx5 作为热键源)
- unicode_keystroke.rs 的 Linux impl / enigo 错误变体 / Linux 再导出 / 相关测试助手
- insertion.rs 的 Linux insert(fcitx CommitText + 剪贴板回退)与 3 条 Linux 测试
- selection.rs 的 Linux PRIMARY 选区读取模块与两处 Linux 分支
- lib.rs 的 mod linux_fcitx 与 AppImage 期插件自动安装调用
为免误在 Linux 上构建 Tauri 版时面对一堆“找不到模块”的零散错误,lib.rs 增加一条
显式的 compile_error!(仅 target_os = "linux"),指向 egui 前端与出包脚本。
macOS / Windows / Android(target_os = "android")不受影响。
egui 侧未受影响:cargo check --workspace 通过、openless-linux-egui 测试全绿(181+87…)、
package-linux-egui.sh 出包成功。
* feat(linux-egui): 图标装多档 hicolor 尺寸 + 提示音与 Tauri 逐参数对齐
图标(用户要求:复用 Tauri 的图标集):
- egui 侧不能引用 Tauri 源码树(两条契约都禁止脚本出现该路径),所以把
src-tauri/icons 的 5 档 PNG 以**字节相同的副本**放进 linux-egui/packaging/icons/,
由 release 契约做逐字节一致性断言 —— 素材仍是一份,且不会静默漂移。
- package-linux-egui.sh 由「只装一档、且把 512×512 的图塞进 256x256 目录」改为装
32/64/128/256/512 五档 hicolor(256 取自 128x128@2x.png,512 取自 icon.png)。
- 已核对 public/AppIcon.png 与 src-tauri/icons/icon.png md5 相同(7017a34f…,均 512×512)。
提示音(用户要求:与 Tauri 完全一致):
- 参数本就与 audioCue.ts 逐字段一致;本轮补上两处**语义**差异:
* 包络改为 Web Audio 的两段指数斜坡语义
(0.0001 → 5ms 指数 attack 到 peakGain → 指数 decay 回 0.0001),
原来是线性 4ms attack + 任意 tau 的 exp(-5t) 衰减;
* 去掉 egui 自加的「结束提示音」(下行小三度)—— Tauri 的 stopAudioCue() 只停不发声;
现在 play_cue_stop() 等价于作废当前代次(正在播的提示音静音),与 Tauri 同语义;
* 新增代次机制:新提示音开始时在播的旧音立刻静音,对应 Tauri 排期前的 stopVoices()。
- 设置页「试听」继续复用同一实现(main.rs 的 PreviewAudioCue → play_cue_start())。
* refactor(tauri): 清掉已删除的 linux_fcitx 模块的残留调用点
linux_fcitx.rs 与 Linux 平台实现删除后,仍有多处 Linux 专有路径引用已删的模块/函数。
这些调用只出现在 cfg(target_os = "linux") 分支里,但等于给 Linux 构建留一堆悬空符号,
所以按「Linux 桌面不再支持」一并清理:
- coordinator/capsule_focus.rs:整段 fcitx5「候选词下方显示听写状态」的 aux-down 逻辑
(LAST_AUX / RETRY_GEN / set_aux_down / clear_aux_down)。
- coordinator/hotkey_loops.rs:启动前的插件可用性检查、fcitx 通道准备、
start_dictation_signal_listener 启动块,以及 custom_dictation_key_string /
sync_custom_dictation_to_plugin 两个 Linux 辅助函数。
- coordinator.rs:ensure_modifier_hotkey_monitor 里的插件绑定同步、同样的 fcitx 监听
启动块,以及 combo 热键更新路径里的插件同步调用。
- selection.rs:两处「其它平台」兜底不再排除 Linux(非 Linux 代码不动)。
- core_adapters.rs / unicode_keystroke.rs:流式插入的 Linux 分支不再调用已删的
type_unicode_chunk,改返回显式的 TypeError::UnsupportedPlatform。
macOS / Windows / Android 不受影响(这些分支本就不为它们编译;mac 有自己的
switch_to_ascii / restore_input_source 实现)。egui 侧未受影响:workspace check 通过、
openless-linux-egui 测试全绿。
* refactor: 把 Remote Input TLS 身份实现移到平台中立目录 shared/remote_tls
原先 linux-egui 通过 #[path] 直接引 src-tauri 的源码,egui 无法在 Tauri 之外独立构建。
现在两侧(src-tauri/remote_server、linux-egui/remote_input、backend-tests)都引
shared/remote_tls/tls_identity.rs,egui 对 src-tauri 的引用清零,并收紧
linux-egui-tauri-free-contract(去掉 remote_input.rs 的豁免)。
* fix(linux-egui): 修风格市场对官网 API 的请求放大
对齐 Tauri 市场页(src/pages/Marketplace.tsx):
- 搜索加 300ms 防抖(原先每敲一个字符、以及每次输入法预编辑变化都发请求)
- my_likes 从 load_marketplace 拆出,改为每次会话只取一次(原先每次搜索都带一个认证请求)
- 列表请求加单调序号,丢弃过期响应(对齐 Tauri 的 reqSeqRef,latest-wins)
- limit 100 -> 50
原先一次 4~6 字符的搜索会打出 8~12 个请求(半数为认证请求),输入法预编辑下更多。
* [pi] 盘点完成(只读,未改任何仓库文件)。报告已落盘 `/tmp/ime-inventory-report...
* refactor(fcitx5): 删除插件里已无人调用的候选词下方状态弹窗
SetAuxDown/ClearAuxDown 的实现、焦点切换补写、PostInputMethod 恢复、
lastAuxText_ 缓存全部删除:Tauri 的 Linux 调用点已随 linux_fcitx.rs 一起删除,
egui 从未调用过,全仓库已无调用者(纯死代码)。
* mod(selection): 划词润色预览独立弹窗下线,改为复用选区助手(qa)面板
- 删除独立窗口 selection-polish-preview:窗口创建、能力表条目、前端路由
(main.tsx / App.tsx)与页面 src/pages/SelectionPolishPreview.tsx 一并移除
- 后端 show_selection_polish_preview 改为 show_qa_window(app, "polish-preview")
并向 qa 发 selection-polish-preview:shown;hide 只发 hide 事件,是否收起面板
交给前端判断(面板可能正在提问对话中,不能被润色流程收尾一起关掉;与 egui
侧 HostAction::HideSelectionPreview 的语义一致)
- 新增 SELECTION_POLISH_PREVIEW_PENDING 标志:get_selection_polish_preview 只在
真正处于润色模式时返回负载 —— 面板懒创建后挂载补拉不会误入润色模式
- QaPanel 增加「润色结果」模式:标题/副标题、只读结果框、原文摘要、
取消 / 「确认并替换」(= 写回原选区的插入,走 Core selection.confirm);
Esc 与 ✕ 都是取消并关闭。沿用 selectionPolishPreview.* 文案(5 语言已具备)
- 新增 scripts/selection-polish-panel-contract.test.mjs 锁住合并(npm test 自动发现)
- docs/architecture.md 的窗口清单同步
* docs(linux-egui): 注释改为指向 Tauri 侧合并后的选区助手面板
Tauri 的独立润色预览窗口(SelectionPolishPreview.tsx)已下线,注释不再指向已删文件。
* refactor(tauri): 删除 Tauri 侧剩余的 Linux 专用实现
Linux 桌面已由 egui 前端承担,Tauri 不再构建 Linux。本次清掉残留:
- 纯 Linux cfg 块:audio_mute 的 wpctl/pactl 后端、tauri_coordinator_host 的
gtk 版胶囊窗口与 Linux 早退分支、permissions 的 Linux 麦克风探测、lib.rs 的
gtk 光标穿透与 WebKitGTK nudge、main.rs 的 WebKit 环境变量、core_adapters 的
Linux ASR/插入分支、asr/local 的 Linux 后端与空实现、side_aware_combo 的
「Linux 暂不支持」分支
- 12+ 个文件上的 cfg_attr(target_os = "linux", allow(dead_code, ...)) 抑制
- 保留 lib.rs 的 compile_error! 防线(有意拦住误在 Linux 上构建 Tauri)
- 保留 any(target_os = "macos", target_os = "linux") 共享块(macOS 本地 Qwen ASR)
- 补齐非 mac/win 桌面的中立兜底(hotkey 后端桩、insertion 剪贴板兜底、ASR 不可用),
不引入任何 Linux 命名;删除无调用者的 host 包装 switch_to_ascii/restore_input_source
* chore(tauri): 移除 Linux 专用依赖与 wayland-scanner vendor 补丁
- 删 [target.'cfg(target_os = "linux")'.dependencies](dbus 已零引用、gtk 只被删除的
Linux 代码使用)与 keyring 的 unix-except-macos/android 段(含 linux-native-sync-persistent)
- 删 [patch.crates-io] wayland-scanner 与 src-tauri/vendor/wayland-scanner/(只为 Linux
构建修 issue #801 的 quick-xml 漏洞)
- arboard 去掉 wayland-data-control feature
- Cargo.lock 随 manifest 重解析(-379 行)
* refactor(react): 移除前端里的 Linux 分支与专属文案键
- WindowChrome:OS 类型/UA 嗅探/LinuxTitlebar/ol-linux-* 类名/专用常量全部删除,
外壳圆角恒为 0(交回原生装饰),并清掉随之无用的 import 与 SVG 组件
- RecordingInputSection:去掉 os !== 'linux' 条件与 Linux 专属「流式输入」开关行
- AudioCue / localAsr / platform / ChannelList / CodingAgentSection / ShortcutsSection:
本地引擎与能力判断只保留 macOS / Windows / Android
- global.css 删掉 html[data-ol-platform='linux'] 规则(该属性在所有分支都已不再取 linux)
- i18n 9 个语言删除 streamingInsertTitleLinux / streamingInsertHintLinux(React 与 egui 均无引用),
同步更新 experimentalTitles.test.ts;「仅 Windows / Linux」这类平台说明文案保留(Linux 仍由 egui 提供产品)
* test(contracts): 新增 tauri-linux-free 契约并修受影响的既有契约
- 新增 scripts/tauri-linux-free-contract.test.mjs:断言 src-tauri 无 Linux 专用 cfg、
manifest 无 Linux 目标依赖/wayland-scanner patch、vendor 补丁目录不存在、React 无 Linux 分支、
lib.rs 的 compile_error! 防线仍在(反证:注入探针文件后契约确实失败)
- macos-capsule-spaces-contract:函数体边界改为「下一项平台属性」,不再假定紧跟 Linux 实现
- windows-ui-config:外壳断言改查 shellRadius 恒 0;删除已退役的 Linux 标题栏断言
* fix(linux-egui): 胶囊边距按实测面板高度计算,不再依赖 exclusive_zone(-1)
KDE 下胶囊会压在底部面板上:原先靠 exclusive_zone(-1) 让合成器把表面挪出其他
独占区,但并非所有合成器都兑现该请求。现在改为自己量:
- bind wl_output,读 CURRENT mode 得到整屏高度
- 两个一次性 layer surface 探针(anchor=bottom+left+right,size=0)分别请求
exclusive_zone(-1) 与 (0),拿到合成器给出的可用高度,探针随后销毁
- 纯函数 capsule_bottom_margin/PanelInsetProbe 决策:合成器不避让面板时
margin = panel + gap;已避让时 margin = gap;测不到时退化为 gap(原行为)
- 胶囊改用 exclusive_zone(0),边距自己扛;测量值打到 stderr 供实机核对
- 新增 5 个单测覆盖决策树
* Revert "fix(linux-egui): 胶囊边距按实测面板高度计算,不再依赖 exclusive_zone(-1)"
This reverts commit 73c59cfd3750e70dcc07933b5224b79b6574d838.
* fix(fcitx5): 选区探针自行读 PRIMARY,修「旧选区被当成当前选区」
现象:在新选区里选中了没有 text mime 的东西(图片/文件/密码)时,划词助手
拿到的还是**上一次**的选区文本。
根因(已用源码核实):fcitx5 clipboard addon 把 PRIMARY 缓存在全局 primary_,
其 waylandclipboard.cpp 的 DataOffer::receiveRealData() 在 offer 既没有
text/plain;charset=utf-8 也没有 text/plain 时直接 return,回调不触发、setPrimary()
不被调用 —— 缓存里留着上一次的文本。而 IClipboard 公共接口
(clipboard_public.h)只有 primary()/setPrimary(),没有任何"选区变了"或"还有效吗"
的通知,插件无从判断缓存是否过期。
改动:
- 新增 primary_selection.{h,cpp}:用 ext-data-control(KWin 6 提供,
协议规定绑定 device 时补发一次 primary_selection,故一次 roundtrip 即拿到当前
状态)读当前 PRIMARY 的 mime 列表与文本;区分 NoSelection / NoText /
ReadFailed / Unsupported,并沿用 fcitx5 的密码管理器 hint 与 1s→150ms 超时策略。
- ③ 来源优先级改为「合成器此刻的 PRIMARY」优先,应用 surrounding 只在探针给不出
结论时兜底(它是客户端本地缓存,且协议限定 ≤4000 字节)。
- ② NoText 判失效,既不回退 clipboard 缓存也不回退 surrounding。
- ④(部分):更新插件文件头 GetSelectionText 的说明;GetSelectionText 与
CaptureSelectionTarget 共用同一探针,探针不可用才退回缓存。
- 构建:内置 wayland-protocols 1.49 的 ext-data-control-v1.xml(见 protocols/README.md),
由 wayland-scanner 生成胶水;project() 需声明 C 语言,否则生成的 .c 会被
CMake 当作头文件跳过(链接期 undefined reference)。
- 契约:input_target_contract 通过 setPrimaryReader 注入探针结果,新增 ①②③ 用例。
* fix(fcitx5): 探针不可用时才回退 PRIMARY 缓存,并加契约双向覆盖
任务书 ② 要求“新选区没有 text mime 就判失效、不回退旧值”,但严格照做会让
**探针不可用**的场景(X11 会话、合成器没有 ext-data-control)失去唯一来源:
X11/XIM 类应用既不导出 PRIMARY、也没有 surrounding text,于是划词彻底拿不到
选区 —— 相对改动前是功能回退。
改为分级边界:
- 探针**给出结论**(NoText / NoSelection)→ 仍严格判失效,绝不回退旧值(② 的本意);
- 探针**不可用**(Unsupported)且应用 surrounding 也为空 → 才退回 clipboard
addon 缓存,这是最后一级兜底,也是改动前的行为。
为此给缓存读取加了注入缝 selectionCacheReader_(默认实现仍是
cachedPrimarySelection()),契约用例据此同时覆盖两条边界:
- Unsupported + surrounding 空 → 命中 "clipboard-cache-fallback"(返回缓存文本);
- NoSelection / NoText → 捕获为空、getSelectionText() 也为空,缓存里有值也不读。
* refactor(linux-egui): 市场/风格弹窗遮罩统一为「单 Area」,历史确认框加固
「点阴影后阴影上移、设置没法用」的根因是遮罩与卡片处在不同容器:egui 会在指针按下时
把被按到的 Area 抬到同层最上(egui-0.33.3 area.rs:549 的 move_to_top),独立遮罩一旦
被抬起就盖住卡片。设置弹窗此前已改成「遮罩 + 点击拦截 + 卡片同属一个 Area」,这次把
同样的结构铺到其余两处遮罩:
- marketplace.rs:删掉 painter 遮罩 + 独立 backdrop-input Area + Tooltip 卡片的三段式,
合并为单个 openless-marketplace-detail-modal(Foreground)。卡片高度由内容决定(描述
行数可变),所以把上一帧实测高度记在 memory 里居中(首帧用估算值 + request_repaint
收敛),并把本帧**实际画出**的卡片矩形写进 openless-marketplace-detail-card-rect
供测试查询。
- style.rs:同款三段式合并为单个 openless-style-editor-modal(Foreground);卡片尺寸本
来就固定,矩形直接算出并写进 openless-style-editor-card-rect。
- history.rs:confirm_overlay 本来就是单 Area,但 Area 只 sense hover —— 点遮罩会漏到
下方页面(确认框开着时列表行仍会被选中)。改成 Sense::click() 吃掉点击(Tauri 这里用
原生 window.confirm(),点外面不关闭,所以只拦截不关闭),并写
openless-history-confirm-card-rect。
- 前两处顺带从 Order::Tooltip 挪回 Foreground:跨 Order 是 Tooltip > Foreground,弹窗
占 Tooltip 会盖住同层弹出的下拉/菜单。
- layout.rs 的 resize 条带仍是 Order::Tooltip(必须在遮罩之上),未改动。
测试(ui/frontend/mod.rs):新增 assert_overlay_keeps_the_card_on_top 结构性断言与三处
弹窗各一条用例——卡片必须落在弹窗自己的 LayerId 上、在自己的遮罩区里居中、遮罩点上的
输入层也是弹窗自己;点一下遮罩后卡片既不移位、也不会被抬起的遮罩盖住。反向验证:把遮罩
临时挪到另一个容器(不同 Order/不同 Area)后,用例立即失败于
「the card must live in the modal's own layer」,还原后通过。
* fix(linux-egui): 自己窗口里的热键本地兜底,弹窗崩溃重启加预算,QA 热键按 Tauri 切面板
插件只在 fcitx5 的 InputContextKeyEvent 上匹配热键,而我们的窗口从不注册
text-input(winit 默认 ime_allowed=false),所以窗口有焦点时插件一个信号都
不发:主窗口里热键全死,选区助手弹出并开始录音后按语音热键也停不下来。
现在每个窗口进程(主窗口 + 各弹窗)自己做本地匹配,复用设置页录制器的
captured_binding 与注册给插件的 (keysym, states),并与插件信号共用一张去重表
(同一物理按键只处理一次)。
弹窗崩溃(Exited{crashed:true})以前会无条件立刻重开,必崩的面板就变成
无限弹出;改为 60s 窗口内最多重开 N 次,超预算停手并记日志。
QA 热键以前走 Core 的 toggle_recording(切换录音),与 Tauri 的「切换面板显隐」
不一致;现在按 Tauri 语义切面板。
* fix(linux-egui): openless --help/--version 只打印并退出,不再启动应用
* fix(linux-egui): 润色结果沿用选区助手面板的外壳
润色内容并进选区助手面板后,egui 侧仍自己一套排版:没有头尾分隔线、
没有选区助手的 14px 内容边距,看着就像已经下线的独立预览窗口。现在
CardHeader / CardContent / CardFooter 的留白、字号、分隔线全部与选区
助手面板一致(头部还可拖动窗口)。
新增回归测试 the_polish_result_reuses_the_selection_ask_chrome:断言
润色模式画出的头部分隔线与选区助手面板的位置、宽度一致。
* docs: Linux egui 依赖升级计划(依赖批次 / Rust 1.98.1 / Vulkan-wgpu 工作量评估)
把升级调研结论固化进仓库:当前图形栈与全部依赖的可升/已最新清单、
四批执行计划(Rust 工具链、semver patch、10 个 breaking、egui 0.36+glow 0.17)、
Vulkan(wgpu) 的文件级工作量(siri_gl.rs 1090 行 / popup_layer.rs 919 行 / 4 个
GLSL→WGSL)与'双后端不划算、先 profiling'的结论、以及 egui 升级必查的回归清单。
状态:暂缓执行,后续照单执行。
* fix(linux-egui): 修「alt+X 弹出空面板」与「录音热键误弹选区助手」
两个宿主侧状态/时序 bug:
1) show_selection_popup() 先 ensure_popup 再 early-return:没有选区或没
有会话时会把弹窗进程拉起来却一个内容帧都不发 → 凭空出现一个**空的
选区助手面板**(用户报的 alt+X 弹出错误弹窗),并留下
polish_result_visible 脏标志。现在先校验负载再拉弹窗,缺负载时记 warn
并清标志。
2) qa_visible 残留:面板 ✕、弹窗进程正常退出、崩溃重启预算用尽这三条
路径都没清 qa_visible,于是下一次「录音热键」被 apply_local_hotkey_edges
当成「向选区助手提问」,把面板又弹出来(用户报的第二个症状)。现在
统一走 forget_qa_panel_visibility(),并要求 qa_state 存在才算面板在
提问态。
* alt+X 不再弹空润色面板:快照要带选区且结果非空,否则只在语音窗报错
- SelectionStateChanged 以前只看 phase:起手 Capturing 就 ensure_popup +
送一帧空 PolishPreview,于是按 alt+X 会先冒出一个空的润色结果面板
(用户报的「弹窗不对」),没有选区时更是空转一圈再消失。
- 现在要求 phase=Preview 且 preview_text 非空、并且 source_text(捕获到的
选区)非空,才把润色结果帧送进选区助手面板并置 polish_result_visible。
- 没有选区时改为弹语音输入窗(胶囊)报错后退出:文案用 Tauri 的
capsule.selectionPolish.noSelection('未选中内容'),终态 2 秒后自动收起,
不再静默无反馈。
- 诊断日志:有快照但结果不可用时打 debug,无选区时打 warn,便于下次真机定位。
* fix(core): 缓冲转写后台挂接改走注入的 TaskSpawner
CI 的 linux job 卡在 Enforce Linux dependency boundaries:
crates/openless-core/src/dictation_engine.rs:1154 在生产代码里直接
tokio::spawn,绕过 TaskSpawner(check-core-runtime-seam.ps1 明令禁止)。
该行由上游 a8ebcf6ff(#1082 录音优先/稳定模式)引入。
- BufferedTranscriptionInner 增加 task_spawner 字段;
BufferedTranscriptionSession 增 with_task_spawner 构造器,
new/new_with_limit 保持原签名并默认 TokioTaskSpawner(与
BackendDependencies::default 同源)。
- buffered_transcription_session 接收 task_spawner。
- DictationEngine 增加可覆写的 buffered_task_spawner(),默认返回
TokioTaskSpawner;start_transcription_with_progress 用它喂给缓冲区。
- attach_in_background 不再直接 tokio::spawn,行为不变。
* chore(linux-egui): 清掉全部编译警告(死代码与残留状态)
bin 与 test 构建现在都是 0 warning:
- UiResult 里 ProviderMutation / MarketplaceFlow / MarketplaceAuthPoll /
UpdateProgress / UpdateInstalled 五个变体从未被构造(新前端没有市场登录
与「安装更新」入口),删掉变体及其处理分支。
- OverviewSummary 的 last_7 / last_30 恒无读者(前端用 activity_daily 的
尾段自己切窗口):连 ActivityAggregate、aggregate_window 一起删,
测试改为只断言 daily/heatmap 的窗口化。
- 旧 settings 表单草稿字段(qa_input / new_channel_name / vocabulary_* /
correction_* / vocab_preset_* / history_search / marketplace_flow /
style_hotkey_*)在 OpenLessEguiApp 上已无读者,删字段与两处构造初始化
(弹窗进程自己那份 qa_input 保留)。
- ProvidersState::Failed / ProviderEditorState::Failed 的 String 负载、
ProviderEditorState::Loading 的 kind/channel_id 无人读,改为无负载变体。
- 删掉被新 UI 取代的 shortcut_editor / optional_shortcut_editor;
set_style_pack_hotkey 只有测试用,移进 mod tests。
- animated / painted 的初值恒被覆盖,改为延迟初始化。
- 顺手修掉 --help 那个测试上重复的 #[test](重复注册了同一个用例),
以及 popup.rs 测试模块里没用的 use super::*。
* [pi] Work in progress
* [pi] Work in progress
* test(linux-egui): 跟上 egui 0.36 的测试侧 API,并消费 FullOutput 的纹理增量
d055873a 把 egui/eframe 升到 0.36.2 时只改了生产代码,测试目标编译不过
(13 个 E0308/E0599/E0560),另有一批测试在 drop FullOutput 时 panic。
- 0.36 删掉了 CentralPanel::show(&Context, ..) 与 Context::run:测试改用
ctx.run_ui(RawInput, |ui| ..),即取 root Ui 再交给被测函数。
- RawInput 不再有 modifiers 字段:改在 begin_pass 后用
ctx.input_mut(|i| i.modifiers = ..) 注入(仅测试需要)。
- epaint 0.36 起 FullOutput 的 TexturesDelta 未处理就 drop 会 panic
("Dropped TexturesDelta with N unapplied deltas")。无头测试没有渲染器,
新增 run_pass / end_pass 两个测试助手统一 clear,替换 50 处出口。
剩余 6 个 GPU 回调断言仍失败:d055873a 把 siri_gl 的 glow 着色器路径从渲染
路径上摘掉(见该文件第 7 行的 allow(dead_code) 注释),paint() 现在只画 CPU
形状且恒返回 true,warm_up() 是空函数,但测试还在断言 Shape::Callback。
* test(linux-egui): 光效测试跟上「着色器路径已摘除」的现状
d055873a 把 siri_gl 的 glow 着色器从渲染路径上摘掉(paint 改为直接用 CPU
形状画光效并恒返回 true,warm_up 成空函数),但 6 个测试还在断言旧的
「GPU 回调 + CPU 回退所有权」语义。按现状改写断言,等 wgpu 光效落地后再恢复:
- paint_draws_every_mode_on_the_cpu_and_claims_ownership(原
paint_queues_a_gpu_callback):三种模式都画 CPU 形状且返回 true,帧里没有
Shape::Callback。
- warm_up_queues_nothing_while_the_shader_path_is_off:warm_up 不排队、不改
状态,也不影响 paint 的所有权结论。
- a_rejected_driver_still_gets_the_cpu_glow:GPU_FAILED 时 CPU 光效照画并
拿走中心,回调数 0。
- capsule_keeps_the_centre_glow_off_the_gpu_path(原
capsule_queues_the_gpu_glow_per_state):录音/思考的中心光效仍在(CPU
波形折线 / 流体圆点,新增 count_centre_glow 计数),回调数恒为 0,终态
无光效;继续锁死「没有外圈红扫光」。
- recording_capsule_keeps_its_centre_visual:中心视觉的判据补上 CPU 波形。
- capsule_paints_state_specific_content:文字口径按样式分——classic 中心画
「思考中」文字,siri 中心是光效不再叠文字(与 GPU 就绪后的终态一致)。
测试:--lib 192 passed、bin 93 passed、5 契约 PASS、i18n --check PASS。
* [pi] 搞完了,包给你:
* fix(linux-egui): refine titlebar and hotkey capture
* fix(linux-egui): synchronize provider channel editing
* fix(linux-egui): align shortcut handling and settings UI
* fix(linux-egui): refine Siri capsule and popup layout
* fix(linux-egui): keep capsule within desktop work area
* fix(linux-egui): keep capsule native on Wayland
* fix(linux-egui): keep Wayland capsule from stealing focus
* fix(linux-egui): align settings controls in fixed columns
* fix(linux-egui): refine Siri capsule and settings identity
* fix(linux-egui): 设置行固定标签列,模型块照 Tauri 重写
两处用户报的问题 + 三个测试构建遗留错误。
1) 录音与输入等设置页的开关/下拉又贴回标签了。17c8a620 把 row_desc 从
ui.horizontal 换成 allocate_ui_with_layout,但没撑住标签列宽度——egui 只按
内容实际宽度推进光标,于是标签列塌缩成文字宽度,控件紧贴标签(实测同一页
12 个开关的 x 是 235/167/124/138/181/100…)。现在显式 set_min_width,恢复
Tauri 的 minmax(0,200px) + 16px 间距硬列宽。新增几何回归测试
setting_rows_put_every_control_on_one_column 把「所有控件同一列」钉住。
2) provider 编辑器的「模型」块照 Tauri ProvidersSection 重写:
- 模型字段(ChannelModelField):descriptor 带 staticModels 时是预设下拉,
末尾附「自定义模型…」逃生口切手输;无预设时是手输框;值为空且有
defaultModel 时给「填入默认」。
- 可用模型(ProviderTools):当前 endpoint 命中带文档页的预设时按钮是
「查看支持的模型」(打开文档),否则「拉取模型」;拉取为空时明说
「鉴权成功,但没有返回可用模型。」;拉到后是带搜索框的下拉,**选中即写入
模型字段并落地凭据**(Tauri applyModel),不再要求用户再按一次保存。
- orcarouter 这类没有固定清单的服务商,打开编辑器时自动拉一次目录。
仍然替换掉自造的 providers.model_list / btn.list_models 文案,改用 Tauri
catalog 的 settings.channels.modelTitle/modelHint/availableModels 与
settings.providers.*(i18n 同步新增 16 个 key)。
3) 修 ChatGPT 那两个提交留下的测试构建错误(以前根本没编译过,所以从没跑):
popup.rs 里 env 迭代拿到的是 &OsStr,不能和 &str 直接比;fcitx5_contract /
host_contract 还在用 TranslationPressed 的旧 unit 形态;capsule 的 Wayland
断言与 popup_command 实际行为不符(只设 OPENLESS_CAPSULE_PATH,不显式设
WAYLAND_DISPLAY),改成断言「不删 WAYLAND_DISPLAY + 带上 layer 提示」。
测试:lib 192 passed、bin 95 passed、5 契约 PASS、i18n --check PASS、fmt 干净。
* fix(linux-egui): 对齐上游 Beta.2 的窗口层次、侧栏与历史详情
用户拿官方 Tauri Beta.2 实况(视频 2026-09-25)逐项对照,一次修完:
窗口/侧栏底色(左右搞反了)
- 侧栏改 n--ol-sidebar-bg (#f0f0f1),内容区底板从前是 CANVAS (#fafafa) 改回
纯白(Tauri 的 ol-console-main 是 --ol-surface);两个层次之前正好反着。
- 自绘标题栏不再是纯白,改浅灰 #f4f4f5 + 一条底部分隔线(对照视频里那条
标题栏带的灰底);标题栏高度 38 → 36(对齐 Tauri LINUX_TITLEBAR_HEIGHT)。
- 新增几何回归测试 shell_paints_a_gray_sidebar_over_white_content_and_a_
tinted_titlebar,直接钉住「侧栏灰 / 内容白 / 标题栏更灰」。
BETA 徽章与版本号
- 徽章照 Tauri FloatingShell:透明底 + 0.5px 蓝描边(--ol-pill-blue-border)+
圆角 5 + 10px 半粗蓝字(之前是蓝淡底、无描边、圆角 7)。
- 版本号另起一行:侧栏只有 188px,同行会把版本号拦腰折断
(「版本 v2.0.0-」/「Beta.2+…」),视频里上游也是换行显示的。
设置/内容区留白与滚动
- 页面顶部留白 0 → 56px、底部 8 → 24px,对齐 Tauri 的页面容器
(padding: 56px 28px 24px)——「今日概览」「历史记录」这些标题下移。
- 滚动条改预留 8px 槽位(Tauri 是 scrollbar-gutter: stable),不再悬浮压住
右边那列文字。
概览页
- 「概览」导航图标改成上游的 lucide ChartNoAxesColumn(只有三根柱,没有
坐标轴);之前多画了一条 L 形轴。
- 大标题 / 分区标题改用 Medium 字重(Tauri 是 600),字号不变。
- 窄屏判定改成按**窗口宽度**比 720(Tauri useMobileLayout 的阀值),不再是内容区
比 760:主窗口缩到最小(960)时内容区只有 748,之前会错把「星期几张图」和
「最近识别」上下堆叠。窗口太矮时整页滚动,不再把年度活动热力图裁掉。
历史页(按用户要求 + 上游 History.tsx)
- 详情头右侧的三个平铺按钮(删除/重新转录/导出)收进「…」菜单:右对齐、向下
展开,含 播放 / 导出录音 / 重新转录 / 删除(上游还有「重新润色」,egui 后端
还没这个能力)。菜单默认收起,点外面或 Esc 关闭。
- 流水线明细只留「识别」一行(用来语音识别的模型);润色/插入两步不再展示。
- 结果区默认只显示润色结果(胶囊是「润色 · 风格名」),原文靠「查看原文 /
隐藏原文」开关展开,不再默认摆两栏重复内容;润色失败时回退显示原文。
- 新增 i18n key:history.show_raw / history.hide_raw / shell.beta_tag /
history.play_recording(前两个同时补进 Tauri 的 8 个语言文件,与上游一致)。
省略号「···」变「…」
- 根因是字体:本机 fc-match sans-serif 命中的是 Noto Sans CJK SC,CJK 面的
U+2026 是**居中**的全角省略号;把拉丁面(sans-serif:lang=en)排到链首,
与 macOS 的 system-ui(拉丁优先、CJK 回退)一致。
测试:lib 192 passed、bin 96 passed(新增 1 个配色几何测试、改 2 个过时断言)、
5 契约全 PASS、i18n --check PASS、fmt 干净。
* fix(linux-egui): 纯图标按钮的图标居中
历史详情右上角的「…」菜单按钮传的是空 label,原来会按「图标 + 间距」的一半
偏移 3px,看着不居中。
* feat(linux-egui): 对齐历史、词典、风格与市场的第一批交互
* fix(ci): 用 basename 判定 lib.rs,兼容 Windows 路径分隔符
`tauri-linux-free-contract.test.mjs` 之前用 `file.endsWith('/lib.rs')`
判断「Linux 守卫文件」,在 Windows runner 上路径分隔符是 `\`,于是
`openless-linux-egui/src/lib.rs` 的 `cfg(target_os = "linux")` 守卫块被判成
违规,job 直接失败。改用 `path.basename`,并把 i18n 目录的跳过条件换成能同时
匹配 `/` 与 `\` 的正则。
本地 `node scripts/tauri-linux-free-contract.test.mjs` 通过(122 个 Rust 文件、
214 个 React 文件);远端 Windows CI 需在推送后确认。
* feat(core): 速记采用独立历史语义(永久录音、同一 id 更新、免于普通清理)
对齐上游 Beta.2 的 QuickNote:速记不是「history 页改个名」,它有自己的
持久化语义。
- `HistorySource::QuickNote` + `DictationOutputTarget::QuickNote`:录音开始时
先落一条草稿(`error_code = "recording"`),完成时用**同一个历史 id** 覆盖它,
取消时把草稿标记为 `cancelled`,而不是留下半条记录或整条消失。
- 速记不插入文本(`insert_text = false`)且音频永久归档。
- 历史清理策略区分两类记录:普通记录仍受 `retention_days` 与 `max_entries`
限制,速记只受 200 条硬上限约束;`clear()` 也只清普通记录,不再连速记一起删。
- `dictation_output_target()` 让宿主能查询当前会话的输出目标(按钮/快捷键
切换录音时不必猜)。
- 偏好里新增可选的速记快捷键,并接入既有的冲突消解顺序(听写 > 翻译 > 选区
助手 > 速记 > …)。
`cargo test --locked -p openless-core` 通过。
* feat(linux-egui): 速记页、快捷键、风格包图标与翻译页缩放修复
对齐上游 Beta.2 的界面与交互,并修掉用户报告的几处偏差。
速记(QuickNote)
- 新增侧栏页面:复用历史列表/详情,只筛 `HistorySource::QuickNote`;按钮与
快捷键共用同一条开关逻辑(未在速记中则开始永久录音,开始中则结束)。
- 速记页顶部是上游同款可收起的快捷键卡片(录制 / 停用 / × 收起),收起状态
持久化到 `linux-ui-state.json` 的 `quick_note_shortcut_hidden`(该写入改为
「合并单键」而不是覆盖整个文件,避免把语言偏好清掉)。
- 设置页「快捷键与选区」新增速记行;宿主的本地热键、fcitx5 插件(新增
`QuickNoteEvent` / `SetQuickNoteHotkeyRaw`)与去重都接上同一条通路;插件没
这个新方法时不报错(旧插件兼容)。
风格包图标
- 新增 `style_icon.rs`:本地把用户选的 SVG 栅格化成 128px PNG 再交给 Core。
先做 XML 结构校验(根元素、禁用 `script`/`foreignObject`/`image`/`animate*`、
禁止外部 `href`/`url(...)`/`@import`/`on*`),再限 256 KB 输入 / 64 KiB 输出。
- 卡片上的图标按钮(含铅笔角标、自定义时的 × 复位)走真实的
`set_style_pack_icon`;纹理解码按 `(pack id, icon path)` 缓存,不再每帧重解码。
翻译页
- 卡片头部的标题/说明与状态药丸在同一 `horizontal` 里:窄列下文字不换行会把
药丸顶出卡片,egui 的 `columns` 随即把整页宽度记为「内容宽度」,下一帧按更
大的宽度再排一次——这正是用户看到的缩放卡顿与「使用方法」被顶出可视区的
来源。给文字块加上按药丸宽度算出的 `set_max_width`,让说明换行。
- `ComboBox::width` 是最小文本宽度,最终按钮还要加图标与内边距,原来的
`available - 4` 会溢出卡片;改为预留 28px。
- 新增回归测试:大→中→小三种窗口下页面都不得超出视口,且最小窗口下内容必须
高于视口(说明「使用方法」靠滚动可达)。
纠正规则页
- 按上游合成单张卡片:「只看自动收集」改为真正的复选框(此前只是个不可点的
徽章,筛选根本用不了),与「一键清空」同行;规则为空时在同一张卡片内给出
占位说明。
选区助手
- 「保存历史」卡片的图标改为上游同款 40×40 圆角底 + 居中图标,标题/说明按
12/13px 与 5px 间距居中排布,开关尺寸对齐 40×24。
词典
- 筛选标签的选中胶囊只用一个动画值插值(原来在同一帧对同一个 id 写三次不同
目标,胶囊会抖)。
风格/市场
- 卡片角标配色与「衍生自」判定对齐上游(作者与来源作者相同时也算衍生);
市场卡片去掉未打开详情就出现的安装按钮,详情弹窗按 560px 固定宽度居中。
i18n:`src/i18n/*.ts` 与 `linux-egui/src/i18n.rs` 同步(速记导航、快捷键卡片、
图标提示等)。`node scripts/sync-egui-i18n.mjs --check` 无漂移。
* chore(release): 同步上游 Beta.2 的两条发版约定
按上游 Beta.2 的修法同步(上游 commit
|
||
|
|
06db7d2978 |
chore(release): integrate reviewed changes for Beta 3 build 20260925
Integrate Android CI acceleration, modifier-only dictation chords, Requesty, and remote-input generation guards over the verified Beta 2 baseline and macOS CI improvements. Fix review findings in signing configuration caching, APK validation, and modifier arbitration/recording. Keep Linux egui and its release workflows identical to Beta 2. Co-authored-by: HKLHaoBin <we3q@qq.com> Co-authored-by: leolulu <35873316+leolulu@users.noreply.github.com> Co-authored-by: Thibault Jaigu <thibault.jaigu@gmail.com> Co-authored-by: Ethan <257484394+EthanYoQ@users.noreply.github.com> |
||
|
|
16c3f841b5 |
chore(release): prepare 2.0.0-Beta.2 build 20260924
Integrate the reviewed 2.0 PRs and local fixes from 03136853. Preserve the verified subtitle behavior and model/runtime corrections. Keep the numeric Beta tag compatible with released clients while exposing the date as application build metadata. Co-authored-by: DepengWang <2818245+DepengWang@users.noreply.github.com> Co-authored-by: HKLHaoBin <we3q@qq.com> Co-authored-by: Nahida <1139500183@qq.com> Co-authored-by: TRIP <1933142963@qq.com> Co-authored-by: chyuwei <weichangyuwcy@gmail.com> Co-authored-by: leonoxo <leonoxo@gmail.com> Co-authored-by: tayir <arnoldincredible@126.com> |
||
|
|
04719aac37 | 方->圆 | ||
|
|
cb5438a9d4 | feat(docs): 更新 README 和 README.zh.md 中的贡献者信息 | ||
|
|
0ac0fbd391 |
merge: 汇入 origin/beta(#1037 远程输入持久 CA、#1040 F13-F20/Mac 听写键、#1020 腾讯云实时语音、#1044 统一模型字段与异步保存隔离)并完成 2.0 UI 适配
冲突解决原则:2.0 重写的界面以本地为准,云端功能语义全部移植—— - ProviderForm 离开收敛接入 SettingsModal / ChannelList / LlmProtocolFields / CredentialField(关闭、切节、换供应商前冲掉防抖写入并等待在途写完成) - 腾讯云三段式密钥 + 固定模型档分支移植进 2.0 ChannelCredentialFields;OAuth 静态模型走描述符 - Mac 听写键进入 ShortcutRecorder(allowMacDictationKey)并改用 refresh 拉新偏好 - 远程输入手机页新证书信任流程(持久本地 CA + 描述文件/CA 下载 + 复制链接),de/es/fr 补齐全套新文案 - 纯格式分歧文件取云端后 prettier 归一;双方新增内容(i18n、测试)两侧保留 |
||
|
|
bf1237c6b1 |
feat(provider): 增加腾讯云实时语音处理链路 (#1020)
* feat: add Tencent TokenHub and realtime ASR Rebase onto Core 2.0: Hunyuan realtime ASR and TokenHub polish now live in openless-core with the shared provider registry, credentials, and cloud transcription session lifecycle. * fix: harden Tencent ASR and TokenHub --------- Co-authored-by: Chris233 <h-chris233@outlook.com> |
||
|
|
0fa21daaf0 | Merge origin/beta into codex/ui-2.0-details(汇入 #1022/#1023/#1041/#1042,保留 2.0 渠道卡片结构并整合 OrcaRouter 目录选择) | ||
|
|
1d29f8a6cd |
feat(app): finalize 2.0 channel settings UI, 8-language alignment and cloud-sync groundwork
- 渠道设置界面:ChannelList/LlmProtocolFields/ProvidersSection 渠道级协议与模型配置 - 8 种界面语言 1711 项文案对齐(de/en/es/fr/ja/ko/zh-CN/zh-TW) - Core 协议层 llm_protocol 与 provider_service 配套、云同步准备 - linux-egui 后端与 Android updater 配套调整 |
||
|
|
66915405fd |
feat(providers): add OrcaRouter model catalog, LLM, and ASR support (#1022)
* feat(providers): add OrcaRouter model catalog integration * fix(providers): harden OrcaRouter catalog integration --------- Co-authored-by: Chris233 <h-chris233@outlook.com> |
||
|
|
a3fc7171d2 |
feat(llm): add OpenCode Zen polish provider, default deepseek-v4-flash (#1023)
* feat(llm): add OpenCode Zen polish provider, default deepseek-v4-flash Dictation polish uses OpenAI-compatible chat/completions. For OpenCode Go, change the Base URL to /zen/go/v1. * fix(macos): 避免 Fn 长按与功能键误触发听写 (#1008) Auto 和 Toggle 模式只在 Fn 短按松开且未叠加功能层操作时触发听写;长按及亮度、音量等 systemDefined 事件留给 macOS。 保留显式 Hold 模式的按住说话语义,并跳过 Fn 短按释放后的冗余仲裁延迟。 Co-authored-by: Chris233 <h-chris233@outlook.com> * fix(asr): 百炼 WebSocket 优先走 IPv4,避开本机 IPv6 黑洞 (#1036) * fix(asr): 百炼 WebSocket 优先走 IPv4,避开本机 IPv6 黑洞 dashscope 的 AAAA 在部分 Windows 双栈网上 TCP 443 会挂死,connect_async 先试 IPv6 会卡满 5 秒超时,听写进不了 Listening。 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(asr): preserve websocket ports and retry handshake addresses --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Chris233 <h-chris233@outlook.com> * fix(updater): Beta 构建默认跟随 Beta 渠道 (#1041) * fix(updater): default beta builds to beta channel (#1034) * fix(updater): persist channel switches atomically * feat(llm): 支持渠道级 Chat Completions、Responses 和 Messages (#1042) * feat(llm): add channel request formats and compatibility presets * fix(llm): harden channel protocol handling * feat(llm): integrate OpenCode Zen with shared request formats --------- Co-authored-by: Mark <shawninjuly@gmail.com> Co-authored-by: Chris233 <h-chris233@outlook.com> Co-authored-by: 浩彬(^-^) <146098469+HKLHaoBin@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
69f454bd68 |
feat(ui): integrate 2.0 interface and agent work
Consolidate the current 2.0 sidebar, overview, settings, vocabulary editing, correction rules page, and first-launch splash with their Core and Tauri adapters. Include the accompanying source and architecture documentation updates. Validation: npm test (build and 73 discovered test files); cargo test -p openless-core --locked; Core/Linux and Tauri rustfmt checks. |
||
|
|
92da4fbf9b |
docs: 清理已了结的规划过程文档并同步引用
删除已完成/已收口任务的规划与过程记录(2.0 UX 收口、Core 2.0 计划、 PR#1019 复核与桌面复审、平台边界审计、Linux 引导知识快照、 Linux 共享后端阶段计划、供应商渠道计划、Windows sherpa 规划、 PR#711 评审截图、应用内测试计划),保留规范性文档、验收清单、 Linux 交接目录与运营指南,并修复全部引用链接。 |
||
|
|
fc38edd1a3 |
refactor: consolidate Core 2.0 and restore cross-platform behavior (#1019)
* refactor: split Linux egui host from shared backend * fix: keep CI contracts portable across runners * fix(ci): preserve CRLF while removing qwen dependency * fix(ci): regenerate target lockfile and install ripgrep * fix(ci): gate desktop-only selection voice module * ci: limit native build parallelism on cross-platform runners * fix(host): expose blocking task seam to coordinator * fix(settings): avoid macOS default hotkey collision * docs: record cross-platform CI evidence * ci: expose Linux validation package artifact * ci: grant reusable Linux package permissions * refactor: remove legacy coordinator dictation facade * ci: skip unsupported MSI prerelease builds * ci: install minisign only for signed Linux releases * fix: keep test-only coordinator session helper * refactor: move style preview diagnostics into core * fix: import shared preferences type from core * refactor: centralize enabled vocabulary projection * ci: install ripgrep for Linux package gates * test: make source contract line-ending agnostic * fix: keep Linux desktop entry standards compliant * docs: expose core style diagnostics contract * ci: resolve Linux package version in app directory * refactor: centralize ASR vocabulary priority * fix: use shared vocabulary note constant * refactor: keep vocabulary priority inside core * test: retain starting-stop queue coverage * ci: scope Linux updater URL to the current repository * docs: close implementation and artifact evidence * docs: record final CI artifact evidence * fix(android): align updater plugin versions * fix(android): pin updater crate exactly * refactor(core): remove unused type anchor * docs: record final artifact runs * fix(remote-input): preserve beta status fields * fix(tauri): finish beta host merge * fix(asr): pass scoped operation to native qwen adapter * fix(macos): bundle mlx metallib as a resource * test(remote-input): update status wire contract * feat: share Less Computer voice flow with Linux * refactor(core): centralize host-independent runtime policy * refactor(core): complete 2.0 backend consolidation * fix(ci): prepare Sherpa after cache restore * fix(macos): route native model paths through Core store * fix(macos): use ModelStore catalog API * fix(ci): serialize macOS native test build * test(ci): preserve macOS cargo diagnostics * fix(macos): release input source lock before await * test(core): replace legacy hotkey injection gate * test(core): move Codex sandbox contract into Core * fix(core): close final runtime policy gaps * fix(windows): terminate coding agent process trees * fix(core): complete local model migration * fix(credentials): align Core LLM model account * fix(core): restore 1.x parity across shared backend * fix(android): complete shared backend stubs * fix: close cross-platform migration review gaps * fix: complete migration lifecycles and platform compatibility * docs: define desktop 2.0 acceptance and Linux handoff * fix: close desktop parity and session lifecycle gaps * fix: finish voice lifecycle and desktop feedback parity * fix: close desktop cancellation and feedback race gaps * fix: honor proxy settings and reject stale session effects * fix: dispatch native cleanup on host runtimes and order events * fix: keep optional recording effects best effort * fix: close desktop agent model and QA lifecycle gaps * fix: preserve desktop raw voice and QA turn ownership * fix: finalize desktop dictation and QA feedback |
||
|
|
1933a283c6 | chore(release): prepare 1.3.18-Beta.5 | ||
|
|
42c1b31754 |
本地模型增加whisper相关模型的支持; 增加qwen3-asr的rust版本, 相比C版本速度更快 (#940)
* feat: add selectable local ASR backends * fix(build): 修复本地 ASR 多平台构建与旧渠道兼容 * fix(asr): 修复 MLX 权重跨线程 stream 错误 * fix(ci): exclude MLX Qwen3 from non-macOS jobs * fix(ci): use app-relative ASR submodule path * fix(android): skip qwen-asr C backend * fix(android): guard qwen-asr by target triple * fix(ci): 修正语法错误 * fix(ci): remove invalid checkout step options * fix(asr): harden local backend integration * fix(asr): 处理 PR #940 审查发现(9 项) - strip: 全局保留 symbols,macOS release profile 单独退回 debuginfo(Homebrew rustc 的 LINKEDIT 问题仅限 macOS) - 转录超时改为驱逐引擎而非无效 cancel():spawn_blocking 解码任务无法被中止,驱逐后下次会话加载新引擎而非排队 - Turbo/Q5 共享目录模型在 is_downloaded/downloaded_bytes/delete_model 上互相认账 - MLX 临时 WAV 用 RAII guard 兜底清理;HF tree API 改 Link rel=next 游标分页(offset 被服务器静默忽略),带防御上限与 5 个单测 - Intel Mac 渠道下拉不再闪现 MLX 预设;MetalToolchainGuide 默认折叠并标注仅源码构建期要求 - qwen3-asr-rs 子模块 URL 与 gitlink 指向 Open-Less org 镜像,嵌套 mlx-c 同步改指 org(供应链 N-6) - README/注释补充:所有平台源码构建均需递归初始化子模块(path 依赖跨 target 解析) * fix: harden local whisper model lifecycle and downloads * fix(asr): address PR 940 review findings * fix(ci): add local ASR model stub to backend tests * fix(linux): bundle and sync fcitx5 selection plugin * fix(macos): avoid duplicate whisper test module * fix(history): make detail sibling keys unique * fix(ci): stub macos whisper model constant * test(ci): avoid macOS streaming timeout race --------- Co-authored-by: Chris233 <h-chris233@outlook.com> |
||
|
|
7a66ca625b | docs(readme): 移除对 jiangmuran 的赞助商感谢信息 | ||
|
|
b4099362d2 | docs(readme): 更新 bigsong 的角色为“贡献者/测试” | ||
|
|
9bf492e04d | docs(readme): 更新开发者信息和链接格式 | ||
|
|
6f4ed5b128 | 更新 README.md | ||
|
|
9cc510e86b | 更新 README.md | ||
|
|
09828b7ea6 | docs(readme): restore jiangmuran and Cooper (#971) | ||
|
|
4cd93bd006 |
fix(deps): resolve memmap2 and rkyv advisories for #815 (#938)
* fix(deps): resolve RustSec advisories for issue 815 * ci(rust): verify Rust 1.88 MSRV * test(linux): stub fcitx in backend harness * docs: document Rust 1.88 MSRV in Chinese README |
||
|
|
f016e57952 |
feat: 让词典自己长——手改一次,它就记住(光标上下文 + 手改学词 + 热词预算优先级) (#917)
* feat(macos): read cursor context from the host app (module only, not wired up) Adds `host_document/` — the one place that touches "the document the user is currently writing in". Nothing calls it yet from the product path; the only consumer is a debug command. Wiring it into LLM polish is the next step, and that is where the (default-off) user-facing switch will live. Why: ASR only gets hotwords and polish only gets the QA selection, so the doc the user is writing is invisible to both. Chinese homophones (接口/借口, 大鱼/大禹) are indistinguishable acoustically but trivial in context. - window.rs: 80/20 cursor window, pure and char-based. Unused budget on one side flows to the other, so a cursor at the top of a document still gets a full window. Slicing by bytes would split CJK chars. - mod.rs: safety gate. Secure Event Input, AXSecureTextField role/subrole, and a hardcoded bundle-prefix blocklist (password managers, keychain, terminals). Gate inputs are a plain struct so the decision is unit-tested without AX — getting this wrong means shipping a password to an LLM. - macos.rs: AX read. Sets AXUIElementSetMessagingTimeout(200ms) — the existing AX code in selection.rs and lib.rs sets none and inherits the ~6s default, which means a 6s freeze against a hung app. Runs under spawn_blocking with an outer tokio timeout, never on a tokio worker. Large documents go through AXStringForRange instead of copying the whole AXValue across processes. - AX indices are UTF-16 code units while the window algorithm is char-based; the conversion is explicit and tested against surrogate pairs. Also folds coordinator::capsule_focus's near-verbatim copy of the frontmost-app lookup into selection.rs, which now exposes structured `current_front_app_parts() -> (name, bundle_id)`. The old display-string-only form was unusable for the bundle blocklist. Non-macOS returns Unsupported: Windows has no UIAutomation code and TSF is only live at commit time; Linux fcitx5 SurroundingText is unsupported by most clients. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 02aa66ac3d303df25334f05e50e08339036539e9) * feat(polish): feed the cursor's surrounding text to LLM polish (default off) Wires host_document into the polish path behind `cursorContextEnabled`, which defaults to false and must stay that way: turning it on means every dictation reads the foreground app's text and ships a slice of it to the user's LLM provider. That is data the user never handed us, so it is theirs to opt into. - `<cursor_context>` envelope through the existing `sanitize_for_xml_envelope`, injected in `compose_polish_prompts` — the one funnel both the OpenAI-compatible and Gemini paths go through. `assemble_polish_system_prompt` takes the same argument so the settings preview cannot drift from what actually gets sent. - The envelope carries a cursor marker, because "context" without knowing where the cursor sits doesn't distinguish finished text from the stub the user is mid-way through — and those two are worth very different amounts. Forged markers in the document are stripped before the real one goes in. - The injection-defense clause for `<cursor_context>` is a SEPARATE string appended only when context is actually present. Folding it into `polish_injection_defense()` would have changed the prompt for every user who has this switched off; off must mean the feature does not exist, down to the byte. There is a test for exactly that. - `sanitize_for_xml_envelope` now also neutralizes `< /tag>` (whitespace before the slash). It already handled `</ tag >` and `< tag >`; the gap let a forged boundary through. Not valid XML, but an LLM may not agree, and this envelope carries text from other people's documents. Read point is `end_session`, next to where front_app is read: focus is still on the target app there (the capsule is a non-activating panel) and polish is about to fire. Switch off means host_document is never called — not one AX message. Any read failure degrades to no context; never to a lost word. The toggle lives under Privacy, not under polish settings: its real cost is not tokens, it is that text from another app leaves the machine. macOS only — a switch that cannot change the outcome is worse than no switch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit f787a314075f2109b168dd3d66df06791667c7a8) * feat(macos): detect when the user hand-corrects text we just inserted Perception only — this logs the edit and produces no rules. Getting the sensing right is the whole job here; letting it touch the user's dictionary is the next step. - `diff.rs`: minimal-edit extraction, pure and char-based. Longest common prefix, then longest common suffix on what's left; the middle is what the user actually touched. Whole-text diffs teach nothing — "大禹 → 大鱼" can become a rule, "this paragraph → that paragraph" cannot. Six rejection rules, each guarding against a class of false positive that would silently corrupt every future dictation. Pure insertion is rejected (a rule that inserts unconditionally anywhere is the worst of them); pure deletion is kept, since it is specific and cannot fire everywhere. - `edit_is_within_typed_text`: the observer watches the whole control, so a user editing their own earlier text in the same field also fires it. That edit has nothing to do with this dictation. This predicate is the only line between "learn our own mistakes" and "learn whatever moves", so it is a pure function with tests rather than an inline `contains`. - AXObserver on a dedicated thread, shape copied from `device_watch.rs` (thread → register → `run_in_mode(1s)` + exit flag → unregister → warn on failure), including why it does not use `CFRunLoopRun()` with a cross-thread stop. Uses `core_foundation`'s runloop wrapper rather than re-declaring `CFRunLoopGetCurrent`/`AddSource`, which `hotkey.rs` already declares — duplicate externs only work by luck. - Teardown has four independent guarantees, because a leaked observer means holding another app's AX reference and waking on its every keystroke: `EditWatcher` disarms on drop, the next dictation drops it, a 60s cap, and the thread kills itself when the front app changes. Baseline is `finalize_polished_text`'s return value — under streaming that is `typed_text`, what actually reached the screen, not the full LLM output. Using the full output would read every interrupted session as "the user deleted a large chunk". Adds `DictationSession.asr_transcript`: `raw_transcript` holds the text *after* local correction rules ran (dictation.rs rewrites `raw.text` in place). Telling "ASR misheard" from "the LLM broke it" needs the version from before that. Only written when the rules actually changed something. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 06f49faede8f4c47eae6be155feddb65da1b9c01) * feat: turn detected hand-corrections into dictionary entries Closes the loop: an edit the user makes by hand becomes a correction rule plus an ASR hotword. Those two do not substitute for each other — the rule guarantees this word is right next time, the hotword raises the odds we hear it right in the first place. **No pinyin dependency.** The plan called for one to power a "same reading → collect silently" tier, but the reference spec this was drawn from (§19.6) does not use phonetics at all — its six boundaries are purely textual, and the one place it does mention pinyin (IME cloud candidates) is explicitly listed as not worth copying. So: - Tier 1 (silent, tagged `learned`): cross-script only — one side pure CJK, the other pure ASCII letters (扣德克斯 → Codex). Nobody swaps a Chinese word for an English one to change their tone; that is us mishearing a loanword. - Tier 2 (confirm): everything else, including Chinese homophones. 大禹→大鱼 and 明天→后天 are textually identical in shape, so the text alone cannot separate a correction from a change of mind. Ask. Two problems found while building this, both fixed: - The minimal diff of a Chinese homophone correction is usually a SINGLE character (大禹→大鱼 strips to 禹→鱼), and a one-char rule fires everywhere — 禹州 would become 鱼州. So a rule widens outward through the stored context until it is at least 2 chars, yielding 大禹→大鱼, which is what the user meant. Tier is decided before widening, or the CJK context glued onto an ASCII pattern would hide every cross-script case. - Pure deletions never become rules. As a global replace, "delete 的" means every 的 in every future dictation disappears. The risk is nowhere near the reward. Detection still logs them. Suggestions queue in memory rather than firing a toast: at that moment the user is typing in another app, and stealing focus is the rudest thing we could do. They surface in the vocabulary page, which is also where `learned` rules carry a badge, a filter, and a bulk delete — being able to see and undo what was collected is the precondition for collecting anything at all. Dedup is by pattern and includes manual rules, so auto-collection can never duplicate or relabel something the user wrote themselves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit a603d0f8080a258449cffac20bb94e52b1fe0a3d) * feat(settings): add a cursor-context probe to the debug tools Milestone 1 shipped `debug_read_cursor_context` with no way to reach it. A Tauri command with no caller is not a debug tool — the whole point was being able to see, per app, what we actually read. Only noticed after installing the build and looking for it. Settings → Advanced → Debug tools now has a probe button. The 5s countdown is the part that makes it usable: at the moment you click, the frontmost app is OpenLess itself, so a probe with no delay only ever reads our own settings window. The countdown gives you time to switch to Notes / VS Code / WeChat and click into a text field. Shows status, reason, elapsed ms, the front app, and the windowed text with the cursor marked — enough to tell "the safety gate blocked this" from "AX doesn't support this control", which is exactly the question the verification gate needs answered per app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 853f01ac145d248ad6da26ac651caa384ebeed35) * docs: add the cursor-context install test plan Written for a dogfooding pass: what to test, in what order, and what the answer should look like. The AX-coverage table at the top is the one that matters — which apps we can actually read is the biggest unknown in this feature, and the probe makes it answerable without dictating a word. Records what I verified on the installed build (Notes reads at 11ms with the cursor marker in the right place) so you are not re-running what is already settled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 17083a476fbdd20d3d71c33b97861b5aedd25222) * fix(macos): anchor the edit-watch baseline after the insertion lands Two real bugs, both found only by installing the build and using it. **The baseline was read before our own text arrived.** `inserter.insert()` returning means the Cmd+V event was posted, not that the target app has put the text in the document — that takes tens to hundreds of ms. We read the baseline in that gap, so it captured the document as it was *before* the insertion. The first comparison then saw our own 25 inserted characters as the difference, called it a pure insertion, and dropped it. The word the user actually corrected was buried under that and never got looked at. Log from the failing run: `cursor context read OK: 44 chars` at 19:18:43, `edit watch armed` at 19:18:52, first notification `baseline=44, current=69`. The baseline now anchors when the insertion is observed to have landed — either our text shows up in the document, or 1.5s passes (apps that reshape what we typed, via smart quotes or autocorrect, would otherwise never match and the watcher would wait forever, failing silently). **Submitting a chat box was learned as a correction.** Pressing Return in Claude Desktop clears the input and shows a placeholder, which is structurally a "replace this whole sentence with that one" edit. `MAX_EDIT_CHARS` did not catch it — the sentence was 25 characters. It was suggested as a rule, so saying that sentence again would have replaced it with "Type / for commands". Rejected now by sentence boundary: an edit whose source or target contains a newline or CJK punctuation or `?!;` is not someone fixing a word. Tried a proportion guard first (edit vs document size) and threw it away — real false positives and legitimate corrections overlap on that axis, and it killed several valid cases. The boundary check deliberately ignores the ASCII period: Node.js, co.uk and v1.2 all contain one, and technical names are exactly what this feature exists to learn. Also adds the diagnostics that made this findable: a notification count on disarm (0 vs >0 separates "the observer never fired" from "it fired and something downstream ate it"), and a log line on every early return in the callback. The first debugging round produced nothing because that whole path was dark — and because the logs were at `debug` level, which this app does not record. Registers `AXSelectedTextChanged` alongside `AXValueChanged`. Not every text control emits the latter, and a user fixing a word always moves the caret, so it is a second evidence path for the same event. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 80e1691467a319331efd419a876507c9522ccee8) * fix(macos): only judge an edit once the user has stopped typing There was no notion of "the user is done" at all — every notification was judged immediately. That survives a one-character fix by luck, and falls apart on anything longer. Changing 扣德克斯 to Codex is delete-four-chars, then C, o, d, e, x. Six notifications, and the intermediate states — 扣德克斯→C, →Co, →Cod — are each a structurally valid CROSS-SCRIPT edit, which is the tier that gets collected silently without asking. One correction would have dropped four pieces of garbage into the user's dictionary. The dedup set does not help: it stops the same pair repeating, not a sequence of different wrong pairs. The callback now only records that something changed and when. The watcher thread checks once per turn of its runloop and judges only after 1.2s of quiet, plus once more before disarming (a user who corrects a word and immediately switches app would otherwise lose it). 1.2s is well above the gap between keystrokes while typing and well below how long it takes to move on to the next thing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 5da1e06ec80d32a0853739ecc953ffe506009f8e) * fix(macos): end an edit when the caret leaves it, not when a timer expires The previous commit picked "1.2s of quiet" as the definition of "the user is done." That is a guess, and it is wrong at both ends: pause to think mid-edit and it cuts you off; carry straight on to the next sentence and it judges while you are already somewhere else. We were already receiving the signal that answers this properly. Both notifications were registered but treated as interchangeable: typing → text changed AND caret moved clicking away → text unchanged, caret moved "Caret moved but the text did not" means the user left this spot — that is when the edit is final. Semantic, not a timer, and it costs nothing new: the `AXSelectedTextChanged` subscription was already there, we just never compared the text to see which kind of notification it was. So: record throughout, analyse only at a boundary. Boundaries are caret-moved, app switched, watch ended. The timer stays purely as a backstop for apps that do not emit caret events, and relaxes 1.2s → 5s — no longer the main judge, and being slower makes it *less* likely to catch a half-finished edit. The baseline advances only after a successful report. A rejected comparison has no verdict yet: delete a word, go copy something from elsewhere, come back and type the replacement — keeping the old baseline is what lets the whole edit be computed once you finish, instead of learning the deletion halfway. Design credit: the user asked what actually defines "done typing" instead of accepting the debounce, which is the question all of this hangs on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 4d9c96338db9143b4b26268a27a7a4c554b8f5db) * fix(macos): don't mistake typing's own caret event for the user leaving Real-machine log: every single keystroke logged "caret moved away; settling". The user corrected a word and nothing was learned. The two notifications arrive as a PAIR from one keystroke — AXValueChanged, then AXSelectedTextChanged milliseconds later. The callback updated last_text on the first, so the second saw "text unchanged, caret moved" and read it as a boundary. So every key press settled, every intermediate state was rejected (they are pure insertions), and each rejection consumed the pending edit. By the time the user finished typing there was nothing left to judge. The callback had the notification type in its parameters and ignored it. Now it uses it: AXValueChanged is always an edit; AXSelectedTextChanged only ends an edit if at least 300ms have passed since the last text change. Paired notifications are milliseconds apart, so they fall below the threshold; a genuine "stop typing, click elsewhere" is far above it. Erring toward missing a boundary rather than inventing one: a missed boundary is caught by the 5s backstop, an invented one learns half a word. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 347391d0cef20d780d9d50617ba5b4f214944a05) * refactor: learn vocabulary entries, not correction rules Writing both was wrong, and the machine proved it. The dictionary held the hotword `Codex` — "I want this word" — while the learned rule said `Codex → 扣的爱思` — "replace this word". Same word, opposite meaning. Delete one and the other stays, so the behaviour was undefined. That is the overlap the user spotted. Learned knowledge does not deserve literal-replacement power: - A correction rule fires on sight. When it is wrong it is silent, global and invisible. Real logs from this session: `小鱼 → x`, `都去 → h` — half-typed intermediate states that would corrupt every future 小鱼. - A vocabulary entry is a hint. It goes to ASR to improve recognition, and into the polish prompt where the LLM decides WITH CONTEXT whether to apply it. Wrong, it merely fails to help. The dictionary already carried the correcting power through the LLM — the hotword block says "when the transcript contains a homophone of these, prefer this spelling". That path has judgement; literal replacement does not. And now it has the cursor context too. The cost, stated plainly: no deterministic correction in Raw mode, and ASR may still mishear. Acceptable — misfiring is silent, not-helping is visible. Tiering is rebuilt around the new question. It used to ask "is this replacement safe", so it inspected the source→target mapping and its direction. It now asks "is this WORD worth remembering", so it only looks at the target. Direction stops mattering, which is what dissolves the loop: whether you changed Chinese to English or the reverse, what gets remembered is the word you ended up with. - Latin-script word (Codex, Node.js, GPT-5) → collected silently. Changing a word to an English spelling is itself the evidence that it is a proper noun. - Anything else (mostly Han words) → ask. 大鱼 could be a company or could be literally "big fish"; 接口 is a term and also an extremely common word. A common word in the hotword list makes recognition over-eager for it. - Not a word at all (empty, crosses a sentence, over 12 chars) → dropped. `RuleSource` stays on CorrectionRule: nothing writes `learned` any more, but early builds already wrote some into users' files and the UI must be able to show and remove them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 5a0135c3209abac4929dfa1ddc120571476c3ecd) * feat: ask about a new word on a card, where the capsule sits The queue in the settings page was the wrong place. A suggestion matters at the moment you just corrected the word — that is when you still remember why. Buried in a settings page you never think to look, and once it fills up it starts dropping the oldest, so it was accumulating nothing. The card appears where the capsule does. That window is already a nonactivating panel, so it cannot steal the caret while you type in another app, and it is a position you are already used to watching. Reuses the capsule window rather than opening another one: multi-monitor placement, Space attachment (including the macOS 26 bug that pinned the window to a single desktop) and the nonactivating panel setup were all arrived at the hard way, and a second window would have to earn them again. One thing had to change. The capsule is normally fully click-through — it floats over other apps and must not block what is underneath. A card you can click needs that off, and a transparent window that is not click-through blocks the mouse across its TRANSPARENT area too. So while the card is up the window shrinks to the card's own size, and the blocked region is only the card itself; both are restored when it goes away. The card takes an independent event channel rather than a new CapsuleState. `emit_capsule` is the single exit for session state and carries Esc exclusivity, Space re-assertion, monitor placement and the Linux fcitx text with it — a non-session state does not belong in there. Per the design decision: appears as soon as the caret leaves the edit, gone after 10s, several edits merge into one card, and nothing is recorded when dismissed. No rejection list — an invisible list would only leave the user wondering later why a word stopped being learned. Say it again and it asks again. Vocabulary page gets the divider layout: your own entries above, auto-collected below the line with its own count and a Remove all. No per-chip badge — mixed into one wall of chips you would have to read every one; a section you take in at a glance, and "remove all" naturally governs the block beneath it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 5dfad460f5ccc23be8ea9a30382fd1f9db2946fa) * fix(macos): give the capsule window back after the card closes Two ways the card left the capsule broken, both reported as "the hotkey stopped working" — the hotkey was fine, there was just no visible feedback. `begin_session_as` calls `hide_vocab_suggestion_card` on every dictation. With no guard it went on to `window.hide()` unconditionally, racing `emit_capsule`'s show over the same window. Now it returns immediately unless a card is actually up. And hiding restored click-through but not the size. The card shrinks the window to its own 300×108 (a transparent window that is not click-through blocks the mouse across its transparent area too, so it has to be small), and without restoring it the next capsule was squeezed into that — effectively invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 4d4170f935e21ad9d57043b5e0da10a4b70fcf93) * fix(daily): 把光标上下文系列接到新版 beta 上 daily 重建到 origin/beta 后,几个本地 commit 写作时上游还没有「划词润色」, 把它们放到新 beta 上就缺了三处对接: - selection_polish 调 polish_text 时少了新增的 cursor_context 参数。传 None: 选区润色的输入是用户选中的整段文字,本身就是完整上下文,光标前后文是给 「对着光标口述」用的,这里没有意义。 - 选区润色写历史时缺 asr_transcript 字段(本地新增,存「纠正规则生效前的识别 文本」)。选区润色不经过 ASR,恒为 None。 - polish.rs 里一个 beta 侧新增的单测调 compose_polish_prompts 时少了 cursor_context 参数。 仅对接,不改任何行为。cargo test --lib 990 通过,npm test 通过。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 69e21eebaa0a8de024a23e743a03af4b0d4b6fb0) * chore(macos): quiet the edit-watch diagnostics down to debug The per-event logging earned its keep — it is what located the baseline bug and the paired-notification bug, neither of which was findable by reading the code. But it fires on every keystroke: a day of normal use produced 2341 cursor-context lines in a 5.5MB log. Dropped to `debug` (this app records `info` and above, so they are silent in normal use): baseline anchored, caret moved away, settled-with-no-edit, unreadable AXValue, edit-outside-our-text. What stays at `info` is one armed/disarmed pair per dictation, and the disarm line now carries both counts — notifications received and edits reported. Two numbers are enough to tell whether an app emits usable notifications at all, which is the per-app coverage data this was supposed to collect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 6d84fa54275d49d21a7b3a6ea9176d3d55db3b6c) * docs: rewrite the test plan for what the feature actually became The old plan described correction rules and a Tier1/Tier2 split that no longer exist — the design changed twice while dogfooding it. Now covers what shipped: vocabulary entries only, the card at the capsule's position, and the divider layout. The rejection table names the two false positives that actually bit us on the machine — pressing Return in a chat box, and half-typed intermediate states — since those are the cases most worth re-testing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 7667ddd4fc4b58ee828feb8fa1fddcf580c55b3d) * feat(vocab): 每条建议都要你点勾才入库,卡片挪到右下角 真机跑两天的证据:自动收进词汇表的 5 条只有 1 条是对的。 Tailscale (was telskill) ✓ 唯一一条真纠错 ype (was ap) 逐字敲 Type 的中间态 ess (was ice) 同上 typeless (was TypeScript) 用户本来就要打这个词 " claude" (was cloud) 带前导空格,永远匹配不上 「跨文种的改动一定是专名」这条假设是错的。观察器看到的是编辑过程中的每一帧, 而中间态和一次真纠错在文本上没有任何区别——粗筛拦不住,也不该硬拦。 所以删掉 RuleTier 这一整个分级概念,只留一条路:提成建议 → 卡片 → 你点勾。 LearnedRule 不再带档位,handle_user_edit 也就没有第二条分支了。 顺带修掉那个前导空格:最小差异是按 char 剥前后缀剥出来的,边界上很容易挂着 一个空格,learned_rule 收尾补 trim。 卡片三处改动: - 位置从「屏幕正下方居中」(胶囊那儿)改到右下角。居中那块正是用户在写字的 地方,卡片停十秒直接盖住正在编辑的那一行。 - 「好 / 都不用」换成每条一勾一叉。尺寸、配色、SVG path 全部照搬胶囊上那对 确认/取消按钮——同一个产品里的同一个手势不该长成两个样子。没有批量入口: 逐条看一眼是这里唯一可靠的判据。 - 配色从写死的深色换成 --ol-capsule-* token(原来在亮色模式下是一块黑的), 描边从 0.5px solid 换成 1px + 扩散阴影。非整数边框落在半个物理像素里, 圆角边缘看着就是糊的。 逐条点完会重算窗口高度:显示卡片期间窗口不穿透鼠标,不重算的话已经空掉的 那片透明区域会继续拦住底下的点击。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 7c09724253cdac4ca62392564b67e52030546340) * fix(asr): 别让刚添加的词把常用词挤出 ASR 词表预算 发给 ASR 的词汇偏置 prompt 有 240 字符预算(whisper.rs::PROMPT_CHAR_BUDGET, 按 OpenAI 244 token 上限的 CJK 最坏情况折算),装不下的词条直接跳过。而词条是 按词典顺序填进去的,词典顺序又是「最近添加的在最前」(DictionaryStore::add 用 insert(0),为的是词汇表页面把刚加的词排在上面)。 两个各自都合理的决定撞在一起:预算永远优先喂给最新的词,最老的先掉出去——而最 老的那批恰恰是攒了最多命中的常用词。真机上一份 40 条的词典里,命中 18 次、10 次、 7 次的三个专有名词全部排在预算外,一次都没送到过 ASR。用户在词汇表里看得见它们、 以为在生效,实际上从来没生效。这也解释了为什么「加了热词还是识别不出来」——问题 根本不在识别,在于那个词压根没送出去。 改成按优先级排序再填预算: - 最近添加的前 5 条保底。刚加的词还没机会攒命中,纯按命中排会让它永远进不去, 而用户刚加它多半就是因为刚被它坑过。 - 其余按命中次数降序。命中是扫描最终文本累加的,与词条有没有进过 ASR 词表无关, 所以是个干净的信号。 - 同词异形(claude / Claude)只留命中多的那个写法,位置取最靠前那次。既省预算, 也免得两种写法一起进词表让模型无所适从。 只改 ASR 侧(听写与 QA 两处构造点)。LLM 热词块没有名额限制,仍走 enabled_phrases, 顺序不变。 另外给预算丢弃加了一行日志。之前是完全静默的,真机上排查花了很久;只在丢弃集合 变化时打,词典不变就不重复刷。 同一份真实词典(40 条)改动前后: 改动前 Typeless / hermes / win-shukong / 大曹 / NewAPI / songkey / PR 全部在预算外 改动后 以上全部进入,被挤掉的换成命中 0~1 的冷词 cargo test --lib 908 passed,含 3 个锁住排序规则的新测试。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 983b6d0cb892ff3667cd764c5eca1bab4f76d80e) * fix(vocab): 学来的词条追加到词典末尾,不占 ASR 预算的保底席位 两个功能合到一起才暴露出来的相互作用: - 学习路径走 add_if_absent,原本和手动添加一样 insert(0),插在词典最前面 - ASR 词表预算把「词典最前面的 FRESH_VOCAB_SEATS(5) 条」当保底席位 保底席位的理由是「用户刚手动加这个词,多半是刚被它坑过」。这对手动添加成立, 对着建议卡片点一下勾不成立——门槛低得多,而卡片本来就会建议出半截词(真机上 见过 ap → ype 这种逐字打字的中间态)。 连点五个勾,五个保底席位就全被它们占住,把用户攒了几十次命中的常用词挤出 240 字符预算——那正是 FRESH_VOCAB_SEATS 所在的那个提交要解决的问题本身。 改成追加到末尾。不需要给「学来的」加任何特判,位置本身就把两类区分开了。 排在队尾不等于永远进不了预算: - 词条进 LLM 热词块没有名额限制,那一侧立刻生效; - 命中计数扫的是最终文本,与有没有进过 ASR 词表无关,所以这个词一旦真的开始被 用上就会按命中自己爬进预算。 add_if_absent 只有学习路径这一个调用方,手动添加的 add() 不受影响。词汇表页面 按 note 字段分上下两组,与存储顺序无关,展示不变。 顺带给 DictionaryStore 补了 new_at 测试构造器(与 CorrectionRuleStore 同形), 新增的两个用例因此不会碰用户真实的 dictionary.json。 cargo test --lib 991 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: README 补上「会自己长的词典」,并兑现它已经承诺过的那句话 README 的词典章节里一直写着: The app auto-learns candidate corrections such as `Claude`, `ChatGPT`, and `OpenLess` from your history and offers them later. 应用会从你的历史中自动学习候选纠正,并在之后向你推荐。 这句话在 beta 上没有任何实现支撑——搜遍 persistence/dictionary.rs、coordinator 和 Vocab.tsx,没有任何「从历史学词」或「候选建议」的代码。它一直是一句空头支票。 这个分支把它兑现了,所以顺手把描述改成实际做出来的东西:不是「从历史里挖」, 而是「你改掉我刚打出来的词时,当场问你一句」。 三处改动: - 词典章节:换掉那句空话,写清真实的闭环(手改 → 卡片 → 点勾入库),并说明 每一条都要用户过目、没有静默入库。新增「光标上下文」小节交代它是这一切的 前提:只有看得见自己刚打出去的文字,才可能发现用户改了哪个词。同时写明默认 关闭、仅 macOS、以及永远不读的四类场景。 - 更新亮点:加一条。措辞落在「从转写工具变成认得你的词的输入法」这个转变上, 而不是罗列功能点。 - 对照表 Typeless 那一行:补上「学到的东西留在本机,且不会在你确认前加进去」。 这是同类闭源订阅产品给不了的那部分。 只改文档。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: 修 Android 编译,并把 AX 抓取挪出 tokio worker CI 与机器人审阅回来的三条,全部核实成立。 ## 1. Android cargo check 挂了(硬阻塞) `set_ignore_cursor_events` 在 Android 的 tauri::WebviewWindow 上不存在,卡片 显示/收起各调了一次。仓库里 capsule_focus.rs:645 对同一个 API 早就有处理, 还专门写了注释说明 Android 不可用——照它加 #[cfg(not(mobile))]。 Android 上根本没有胶囊窗口,get_webview_window("capsule") 会先返回 None, 所以只需要让这两句在 mobile 上消失,其余逻辑不必动。 ## 2. AX 调用跑在 tokio worker 上 spawn_edit_watcher 原本在调用线程上抓焦点元素、读基线全文、取 pid,注释写的 理由是「趁焦点还没跑」。但调用方 arm_edit_watch 位于 end_session(async)里, 也就是 tokio worker——而这几次 AX 调用每次都可能耗到 AX_MESSAGING_TIMEOUT_SECS, 对着一个 AX 无响应的 app(正是设这个超时要防的那种)能把一个 worker 卡住几百 毫秒。 **这条违反的是本模块自己文档里的硬约束第 2 条**,也是规划文档点名不许犯的。 改成在新线程里做(抽出 grab_focused_element)。代价是「趁焦点还没跑」的窗口从 零变成一次线程启动,几十微秒——比放进 spawn_blocking 好,那个要排 tokio 阻塞池 的队,负载高时反而更晚。 抓取失败时线程直接返回,观察器成为惰性对象;EditWatcher 的 Drop 只是置一个 flag,没有副作用。 ## 3. 大文档上每条通知都整份读 AXValue 观察窗口最长 60 秒,用户每敲一个键都可能来一条通知,每条都要整份读 AXValue 再做 O(n) 比对。文档大到一定程度,这就成了「用户改一个词,每次击键都跨进程 拷贝一份文档」——卡顿,甚至把 AX 消息拖超时。同一个 PR 里的 read_document 正是 为了避免这个才做了 AXStringForRange 分支,观察路径却没有对应的保护。 加 EDIT_WATCH_MAX_CHARS = 20_000(与 FULL_TEXT_MAX_UTF16 同量级):超过就不武装, 打一行 info 说明。学不到词可以接受,让用户打字变卡不行。 cargo test --lib 991 passed;cargo check(macOS)干净。Android 本地无法验证 (本机 cargo 由 Homebrew 安装,没有 rustup 加不了 target),交给 CI。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 关掉开关立刻解除观察器,并给「Drop 即停止」钉一个测试 机器人审阅报了一条 5/5 + 隐私标记的「watcher cannot be stopped」,说 spawn_edit_watcher 交出去的停止 flag 从来没人置位,drop Arc 停不掉线程。 **这条不成立。** 停止链路是完整的,只是横跨两个文件: watch_for_edits 把 Arc 包成 EditWatcher mod.rs:385 impl Drop for EditWatcher → disarm() → store(true) mod.rs:362 观察循环每次轮转检查 if stop.load(..) macos.rs:776 解除的调用点也不是显式 disarm(),而是 `*slot = None`(arm_edit_watch 和 begin_session_as 各一处),所以只读 macos.rs 会看不到。它由此推出的三个后果 (线程堆积、重复上报、关开关后继续观察)都建立在这个错误前提上。 补一个测试把这条链钉死——它横跨文件、已经被误读过一次,而且一旦断了症状是 静默的(观察器活到 60 秒硬超时才停,期间继续读、继续上报、还和新武装的并行)。 ## 但它指到的地方确实有一个真问题 归因错了,位置对了:**关掉开关时,已经武装的观察器不会被主动解除。** arm_edit_watch 只在 end_session 跑。用户在观察窗口内把「光标上下文」关掉, 上一次听写留下的观察器会一直活到 60 秒硬超时或前台 app 切换——也就是用户明确 关掉开关之后,我们还在读他正在写的文档,最长一分钟。 功能本身是否还有用不重要:开关关掉的那一刻就该停,这是这个功能敢默认存在的 全部前提。 set_settings 里加一个 true→false 的 diff 分支,调新增的 disarm_edit_watch(): 置空 edit_watcher(Drop 即解除),并把还挂着的建议卡片收掉——那些建议是这条 链路的产物,开关关了就不该再让用户看见。 cargo test --lib 992 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 没有光标时别当成光标在开头;词条长度两侧都要量 机器人审阅这轮的两条,都成立。安全标记已撤(No security concerns identified)。 ## 1. 负数 caret location 被钳成 0 copy_caret_offset 原本 `range.location.max(0)`。部分 app(尤其 Electron 那类) 在没有插入点、或元素不是文本控件时,AXSelectedTextRange 返回 kCFNotFound(-1)。 钳到 0 等于把「不知道光标在哪」当成「光标在开头」——于是读回文档**开头**那几百 个字,再当作「光标附近」发给 LLM。 这错得静默,而且很可能就是装机测试里那个一直没解释清楚的现象:Electron 类 app 反复出现 `before=0 after=N`,当时记成「这个 app 读不到上文」,实际是读错了地方。 **错的上下文比没有上下文更糟——它看起来是对的。** 改成负数返回 None。read_document 对 None 早有正确降级(走 Unavailable 分支, 这次不发上下文,探针里能看到原因)。抽出纯函数 caret_offset_from_location 以便 单测,并钉一个用例。 ## 2. is_vocab_worthy 只量了 target 长度 「把一长串不带标点的话改成 ok」能过关:minimal_edit 的 64 char 闸门放它过去 (没超),句读检查也拦不住(没标点)。于是它成为一条建议——「要记住 ok 这个词 吗」,卡片上那条 pattern 还长到显示不下。那是一次改写,不是一次纠错。 两侧都按 MAX_PHRASE_CHARS 量。一个词被听错,错的写法不会比它长太多。 (机器人说这会「pollute the vocabulary」,这半句不准确:入库的只有 target, 而 target 本来就有上限。真实代价是建议噪声和卡片显示,不是词库污染。) cargo test --lib 994 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 已解除的观察器不得再上报——迟到一条就能把胶囊弄没 机器人报的这条成立,而且后果比它描述的重。 原文说「stale suggestion card reappears during the new session」。实际不止多一张 卡片:**卡片会把胶囊窗口缩到卡片大小**(show_vocab_suggestion_card 里为了让卡片 可点、又不让透明区域挡住整块屏幕而做的取舍)。一条属于上一轮的建议在新会话进行 中弹出来,等于把正在进行的那次听写的胶囊弄没了——这个坑真机上踩过一次,当时的 表现是「热键像是坏了」。 ## 两处直接的口子 run_edit_watch_loop 里: - 每轮 run_in_mode(1s) 返回后无条件 settle_pending_edit(false)。stop 若在这 1 秒 内被置位,仍会先判定一次再退出。改成先看 stop 再判定。 - 收尾处无条件 settle_pending_edit(true)。这一刀本来是给「用户改完直接切走 app」 兜底的,但主动解除也会走到。改成只在非解除退出时才补——超时 / 切走 app / 焦点 元素消失这三种自然结束没有新会话在跑,那次改动确实还没判定过。 ## 但堵 flag 只能缩小窗口,关不死 AX 通知回调跑在 run_in_mode **内部**。stop 置位到循环察觉之间(≤1s),回调走 caret 路径照样能完成一次上报。判据放在线程那边就永远有这段重叠。 所以把判据挪到协调方:Inner 加 edit_watch_generation,每次 arm_edit_watch 和 disarm_edit_watch 都 +1,on_edit 闭包捕获自己武装时的代次,对不上就丢弃。 这样「已解除的观察器不得再上报」不再依赖线程什么时候看见 flag——迟到的上报到达 时代次已经变了,当场失效。 cargo test --lib 994 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 解除观察器收口成一个入口;扩长按 trim 后的长度判 机器人这轮两条,都成立。第一条正中我上一轮修复的漏洞。 ## 1. begin_session_as 没推进代次 上一轮加代次守卫时,arm_edit_watch 和 disarm_edit_watch 各自推了代次,唯独 begin_session_as 还是裸的 `*slot = None`——而它恰好是「新会话开始」这条主路径, 也就是上一轮那个 bug 的实际触发路径。等于守卫加了,但没守到该守的门上。 根因是解除动作散在三处、各写各的。收口成 coordinator::disarm_edit_watch(inner), 三条路径都走它: arm_edit_watch 武装前先解除上一次 begin_session_as 新会话开始 disarm_edit_watch 用户关掉开关 两步缺一不可(丢掉 EditWatcher 置停止 flag + 推进代次),现在只有一处实现, 漏一处这种事不会再发生。 ## 2. learned_rule 按 trim 前的长度判扩长 pad_to_min_length 用 edit.source 的原始 char 数当基准,trim 发生在扩长之后。 「大 禹」→「大鱼」的最小差异是 " 禹" → "鱼",带空格数是 2 char,正好够 MIN_PATTERN_CHARS 于是不扩长;trim 之后只剩单字「禹 → 鱼」——正是这个常量存在 的意义所要挡的那种(下次说「禹州」就成了「鱼州」)。 改成按 trim 后的长度算基准,与最终入库的口径一致。 cargo test --lib 995 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(vocab-card): 听写进行中一律不弹卡片——判据下沉到碰窗口前的最后一刻 机器人指出代次检查是 check-then-act:回调读一次代次,然后打日志、跑 handle_user_edit、最后 show_vocab_suggestion_card。这中间 begin_session_as 完全 可能已经推进代次并收起卡片,于是卡片在 hide 之后又被显示出来。成立。 但不再往代次上叠补丁。所有上游判据(观察器代次、pending 是否为空)都是「读一次 再去干活」,而 show 最终要 run_on_main_thread —— 读完到真正碰窗口之间隔着一次 跨线程调度,那一段无论加多少 check 都堵不住。 把判据下沉到主线程闭包里,问一个更朴素、也更真实的不变量:**听写不在 Idle 就不 弹卡片。** - 这是碰窗口之前的最后一个时点,没有更晚的地方可以被抢跑; - 它问的是真正要防的事:卡片和录音胶囊共用一个窗口,显示卡片要把窗口缩到卡片 大小,在听写进行中弹出来就是把那次听写的胶囊弄没了(真机踩过,表现是「热键 像是坏了」); - 它覆盖所有调用路径,不只是手改观察器那条——accept/reject 后的 refresh 也走它。 begin_session_as 是先置 phase 再收卡片的,所以只要它开了头,这道闸必然看得见。 vocab_card_visible 的 store(true) 一并挪到闸门之后,两条时序都自洽: - 卡片已显示 → hide 的 swap 拿到 true → 正常恢复窗口; - show 还在排队时 hide 先跑 → swap 拿到 false 直接返回、不排恢复;随后 show 的 闭包看到非 Idle 也退出 → 窗口从没被改过,无需恢复。 代次检查保留:它让上一代的上报连 pending 队列都进不去,是成本更低的早退。两者 分工写在注释里。 cargo test --lib 995 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(cursor-context): 写明 edit_is_within_typed_text 是按内容匹配,不按位置 机器人指出:判据是「这几个字在插入的文本里出现过」,不是「这处改动发生在插入的那 一段里」。同一个词在文档别处也有时,用户改那一处会被误算到我们头上。属实。 没有收紧成位置判定,是权衡后的选择,写进注释免得下一个人以为是疏漏: - 误算的代价可见且可撤销:每条建议都要用户在卡片上点勾才入库,最多多问一次。 - 收紧的代价不可见:位置判定要在锚定时记下插入偏移再比对,而目标 app 会加工插入 的文本(智能引号、自动补全、字形转换)——那正是 anchored 那套兜底存在的原因。 偏移对不上时会静默地不学,用户看不见自己少学了什么。 - 宁可多问一次,不可悄悄漏学。 真机上这种误算多不多,是装机自用才能回答的问题,真出现了再按数据收紧。 同一轮里另一条(asr_transcript / CorrectionRule.source 缺 serde default 会炸旧 文件)不成立:两个字段都已经带 #[serde(default)],RuleSource 那个还有 a_rule_without_a_source_field_deserializes_as_manual 钉着。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 手改观察器也必须过安全闸门——取焦点元素收口成一个入口 机器人报的安全问题成立,而且是这个功能最不能出的那种。 evaluate_gate 原本只装在读取路径(read_around_cursor_blocking)上。手改观察器 自己另开了一条取焦点元素的路(grab_focused_element),一次闸门都不过。后果: 在终端里听写 → 上下文读取被正确拦住(终端在 bundle 黑名单里) → 落字成功后观察器照样武装 → 读走终端全文当基线,之后每条通知再读一遍 → 手改的 source/target 以 info 级别进日志 → 还可能变成一张词条建议卡片 这个功能敢默认存在的全部前提就是「密码框 / Secure Input / 密码管理器 / 终端一律 不读」,而我在第二条路径上把这个承诺漏掉了。**闸门漏一条路径 = 没有闸门。** 没有在观察器里补一份闸门调用了事——那还是「两处各写各的」,下一个人加第三条 AX 路径照样会漏。把「取焦点元素」和「过闸门」焊死成一个入口 focused_element_passing_the_gate,两条路径都从它拿元素,想读宿主 app 的任何东西 都绕不过去。 顺序在入口里保持不变,两段判定不能合并: 1. 先判不需要 AX 的部分(Secure Input、bundle 黑名单)——命中就一条 AX 消息都不发; 2. 拿到元素后补 role/subrole 再判一次——密码框正是靠这个认出来的,而这两个属性 不拿到元素就读不到。 AXUIElementSetMessagingTimeout 也一并收进入口。 ## 顺带修掉同一轮报的性能问题 grab_focused_element 原本先整份拷 AXValue,再由 spawn_edit_watcher 判 EDIT_WATCH_MAX_CHARS。read_document 早就是先问 AXNumberOfCharacters 的,观察路径 没跟上——在一个十万字的文件上,光 marshalling 就够撞上超时,而那种文档我们本来就 不观察,白拷一次纯属浪费。 改成先问长度再决定拷不拷。拷完之后那道检查保留为兜底,给不报 AXNumberOfCharacters 的 app 用。 cargo test --lib 1000 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 改完按回车不该把整句都算成改动 真机翻车,用户当场看出来的:他只把「压根」改成「根本」两个字,改完顺手按了回车, 卡片上却弹出九个字的整句「压根就没有给我提醒 → 根本就没有给我提醒」。他的原话是 「我只改了一个词,这么长怎么要」。 根因在公共后缀的剥离方式:它从末尾往前逐字符比,末尾只要差一个字符,后缀长度立刻 判为 0,于是「改动点到结尾」的整段都成了差异。 基线 ...压根就没有给我提醒 末尾是「醒」 当前 ...根本就没有给我提醒\n 末尾是「\n」 ← 第一个字符就不匹配 两个字的改动被撑成九个字。而「改完按回车」是最常见的收尾动作,也就是说这条路径 上的建议长期都是坏的。 比对前对两侧 trim_end。尾部空白的差异本身没有词汇价值(多半就是一次回车),去掉 它既修好了后缀剥离,也顺带让「只按了个回车」在相等判定里直接出局。 新增两个用例:真机那句的最小差异必须是「压根 → 根本」;只按回车不算改动。 ## 残留面,写进注释了 这个算法只能表达一处连续差异(前缀 + 后缀两刀剥出中间)。用户同时做两处改动时, 两处之间的字都会被并进同一个 span。trim_end 只治好了「第二处是尾部空白」这一种 ——也是最常见的一种。换成尾部标点(改完词又补个句号)仍会撑开。根治要换成 LCS 之类能识别多处改动的算法,是另一件事;在那之前卡片上偶发的超长 pattern 就是这个 来源。 cargo test --lib 1002 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 学不到的改动不许吃掉基线——「删掉重打」这条路修好了 机器人报的 Split-Edit Miss 成立,而且它命中的是**最自然的那个纠错动作**: 删掉错词 → 停顿 → 敲正确的词。 原来的时序: 删词 settle(光标移开安静 300ms,或 5 秒兜底)→ 纯删除差异被上报 → 基线推进到「已删词」状态 敲词 相对新基线只剩一条「空 → 新词」的纯插入 → minimal_edit 对纯插入一律返回 None 于是只要用户在删词和敲词之间停顿一下(或移动过光标),这次纠正就永远学不进去。 代码和它自己的注释是矛盾的:注释写着「被过滤掉的不推进:用户可能删掉一个词…… 再回来把新词打完」,意图完全正确,但真正的过滤发生在**下游** handle_user_edit 里 (is_vocab_worthy 判 target 为空就丢弃),观察器看不到那个决定,照样把一次注定被 丢弃的改动当成「已结论」,顺手吃掉了基线。 让观察器用下游同一个判据:is_vocab_worthy 为假就既不上报也不动基线。用户把新词 敲完时,相对原基线算出来的正是完整的「错词 → 正确词」。 顺带少一类噪声日志——纯删除以前每次都会打一条 user edit detected,而它从来就 不可能变成词条(词条要求 target 非空)。真机日志里这种占了相当一部分。 cargo test --lib 1002 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 回调也要看停止 flag,不能只有循环看 机器人指出:解除信号到达时观察线程可能正卡在 CFRunLoop::run_in_mode 里(最长 1 秒),这一秒内排队的 AX 通知照样派发到 value_changed_shim,而循环末尾那道 `if !stop.load(..)` 覆盖不到回调这条路径。位置属实。 但它推的后果——「一条属于上一轮的建议会在新会话里弹出卡片」——不成立:协调方那边 已经有两道防线,而且都在 handle_user_edit 之前: - 观察器代次:回调闭包的**第一行**就比对代次,对不上直接 return,连 handle_user_edit 都进不去,更不会排进 pending_corrections; - 听写不在 Idle 就不弹卡片:判据下沉在 show_vocab_suggestion_card 的主线程闭包里, 是碰窗口前的最后一个时点。 不过「循环看 stop、回调不看」本身不自洽,而且不挡的话,一次已经作废的观察还会再 去跨进程读一遍宿主 app 的全文——那既是浪费,也是多读了一次本不该再读的东西。 WatchContext 加 stop 字段,回调开头先看它,**这一刀在读 AXValue 之前**。它不是唯一 防线,是最早、最便宜的一道。 cargo test --lib 1002 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): CF 的 Boolean 是 unsigned char,别用 Rust bool 接 CFStringGetCString / CFNumberGetValue 原本声明成返回 Rust `bool`。CoreFoundation 的 `Boolean` 是 `unsigned char`,不是 C 的 `_Bool`;而 Rust 的 `bool` 要求位模式 **恰好**是 0 或 1,其余一律 UB。拿它接一个 unsigned char,等于赌 CF 永远只返回 0/1——实践中确实如此,但这是赌,不是保证。 同一个文件里 AXValueGetValue 早就是 `u8` 了,这两个是当初照抄 selection.rs 抄进来 的。而本模块开头第三条硬约束写的就是「新代码不得复制既有缺陷,哪怕仓库里的旧 AX 代码就是这么写的」——当时说的是 AX 超时,这里我自己破了同一条例。 改成 `u8` + 与 0 比较,本文件三处 CF/AX 布尔返回值口径统一。 selection.rs 那份保持不动:不在本 PR 范围内,改它要重新验证选区润色整条链路, 风险与收益不匹配。它属于模块文档里点名的那类既有缺陷,值得单独开一个 issue。 cargo test --lib 1002 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 闸门改用元素自己的身份判定,不再信「谁在最前面」 机器人报的竞态成立,而且它指出的其实是一个更根本的设计错误。 bundle_id 由调用方在**取焦点元素之前**采样(current_front_app_parts),而每个 AX 调用都可能阻塞到 AX_MESSAGING_TIMEOUT_SECS。用户在这中间切了 app: 采样 bundle = 备忘录(放行) ...AX 调用阻塞中,用户切到终端... AXFocusedUIElement 返回的是终端的元素 闸门用「备忘录」的身份放行 → 读走终端的文本 终端、密码管理器正是靠 bundle 黑名单拦的,这条路径把它们漏了。 但把它只当竞态修就治标了。真正的问题是**判据本身选错了对象**:闸门要判的是「手里 这个元素属于哪个 app」,而前台 app 只是它的一个近似——两者本来就可能不一致,竞态 只是让这个近似失效得更明显。 改成拿元素自己的 pid 换 bundle(新增 selection::bundle_id_for_pid, NSRunningApplication.runningApplicationWithProcessIdentifier),在第二道闸门里覆盖 掉第一道用的那个前台 app 值。第一道保留——它不需要任何 AX 调用,命中就一条消息都 不发,作为省事的粗筛仍然有价值,只是不再是最终判据。 Secure Input 也顺手在第二道刷新一次:它是全局状态,同样可能在这几次 AX 调用期间 才被打开。 两条路径(上下文读取、手改观察)都从这一个入口拿元素,所以一起修好。 cargo test --lib 1002 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(cursor-context): 写明「改完词接着往下写」学不到,以及为什么这版不收紧 机器人指出:pending_since 每次文本变化都重置,用户改完词不停手继续写的话,判定 一直往后推;等他终于停下来,比对的是「原基线 vs 最终文本」——改的那个词和之后写 的所有内容被并成同一处差异。要么超长/跨句被拒(纠正白做),要么变成一条被污染的 建议。属实。 这跟用户今天自己抓到的「改完按回车撑成整句」是同一个根:minimal_edit 只能表达 一处连续差异,做两处改动时中间的字必然被卷进来。 **这版不收紧行为**,两个方向都会退化掉更重要的东西: - 把 pending_since 改成只在为 None 时设置(等于给窗口加 5 秒硬顶),会重新开始抓到 单个词改到一半的中间态——那正是 EDIT_SETTLE_TIMEOUT 当初从 1 秒放宽到 5 秒要躲开 的,而「什么时候算打完字」这个判定是花了好几轮真机才调对的; - 真正的解法是换成能识别多处改动的差异算法(LCS 之类),那是独立一件事,而且必须 有真机数据才能验证它没把已经调好的判定搞坏。本 PR 已经 20 个 commit,再塞一次 算法重写只会让它更难评审。 所以只把代价写进 EDIT_SETTLE_TIMEOUT 的文档:这条路径上的建议要么没有、要么偏长, 而每条建议都要用户点勾才入库——代价是漏学或多看一眼,不是静默写错。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 去重挡掉的改动也要推进基线,否则后面的纠正全被带偏 同一处 (source, target) 在一次观察窗口里出现两次是常事——听错的专名在好几句里都 出现,用户逐个改过去。第二次被 reported 去重挡掉时,函数直接 return,基线没推进。 于是基线停在「只改了第一处」的状态,而文档已经改了两处。之后用户再改任何东西, minimal_edit 都是拿这个陈旧基线去比,算出来的 span 把「已经有结论的那处重复改动」 和「新改动」搅在一起——多半过不了 edit_is_within_typed_text,新的那次纠正就被静默 丢掉了;万一过了,建议本身也是被污染的。 把基线推进挪到去重**之前**。去重管的是「别重复上报」,不是「这处改动没发生」。 判据统一成一句话:**有结论就推进,无论这个结论是不是新的。** 前面两道 return (不是我们插的文字、注定成不了词条)才是「还没有结论」,那两处保留基线是对的—— 后者正是上一轮修「删掉重打」时加的。 cargo test --lib 1002 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cursor-context): 确认不了元素归属就不读——闸门必须失败关闭 上一轮把闸门改成用元素自己的 pid 换 bundle,但只在成功时覆盖: if 取到 pid && 查到 bundle { gate.bundle_id = Some(owner); // 成功才覆盖 } // 失败时保持采样时那个前台 app 的值 → 照样放行 于是取不到归属时,闸门退回按「谁在最前面」判定——正是上一轮要修的那个漏洞原样 还在。这是 fail-open,比原来更糟:代码看着已经防住了。 改成失败关闭,返回 Unavailable。 注意**不能简单地把 bundle_id 清成 None**:evaluate_gate 对缺失的元数据是放行的 (missing_metadata_does_not_block_by_itself 钉着这个行为),那只是换了一种 fail-open。所以必须显式返回,不能靠清字段。 这跟那条测试的设计意图并不冲突——它的注释写着「读不到 bundle / role 时……读不到 文档自然会走 Unavailable」。本次改动正是把那个「自然会」在这条路径上变成显式的: 之前它在这里并不成立。 代价是没有 bundle id 的进程读不到上下文。那类进程本来就很少,而「宁可不读」是这个 功能对隐私的基本承诺。 cargo test --lib 1002 passed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(selection): 合并时把 beta 的按平台 current_front_app 又带了回来,Linux/Windows 重复定义 上一个合并 commit 解 selection.rs 冲突时,我看到 HEAD 侧是空的,就当成「beta 纯 新增」取了 theirs。判断错了:**空的 HEAD 侧也可能意味着「这一侧删掉了它」**。 本分支在里程碑 1 把 current_front_app 重构成了 current_front_app_parts 之上的通用 包装(无 cfg,一份顶所有平台);beta 没有这个重构,那边仍是按平台各写一份。git 看 到的是「我删了平台版本、beta 改了平台版本」,取 theirs 等于把被重构掉的两份又请了 回来: 1017 pub(crate) fn current_front_app() 无 cfg,所有平台 1066 #[cfg(windows)] fn current_front_app() ← 重新带回来的 1094 #[cfg(not(macos),not(windows))] fn ... ← 重新带回来的 macOS 上恰好不冲突(那两个 cfg 都不成立),所以本地全绿;Linux 上 1017 与 1094 撞 成 E0428,Windows 会以同样方式撞 1017 与 1066——CI 先报了 Linux,Windows 当时还在 排队。 删掉重新带回来的那两份,保留通用包装。beta 真正新增的 current_front_app_pid(选区 润色 macOS 移植 #926 要用)留下。 核对平台覆盖:current_front_app_parts 三个实现互斥且全覆盖;current_front_app_pid 是 macOS 专属,唯一调用点也在 macOS 分支内。 cargo check 干净、cargo test --lib 1032 passed(macOS)。Linux / Windows 交给 CI。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: address PR 917 review findings --------- Co-authored-by: jisongniu <bigsong038@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Chris233 <h-chris233@outlook.com> |
||
|
|
5f9586f739 |
feat(asr): 接入 ZenMux 语音转写(JSON + base64)作为可选云端 ASR(#837) (#915)
* feat(asr): 接入 ZenMux 语音转写(JSON + base64)作为可选云端 ASR(#837) - WhisperBatchASR 新增 ZenMuxJson 请求格式:application/json + base64 input_audio,language 跟随工作语言(ISO 639-1 映射),enable_itn 可配置 - 新增 zenmux 命名预设:默认 https://zenmux.ai/api/v1 + qwen/qwen3-asr-flash, 复用 Whisper 兼容路由与 30s 分片;模型 slug 自由输入 - 设置页新增 enable_itn 开关(per-provider 高级配置,默认开启) - 补齐五语言 i18n、前端单测与 README * fix(asr): ZenMux「测试连接」按 JSON 协议验证 + 词典不生效说明(#837) - validate_asr_transcription 按 provider 真实请求格式发探针:OpenRouter / ZenMux 走 JSON+base64,其余 whisper 兼容厂商走 multipart,避免假阴性 - 设置页对 zenmux 预设增加说明:JSON 协议不发送词典热词(prompt/hotwords), 词典仍进入润色链路但不参与识别偏置;README 同步标注例外 - coordinator 再导出 whisper_request_format 供验证路径复用 |
||
|
|
e46e94995c | docs(readme): remove jiangmuran and Cooper, sync latest stable data | ||
|
|
5be28b1dbd |
feat(asr): 支持讯飞实时语音转写(RTASR)提供商(#842) (#894)
- 新增 asr/xfyun.rs RTASR 流式客户端:signa = Base64(HmacSHA1(MD5(appid+ts), apiKey))
鉴权,1280B/40ms 分块发送,{"end": true} 收尾,seg_id 去重拼接,断连 partial 兜底,
鉴权/限流/超时错误分类可读。
- 全链路接入:dictation 主流程、QA 面板、重新转录、取消、设置页凭据预检与
"验证"探活;凭据 xfyun.app_id / xfyun.api_key 存系统凭据库,不落日志。
- 设置页新增「讯飞实时语音转写」选项(AppID + API Key),5 种语言 i18n。
- 文档:docs/xfyun-asr.md + README/README.zh/USAGE 同步。
已知限制:标准版 RTASR 无请求级热词参数(在讯飞控制台配置),语种默认中文普通话。
|
||
|
|
6b33ba21e0 | fix(updater): prepare 1.3.15-Beta.2 after repository transfer | ||
|
|
7de0c04ba5 |
docs: 用协调沉降定律重构 README 能力/增强叙事,凸显开屏即授权完基础设施 (#765)
- 新增「从工具到基础设施 / 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> |
||
|
|
0fe5fbfdcf |
feat(android): align release pipeline with desktop updater rules
Add signed release APK builds on v*-tauri tags, minisign manifests per ABI, custom Android in-app updater, and CI cargo check for aarch64-linux-android. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
1ea467a001 | feat(android): port Android APK runtime onto beta | ||
|
|
30cf33e2b1 |
Merge pull request #589 from Open-Less/docs/readme-real-example
docs(readme): 实际效果示例换成客户回邮件用例(中英双语) |
||
|
|
5cf015de17 |
docs(readme): 实际效果示例换成客户回邮件用例(中英双语)
把「一个实际例子 / A concrete example」从未充分测试的 SQL→ChatGPT 演示 换成经过测试的客户回邮件用例:口语输入 → 结构化清理稿,更贴合 OpenLess「只清理你的话、不替你作答」的定位,宣传与实际相符。 收束句由「回车发送」改为「粘贴给 ChatGPT/Claude 续写」,标语保持不变。 |
||
|
|
ace7fe47ab | docs(readme): 顶部加入 Discord 社区入口(badge + 社区行,中英双语) | ||
|
|
b0fe9ccb52 |
Revise README for improved clarity and structure
Updated README.md to enhance clarity and formatting. |
||
|
|
08ad6b7979 | docs(readme): 突出风格市场 + 流式输出,状态更新到 v1.3.6 | ||
|
|
4d9b63aab3 |
Route qwen-asr through the organization fork
Keep the submodule pinned to the reviewed commit while moving the fetch URL to Open-Less/qwen-asr, then document the review checklist future upgrades must follow.\n\nConstraint: #301 identified the personal upstream submodule URL as a supply-chain risk.\nRejected: Updating the submodule commit together with the URL change | would mix provenance hardening with unreviewed upstream code changes.\nConfidence: high\nScope-risk: narrow\nDirective: Do not point .gitmodules back to antirez/qwen-asr; sync upstream into the organization fork and review the diff first.\nTested: gh repo view Open-Less/qwen-asr; git submodule update --init --recursive openless-all/app/src-tauri/vendor/qwen-asr; git diff --check; npm run build; cargo check --manifest-path src-tauri/Cargo.toml\nNot-tested: macOS INSTALL=0 ./scripts/build-mac.sh C-link verification. |
||
|
|
b7685cadda |
docs(release): make macOS xattr workaround discoverable (#347)
发现的真因:v1.2.23 Stable 和 v1.2.24-1 Beta 的 macOS DMG 都是 ad-hoc 签名 + 未经 Apple 公证(仓库没配 APPLE_CERTIFICATE 等 secrets)。macOS 14+ Gatekeeper 默认拒绝运行未公证的 app,弹"OpenLess 已损坏"。 build-mac.sh:40-41 注释已经写明这是已知限制: "用户免手工 xattr 的根本方案是 Developer ID 签名 + Apple notarization" 之前 README 只在 brew install 段写了 xattr -cr 指令,DMG 直装路径没写。 用户首次装 Beta 不知道要做这一步,看到"已损坏"以为 release 坏了。 本 PR: - README.md / README.zh.md 的「下载与安装」段,DMG 直装路径加上 xattr -cr 指令 + 一句解释为什么需要(ad-hoc 签名、未公证) - release-tauri.yml 的 Create release step 加 body 模板,所有未来 release 自动 prepend 一段:macOS xattr 指令 + Beta/Stable 渠道说明。用 append_body: true 让它放在 generated notes 之前 不在本 PR:配 Apple Developer 证书 + secrets——那是根治方案,需要 $99/年 Apple Developer 账号 + 用户决定要不要走那条路。 Co-authored-by: baiqing <lbx12309@icloud.com> |
||
|
|
e331784b3b |
docs: add Cooper's circular avatar image
- Add assets/people/cooper.png (circular avatar) - Replace GitHub API link with local image file - Match TRIP and Chris233's avatar style |
||
|
|
620e817939 |
docs: make Cooper avatar circular
Add border-radius: 50% to Cooper's avatar to match TRIP and Chris233's circular style. |
||
|
|
dd96d64672 |
docs: add website URLs back to developer section
- Add website URLs below each developer's name - Increase cell width from 110 to 140 to accommodate URLs - Keep circular avatars and table layout |
||
|
|
712e9564f2 | docs: refine developer section | ||
|
|
fd79a051bd |
feat(updater): manual-download Beta opt-in in Settings → About (#332)
* feat(updater): manual-download Beta opt-in in Settings → About
接入 PR-B-2:客户端「加入 Beta 渠道」开关 + 文档完善。
为什么不走 auto-update 切 endpoint:
tauri-plugin-updater 2.10 的 Builder 不暴露 endpoints() 运行时 API,
endpoints 只能从 tauri.conf.json 编译期读,不能跑时切。继续用 plugin
自带 check 路径意味着 Beta 包会被推到所有用户,违背「Beta 不溢出正式
版」原则。fork plugin 或自实现 update flow ~500 行高风险,本轮采用
最稳路径:plugin 永远只读正式版 manifest(旧文件名),Beta 改成手动
下载——Settings 里展示最新 prerelease 的下载入口,用户点了跳浏览器。
Rust 端:
- types.rs:UpdateChannel enum (Stable | Beta) + UserPreferences::update_channel
字段;UserPreferencesWire / Default / Deserialize 全套兼容(旧 prefs 反序列化
默认 Stable,迁移无痛)。
- commands.rs:三个新命令——
- get_update_channel:读 prefs
- set_update_channel:写 prefs,复用 persist_settings + emit prefs:changed
- fetch_latest_beta_release:reqwest 调 GitHub Releases API,过滤
prerelease=true 且 tag 以 -beta-tauri 结尾的最新一条
- lib.rs:三个命令注册到 invoke_handler
前端:
- ipc.ts:UpdateChannel + LatestBetaRelease 类型 + 三个 invoke 包装
- SettingsModal.tsx:在 AboutMini 末尾加 BetaChannelControl 组件——
Toggle 切换渠道;切到 beta 时拉一次最新 prerelease 信息,展示
「最新 Beta:v1.x.x-beta-tauri」+「前往下载」按钮(openExternal 跳
GitHub release 页面)+ 重新查询按钮。切回 stable 立即清空状态。
- Settings.tsx:把 Toggle 组件 export,让 SettingsModal 复用同一开关样式。
- i18n 五个 locale(zh-CN / zh-TW / en / ja / ko)的 settings.about 都补
齐 betaChannel* 9 个 key(zh-CN 是 source of truth)。
文档:
- README.md / README.zh.md:把 PR-A 的「opt-in,尚未接入」占位换成
manual-download 路径的说明 + tag 约定。
- CLAUDE.md:把「Channel distribution (in progress)」整段重写为已接入
状态,含 tag 约定 / wiring 位置 / 新加的 release verification checklist
(5 条,发版后一定要走一遍,含 stable/beta 双向 cross-check 与 endpoint
采样验证)。
cargo check + npm run build 本地都过。本 PR base = chore/branching-workflow
(PR-A),等 PR-A merge 后会自动指向 beta;与 PR-B-1(CI 端)解耦,可独立
review / merge。
* docs(readme): refresh Status / Architecture / Maintainer release checklist
把 README 的产品/发版描述更新到当前真实状态,配合 PR-A / PR-B 系列工作流改造:
Status (v1.2) 章节:
- 把云端 ASR / 本地 ASR / 润色 provider 三类拆开列;本地 ASR 提及内置
Qwen3-ASR + Windows Foundry Local Whisper 两条线
- 加翻译热键、QA 浮窗、Beta 渠道 opt-in、Homebrew Cask 分发渠道四个要点
- 把「中英双语」改成多语言 UI(实际已支持 zh-CN / zh-TW / en / ja / ko)
Architecture 顶句:
- 由「Auto-updates ride on Tauri updater plugin」改为双渠道描述:Stable
(v<v>-tauri) 自动推送,Beta (v<v>-beta-tauri) 手动下载
Maintainer release checklist:
- 完全重写为双渠道流程:通用准备(5 处版本号 + 冒烟 + secrets)+
Beta 流程(在 beta 分支打 v<v>-beta-tauri tag)+ 正式版流程
(beta→main 后在 main 打 v<v>-tauri tag)
- 把 CI 的 Verify version sync 5-file 校验显式提一次,避免维护者
漏改 Cargo.lock / package-lock 撞 CI 红灯
- 末尾引用 CLAUDE.md 的 5 步 release verification checklist
Roadmap 章节保持原样(多个项目实际状态我无法在不引入回归的前提下
确认,避免误判)。
* docs: drop reference to non-existent bump-version.sh; describe manual bump
---------
Co-authored-by: baiqing <lbx12309@icloud.com>
|
||
|
|
2fe5f2e0d4 |
docs: introduce Beta/Stable two-channel branching workflow (#327)
把分支模型从「dev/main」改造为「Beta/正式版」双渠道,渠道语义直接由分支名承载: - `beta` — Beta 渠道(开发版),默认分支、集成缓冲区,所有 PR 一律打到这里。 Beta 包不推送给普通用户,只面向主动加入 Beta 渠道的用户。 - `main` — 正式版渠道(Stable),始终可发布。仅由维护者在双端冒烟测试通过后从 beta 合入;普通用户默认拿到的就是这条线。 文档与 CI 改动: - README.md / README.zh.md:把「Contributing workflow / 贡献流程」章节 改写为 Beta/正式版双渠道,含分支流向 ASCII 图、6 条核心规则, 以及「Beta 不溢出正式版」的隔离要求。 - CLAUDE.md:把 `### Branch & contribution workflow` 改名为 `### Branch & release-channel workflow`,明确「分支名=渠道名」。 - ci.yml:触发分支从 [main, dev] 改为 [main, beta]。 仓库侧已同步: - 远端 dev 分支已重命名为 beta(git push origin dev:beta + 删除旧 dev) - GitHub 仓库默认分支已切到 beta - release-tauri.yml 用 v*-tauri tag 触发,跟分支无关,发版机制不受影响 Beta 包的 opt-in 分发机制(per-channel updater endpoint + 设置页开关) 将在独立 PR 中接入。在它落地之前,所有 v*-tauri tag 都按正式版对待, 不要从 beta 分支直接打 tag。 Co-authored-by: baiqing <lbx12309@icloud.com> |
||
|
|
9543a5a86f |
feat: Add Homebrew Cask install option (#322)
Adds Casks/openless.rb + update-cask.yml workflow + README install docs. Closes #254 #5. Note: Formula/openless.rb known to fail in Homebrew sandbox (per author); will be removed in follow-up. Cask version will be bumped to current 1.2.23 in same follow-up. |
||
|
|
188efa7e68 | docs: add Cooper to developer list | ||
|
|
13ccf85a48 |
Store credentials in the system vault
Provider credentials should follow the platform credential-store threat model instead of continuing normal plaintext JSON writes. The vault now stores the existing multi-provider credentials payload in the OS credential store, migrates the legacy JSON file only when the vault has no entry, removes the legacy file after a successful write, and keeps raw credential IPC restricted to the main settings window. Constraint: User explicitly chose Keychain/Credential Manager/Linux keyring for issue #230. Constraint: Preserve the existing provider schema and active-provider routing with minimal frontend churn. Rejected: Keep plaintext JSON as the documented product choice | user rejected this direction. Rejected: Split every credential field into separate vault entries | larger migration surface and risks losing active provider state. Confidence: medium Scope-risk: moderate Directive: Security-model and storage-strategy choices must be confirmed with the user before implementation. Tested: cargo check --manifest-path src-tauri/Cargo.toml; npm run -s build; git diff --check; grep for stale plaintext/Keychain fallback wording. Not-tested: Manual migration on macOS Keychain, Windows Credential Manager, or Linux desktop keyring. Related: https://github.com/appergb/openless/issues/230 |
||
|
|
bfc718323b | docs(readme): document macOS-only qwen-asr submodule init step | ||
|
|
2861a30cbe |
Add end-of-file acknowledgements for sponsors, developers, and LinuxDo community
README updates now place a unified acknowledgement section near the end (before License) in both English and Chinese, explicitly thanking sponsors, developers/contributors, and the LinuxDo community with a neutral non-endorsement disclaimer. Constraint: Keep existing sponsor/developer showcase blocks intact while adding final acknowledgement wording Rejected: Put acknowledgement near top profile blocks | user requested end-of-file placement Confidence: high Scope-risk: narrow Directive: Keep acknowledgement wording neutral and avoid implying official affiliation Tested: Manual README markdown diff review Not-tested: Render preview in GitHub UI |