Skip to content

chore: reconcile main with beta baseline - #1295

Merged
appergb merged 33 commits into
mainfrom
appergb/branch-consolidation-20261030
Oct 30, 2026
Merged

appergb merged 33 commits into
mainfrom
appergb/branch-consolidation-20261030

Conversation

@appergb

@appergb appergb commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

What changed

Reconcile the stable main baseline with the current beta history through one reviewable merge PR. This keeps the existing open PR branches isolated while bringing main up to the same product baseline.

Branch cleanup scope

  • Retains main and beta.
  • Leaves all currently open PR source branches untouched.
  • Removes only closed and verified merged branch refs.
  • Leaves unmapped or closed-unmerged refs for a later review instead of deleting them.

Validation

  • Local merge completed without conflicts.
  • git diff --check completed; remaining vendor whitespace is pre-existing in the vendored cpal tree.
  • Full CI is required before merge.
  • ChatGPT/Codex PR review must report no unresolved PR-level severe findings before merge.

TheTouYu and others added 30 commits October 23, 2026 01:11
* fix(ios): 补齐 Core 集成并统一应用与输入法版本

Restore the native input contracts required by the iOS host, reconcile startup with the current Core write gate, and deliver version 0.1.0 (16) from shared bundle settings. Add verified IPA packaging and iOS source CI, refresh dependency attribution, and replace obsolete reports with maintained project entry points.

Co-Authored-By: Codex <noreply@openai.com>

* test(core): 修复翻译字段回归测试的 Clippy 检查

Co-Authored-By: Codex <noreply@openai.com>

* fix(core): 保留旧客户端翻译选择并同步录音目标列表

Reconcile a legacy scalar selection echoed with an initially empty target list without weakening explicit deletion of stored choices. Keep the recording picker selection and ordered targets in one durable update. Add persistence regressions and account for canonical language names in the Linux revision-conflict fixture.

Co-Authored-By: Codex <noreply@openai.com>

* fix(core): 在界面草稿重建前规范翻译偏好

Resolve translation scalar/list compatibility while producing a patched draft so rebasing and serialization cannot discard a pending selection. Cover the round trip and explicit deletion, and verify the Linux settings queue against the normalized result.

Co-Authored-By: Codex <noreply@openai.com>

* build(ios): 对齐 Beta 并隔离移动端构建与测试

Keep desktop sources identical to beta. Prepare the iOS input-contract additions in a generated Core copy owned by the mobile Rust workspace, and verify it through dedicated tests and an unsigned device build in CI.

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: TRIP <192432815+appergb@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
* fix(linux-egui): 让 Menu 键可以注册为听写热键

fcitx5 的 Key::parse 按 X11 keysym 名做大小写敏感查表:SetCustomDictationTrigger("Menu")
→ sym=0xff67 成功,("menu") → invalid key 被拒。而 normalize_fcitx_primary 会把主键名
统一转小写,于是任何非修饰键的命名 keysym(Menu 等)都注册不上。

只对查表中确实首字母大写的名字还原大小写,其余(space / f5 / 单字符)维持原行为。
新增单测 menu_primary_keeps_the_case_fcitx_expects。

* fix(core): 录音流掉线时继续转写已捕获音频

蓝牙/虚拟输入设备在录音过程中可能报 "Device disconnected"(PipeWire 节点被挂起或
蓝牙 SCO 掉线)。此前 recording.stop() 返回错误会取消转写并丢弃已经流进 ASR 的音频,
用户看到的就是「识别到一半整段消失、既不插入也不进剪贴板」(history 记 transcribeFailed)。

改为:掉线只记警告,继续用已捕获音频完成转写;是否真的没有内容交给下游
「未收到有效音频」守卫判断。

* feat(diagnostics): 记录听写失败原因与输入设备选择

- core: mark_dictation_failed 记录 code + message。此前只把 errorCode 写进 history,
  日志里看不到失败发生在哪一步(录音停止 / 转写 / 润色)。
- linux-egui: 输入设备精确匹配失败时打印可选设备列表,并记录最终选用的设备名。
  之前只有一句「using the default device: <填的名字>」,无法判断实际用的是哪只麦。

* test(core): 录音流 stop 失败不再算转写失败

`stop_during_provider_preparation_releases_capture_before_finalization` 的
`stop_fails` 分支断言的是旧契约:录音流 `stop()` 报错时整次听写以
`BackendErrorCode::Platform` 失败、history 记 `transcribeFailed`。

而本 PR 要修的正是这一点 —— 蓝牙麦掉线 / PipeWire 节点被挂起时,已经流进
ASR 的音频不该丢,`stop()` 的失败降级为 warn 后继续转写。于是 CI 上
`openless-core --lib` 稳定挂在这一条(本地 3/3 复现,beta 上通过)。

行为变了,断言要跟着变:`stop_fails` 现在与成功路径结果一致,只多一条 warn。
断言改为「返回 Ok + 文本正确 + history 不记 transcribeFailed」,
并保留原有的 stop_count / archive / has_audio_recording 检查。

* feat(linux-egui): 重做日志后端——可调级别、心跳降噪、轮转与契约

日志是 Linux 用户唯一能交回来的证据,但现在的实现在三处上不去:

一、**级别写死**。`LevelFilter::Info` 是常量,`RUST_LOG` 完全没读。用户报
「听写没反应」时,能做的只有重新编译一个 debug 版本给他。现在
`OPENLESS_LOG`(兼容 `RUST_LOG`)决定过滤,语法 `<level>[,<模块>=<level>]`,
最长前缀优先;无法识别的片段退回 `info` 而不是静默关掉。启动首行写明生效
级别与是否轮转过。

二、**心跳吃满日志**。实测一份 5 天的日志 39,111 行里 36,485 行(93%)是
`[pump] heartbeat` 与 `[ui-client] ipc round-trip`,2 MiB × 2 份的配额一天
就滚完,真正的错误全被挤出去。现在:

- 泵心跳仍每 10 秒采样,但**状态变化立刻记 `info`**(窗口进程/托盘/录音
  这三项的翻转才是当初「关窗后热键失效」的判据),状态没变时每 5 分钟一条
  存活证明,其余走 `debug`;
- IPC 往返探针(每 20 次一条)降为 `debug`。

三、**`error` 当 tracer 用**。`cloud_sync_e2ee/adapter.rs` 把
`stage=gate_open` / `stage=store_ok` / `build start` 这类进度全写
`log::error!`,于是每次正常启动都刷七行 ERROR —— 79 行 ERROR 里 66 行是它,
真错误反而看不见。这些降为 `debug`,失败分支保持 `error`。

顺带:

- 轮转从「1 份 5 MiB」改成「3 份 2 MiB」,且**只有宿主轮转**(UI 窗口进程
  若也改名,宿主手里的句柄会指向被改名的 inode,之后的宿主管日志就写进
  `.1` 里);带单测。
- 时间戳落到本地时区(之前是 UTC,用户对着自己的操作时间找不到对应行)。
- 行格式补上模块目标,线程名与源码位置只在 debug 及以上出现。
- `scripts/egui-logging-contract.test.mjs` 守住四条:必须读 `OPENLESS_LOG`、
  心跳不许无条件 `info`、IPC 探针必须 `debug`、`error` 里不许出现
  `stage=`,另外拒绝形如 `api_key=`/`token=`/`password=` 的日志参数。
- `docs/linux-egui-logging.md` 写清级别语义、开关、轮转、前缀表与不记录的东西。

* feat(linux-egui): 日志级别可设、导出带历史、补上六处盲区

第二轮,把上一版日志「能看」提升到「能定位」。

一、**级别可在设置里改**(设置 → 高级 → 调试 → 日志级别)

- 写 `<data_dir>/logs/level`,重启生效,界面提示;`info`/`debug`/`trace` 三档。
- 宿主本地设置,不进 Core 偏好:不上云、不影响 Tauri 与移动端,也不动
  `contract/backend-2.0.json`。
- 优先级:`OPENLESS_LOG`/`RUST_LOG` → 设置文件 → 默认 `info`。环境变量仍然
  最高,临时排查不该被上一次的界面选择挡住。

二、**导出不再只有当前文件**

`export_error_logs` 按 `.3` → `.2` → `.1` → 当前的时间正序拼接,带分隔头,
并在文件头写明版本、系统架构、生效级别与导出时间 —— 用户报的问题几乎总是
发生在轮转之后,只导 `openless.log` 恰好丢掉现场。默认文件名改成
`openless-diagnostics.log`,避免误以为和那个单文件是一回事。

三、**周期噪声再降一档**

| 日志 | 以前 | 现在 |
|---|---|---|
| `[transcript] partial emitted` | 每个流式分片一条 info | debug |
| `[hotkey] local edge` | 每次按键一条 info | debug |
| `[ui-client] actions from the renderer` | 每批动作一条 info | debug |

四、**补上六处盲区**

| 位置 | 新增 | 以前查不到什么 |
|---|---|---|
| `audio.rs` | `[audio] capture started backend=… device=… format=… rate=… channels=…` | 「录到的全是静音」:成功路径原本一条日志都没有,不知道落到哪个后端/哪只麦 |
| `audio.rs` | `[audio] no input backend could open a stream (tried […])` | 三个后端全失败时的总括 |
| core `api.rs` | `[dictation] start session_id=… target=… asr=… insert=… translate=…` | 「按了热键没反应」与「录上了没识别」在日志里长得一样 |
| core `dictation_engine.rs` | `[asr] session_id=… provider=… elapsed_ms=… chars=…` | 「听写很慢」:分不清慢在 ASR 还是润色 |
| `main.rs` | `[startup] single-instance lock acquired` / `another instance owns the lock; forwarded…` | 「双击图标没反应」原本完全无痕 |
| `settings_save.rs` | `[settings] saved request=…` / `failed: …` | 设置改了没保存,卡在哪一段无从判断 |

五、测试

- `logging::tests` 6 项:级别解析(含最长前缀优先)、垃圾输入退回默认、
  轮转链(`.4` 被丢弃)、导出含全部历史且时间正序、级别持久化。
- `scripts/egui-logging-contract.test.mjs` 扩到 9 组断言:级别必须可持久化、
  导出必须含历史、capture 成功必须记后端与格式、听写起点与单实例结果必须
  留痕、streaming partial 与按键 edge 不许在 info。
- `docs/linux-egui-logging.md` 更新:设置入口、导出行为、新增的「一次听写在
  日志里长什么样」串读示例。

* test(linux-egui): 单测断言消息改英文,别触发本地化契约

localization_contract 会把 linux-egui 源码里的裸简体中文当成用户可见文案拦下来。
我上一版在 logging 的轮转单测里写了中文断言消息("当前文件应被移走"),CI 报
two new raw Simplified-Chinese literals。断言消息改成英文即可。

* fix(linux-egui): preserve effective logging diagnostics

---------

Co-authored-by: TheTouyu <1634389899@qq.com>
Co-authored-by: Chris233 <h-chris233@outlook.com>
Reviewed and validated end to end. Cursor context privacy-path tests now opt out explicitly after the default became enabled; all canonical cloud-sync event fixtures include serviceOrigin; capture-first startup documentation matches the implemented flow. Full local openless-core tests and every configured cross-platform CI check passed.
Reviewed and validated: five Tauri version locations are synchronized, local version contract and 152 frontend tests pass, and all configured cross-platform CI checks are green. This merge prepares v2.0.2-Beta.3-tauri from the reviewed beta baseline.
* feat(audio): add optional playback echo cancellation (#1223)

Add bounded render-reference processing for desktop dictation, preserve playback without muting, expose a local-only best-effort setting, and document platform limitations.

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

* fix(ci): keep recorder harness on cpal 0.18

Update the standalone recorder module harness to match the production cpal API and expose the shared render-reference buffer used by the recorder.

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

* chore(audio): instrument issue 1223 stream failures

Add per-capture structured probes for reference availability, native CPAL errors, first and post-error callbacks, queue backpressure and watchdog faults. Keep microphone error behavior unchanged until the diagnostic build is reproduced on affected hardware. Cover diagnostic counters and error-kind redaction with recorder tests.

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

* fix(audio): keep capture running after recoverable xruns

Separate CPAL Xrun notifications from fatal microphone errors after instrumented Windows logs showed data callbacks continuing immediately after each xrun. Keep capture liveness unchanged so the existing watchdog still detects stalled streams, preserve device and invalidated-stream failures, and retain diagnostic probes for hardware verification. Add regression coverage for xrun handling, repeated notifications, real faults and stopped capture.

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

* fix(ci): allow manual Intel macOS runner fallback

Keep push and pull request defaults unchanged while allowing manual CI dispatches to select macos-15-intel when hosted ARM runners cannot acquire jobs. Apply the same explicit choice to macOS stable and MSRV checks, preserve all gates, and cover the routing contract in the frontend test suite.

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

* fix(audio): adapt native cue to cpal 0.18

Use the current sample-rate alias and pass StreamConfig by value. Include the native cue module in the recorder harness so its CPAL calls are compiled by the standalone remote checks.

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

* fix(linux): preserve backend diagnostics and adapt reference timestamps

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Chris233 <h-chris233@outlook.com>
* feat(linux-egui): 在 Linux 端整体删除 Less Computer

只动 Linux,Core 与 Tauri / 移动端不受影响:`openless_core::less_computer`、
`coding_agent` 领域、`src/`(React)、`src-tauri/`、iOS/Harmony 均未改动。

删除的东西(Linux 侧,共 25 个文件、−2905/+234):

- 设置页:Beta「正在测试中的功能」总开关与 Agent 详情页(后端 / 权限 / 模型 /
  工作目录 / 可执行文件)、快捷键页的 Agent 语音行。删掉总开关是因为它原本只
  门禁 Less Computer,留着就成了「点了没反应」的假开关。
- 快照/事件:`less_computer_input|output|turn_start|session|entries|working|
  popup` 状态、`BackendEventKind::LessComputerEvent` 分支、`HostAction::
  ShowLessComputer`(保留空分支并注明原因:Core 仍会发这个动作)。
- 弹窗协议:`PopupKind::LessComputer`、`HostToPopup::LessComputer`、
  `PopupToHost::{Submit,Approve,Cancel,Dismiss}LessComputer`、
  `LessComputerEntry/Approval/PopupState`、面板窗口尺寸,以及
  `ui/frontend/popups.rs` 里 10 KB 的面板渲染(连带 4 个只服务于它的行内组件)。
- 热键:`LocalHotkey::LessComputer`、`LinuxHotkeyEvent::LessComputer{ Pressed,
  Released,Combined }`、fcitx5 的 `SetLessComputerHotkeyRaw` 方法与
  `LessComputerKeyEvent` / `LessComputerKeyCombined` 信号(Rust 封装 + C++ 插件
  + 配置键 + 近失诊断表),宿主端的采集租约控制(`LinuxLessComputerRecording
  Control`)与对应的 lib.rs 单测。
- CLI 进程适配器:整个 `linux-egui/src/coding_agent.rs`(369 行)与
  `configure_coding_agent_process` 接线。
- i18n:清理 43 条只被上述界面引用的文案;`sync-egui-i18n.mjs` 的 KEEP_EGUI
  换成代理说明(Linux 端不再提 Less Computer)。

顺带吸收上游 4 条文案漂移(nav.selection_ask / selection_ask.title /
qa.title / settings.recording.capsule_style_classic,Tauri 侧已把
「划词追问」改成「问任何问题」),否则 `sync-egui-i18n --check` 会红。

验证:cargo check/clippy `-D warnings` 干净;`cargo test -p openless-linux-egui
--all-targets` 261 + 217 + 其它集成测试全绿(含 localization_contract);
fcitx5 插件 `build.sh` + `ctest` 通过;前端 144 个契约测试全绿;
`cargo build --release --locked` 通过。

* fix(linux-gate): Less Computer 移除后 public-surface 门禁不再要求 reducer 消费它

check-linux-public-surface.ps1 里硬编码了 Linux 事件 reducer 必须出现
`LessComputerEventKind::*` 分支。功能在 Linux 端整体删除后这条断言已经过期,
改为只校验 replay 相关 token,并注明 Core 仍会发该事件、Linux 刻意不消费。

* fix(linux): retire removed Agent shortcut bindings

* test(linux): isolate headless host settings effects

* fix(linux): skip native effects for retired-only binding migration

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
…udio settings (#1258)

* perf(audio): optimize shared DSP and complete audio settings UIs

* fix(ci): gate Android spectrum emitter and refresh IPC audit

* perf(audio): add cross-revision PCM checks and native iOS audio settings

* fix(ios): localize native audio settings and preserve test compatibility

* fix(ios): use host executor in audio flush regression test

* fix(ios): preserve converter output capacity when reusing PCM buffers

* fix(ios): consume converter input completely across variable audio packets

* feat(audio): 调试录音可独立选择保存处理前/处理后的音频

原来的「保留原始录音(调试)」只会落一份处理后的 WAV,没法判断某个识别问题是
麦克风原始信号差,还是降噪/增益/AGC 处理坏了。

Core:

- `UserPreferences` 新增 `debug_archive_processed` / `debug_archive_raw`,两个独立
  开关,默认都关;旧 preferences.json 走 serde default,不迁移。
- `RecordingPlan` 增加 `archive_processed` / `archive_raw`,由上面两个开关推出。
  quick note / undecided 这类功能必需档案始终 `archive_processed = true`,不受开关
  影响;调试录音两个开关都关时 `archive_successful_recording = false`(不落盘)。
- 新增 `openless_core::pcm_f32_to_i16_le`,让宿主把「处理前」的交织 f32 帧也量化
  成同格式 PCM,与处理器内部规则一致。
- 偏好注册表补两条(Portable),云同步与契约测试同步跟上。

linux-egui:

- 主档案 `<session>.wav` 按 `archive_processed` 决定写处理后还是原始 PCM;只有两份
  都要时才额外创建 `<session>.raw.wav`。
- 音频处理线程在处理前就拿到原始帧,因此不做常态额外分配:`raw` 只在开关打开时
  才量化。
- 设置页在「保留原始录音(调试)」下面新增两个开关:保存处理后的音频 / 另存一份
  处理前的音频;打开主开关且两项都关时自动勾上「处理后」,避免主开关看起来没效果。
  五语文案补在 i18n 目录里。

测试:`dictation_context` 新增用例覆盖四种开关组合与 quick note 例外;
`openless-core --lib` 1391 passed,`openless-linux-egui --lib` 266 passed,
`clippy -D warnings` 干净。

* feat(audio-dsp): 补全 MicYou 链路的 AEC 与输出缓冲,egui 设置页跟上

MicYou 的 DSP 参数是 8 项:增益 / 声学回声消除 / 降噪 / 去混响 / 自动增益 /
人声活动检测 / 处理链路 / 缓冲区大小。Core 的 `AudioDspConfig` 一直缺 AEC 与
缓冲区,而且因为 `processing_order` 只有 6 个阶段、`settings()` 里用
`..Default::default()` 兜底,MicYou 的 `processing_chain` 里根本没有 "AEC" 这一级
—— 即使把 `aec_enabled` 打开也不会执行。

Core:

- `AudioDspStage` 新增 `AcousticEchoCancellation`(链名 "AEC"),
  `processing_order` 由 6 级变 7 级,默认顺序与 MicYou 一致(AEC 在最前)。
- `AudioDspConfig` 新增 `aec_enabled` 与 `output_buffer_ms`(100..=1200,默认 300),
  `normalized()` 夹紧范围,`settings()` 映射到 micyou 的 `AudioDspSettings`。
- `processing_order` 反序列化兼容老配置:6 级数组把 AEC 前置补齐,其余顺序原样
  保留(既符合 AEC 必须在降噪之前,也不会制造重复项触发「重复即重置」)。
- 测试:老 6 级配置反序列化保留自定义顺序 + AEC 前置;AEC/缓冲区能到达 DSP
  设置;越界缓冲夹到 1200。

linux-egui:

- 设置 → 录音 → 语音增强设置新增「回声消除与缓冲」段:AEC 开关(附「需要扬声器
  参考信号」说明)+ 输出缓冲区 100–1200 ms 滑杆(附延迟代价说明)。
- 处理链路列表把 AEC 作为一级显示,未开启时置灰;五语文案补齐。

这样 MicYou 的 8 项参数在 egui 前端全部可达。

* feat(tauri): 语音增强设置补上 AEC 与输出缓冲

Core 侧已经支持 `aec_enabled` / `output_buffer_ms`(并真正传给 MicYou 的
`processing_chain`),Tauri 的设置页还停在 6 级链路:

- `AudioDspStage` 类型加 `acousticEchoCancellation`,`AudioDspConfig` 加
  `aecEnabled` / `outputBufferMs`,`defaultAudioDsp` 同步成 7 级链路(AEC 在最前)。
- `AudioDspSettings.tsx` 新增「回声消除」开关(附「需要扬声器参考信号」说明)与
  输出缓冲 100–1200 ms 滑杆;处理顺序列表自动多出 AEC 一级。
- 八语文案补齐(en / zh-CN / zh-TW / ja / ko / de / fr / es),`UserPreferences`
  类型与 mock 数据跟上 Core 新增的两个调试录音字段。

验证:`tsc --noEmit` 干净、`vite build` 成功、`frontend-test-runner` 全过。

* feat(aec): 采集扬声器回环作为 AEC 参考,并允许选择参考源

AEC 打开却一直没效果:MicYou 需要「扬声器此刻在放什么」当参考,而宿主从来没有
把输出信号喂进来。这次把整条链补上。

Core:

- 新增 `FarEndQueue`:宿主在独立采集线程里 push(任意采样率/通道),DSP 线程在
  每帧处理前 drain。队列按 96k 样本(约 2 秒 @48k)限长,丢最旧的;非有限值与
  空帧直接丢弃。
- `AudioProcessor::attach_far_end(queue)` + `process()` 里把参考重采样到 48 kHz
  单声道后交给 `micyou set_far_end_audio`(MicYou 用最近历史做自适应滤波,所以
  参考必须先于本帧到达)。
- `RecordingPlan` 增加 `aec_reference_device_name`,来自新增偏好
  `aec_reference_device_name`(空 = 自动,`"off"` = 不采集;注册表记为
  Excluded/local_hardware,不同步到云端)。

linux-egui:

- `is_loopback_device_name()` 识别 PulseAudio/PipeWire 的 `<sink>.monitor`、
  `Monitor of …` 与 ALSA `snd-aloop`;带单测确认普通麦克风不会被误判。
- `list_loopback_devices()` 按 `pipewire → pulseaudio → alsa` 顺序枚举去重。
- `start_reference_capture()`:另起一条 cpal 采集流抓回环,带自己的启动握手与
  停止标志;同样按三后端顺序尝试,ALSA 无回环时只记一条 warn —— 录音本身照常。
- 录音侧:AEC 打开且参考未被显式关闭时创建队列、挂到处理器、启动参考采集;
  `LinuxActiveRecording` 持有它并在 Drop 时**先**停参考流再拆主线程。
- 设置页:语音增强里 AEC 打开后出现「扬声器参考源」下拉(自动 / 不采集 / 具名
  监听设备),五语文案补齐。

验证(MicYou micyou-dsp 已含 AEC7 模型,beta 上无冲突改动):

```
openless-core --lib                 1399 passed
openless-linux-egui --lib            268 passed
localization_contract                  5 passed
clippy -D warnings                  clean
deb/rpm verify                      PASS(2.0.2-Beta.2-146)
```

* fix(aec): 增强关闭时 AEC 也能独立工作

之前 `process()` 在 `enhancement_enabled == false` 时直接走归一化,AEC 永远不执行
—— 但「外放听写、只想去回声、不想动音色」正是最需要 AEC 的场景。

现在只要 AEC 打开且本次录音接了参考队列,就仍然走 MicYou 链路(其余阶段由各自
开关控制,不会顺带改音色)。

顺带记录实测:单核上 aec7 模型约 2.0x 实时(≈50% 单核),NLMS 类方案约 31x
(≈3%),差 16 倍 —— 弱机上要用轻量参考抵消,别默认开模型。

* feat(aec): 轻量 NLMS 与 aec7 模型两种回声消除并存,用户可选

「能不能两者都留、让用户选」——可以,而且实测下来两者正好互补。

- 新增 `AecMode { Off, Light, Model }` 取代原来的布尔 `aec_enabled`;
  老 preferences 里的 `aecEnabled: true/false` 由自定义反序列化映射成
  `Light`/`Off`,不会丢配置。
- `Light`:把 #1228 的有界归一化 LMS(1024 抽头、双讲时暂停自适应)移植进来,
  在 Core 里对麦克风原始样本做抵消,再进降噪/增益链。
- `Model`:仍走 MicYou 的 aec7 神经网络(经 `FarEndQueue` → `set_far_end_audio`)。
- 两种模式共用同一套参考采集(linux-egui 的 `<sink>.monitor` 与扬声器选择)。

关键修正:早先轻量路径把参考重采样到 16 kHz 却拿 48 kHz 的麦克风样本做抵消,
采样率不一致导致 ERLE ≈ 0(等于没做)。现在参考按麦克风采样率重采样。

实测(单核,120 s 音频,单讲段,回声延迟 15 ms、衰减 0.45):

| 模式 | ERLE | 实时倍率 | 单核占用 |
|---|---:|---:|---:|
| 关闭 | 0 dB | 5719x | ~0% |
| 轻量 NLMS | **22.9 dB** | 6.2x | **~16%** |
| aec7 模型 | **26.9 dB** | 1.9x | **~53%** |

模型只多 4 dB,却贵 3.3 倍 —— 所以默认关、弱机推轻量、强机可选模型。

UI:egui 与 Tauri 的语音增强里都改成三选一(关闭 / 轻量(省 CPU)/ 模型(更强)),
选模型时提示 CPU 代价;八语 + 五语文案补齐。

* refactor(audio-dsp): 去掉十段均衡器,把「去混响 / 人声检测」的真实能力写清楚

对照 MicYou 的实现逐个核对后,界面上三处名不副实:

1. **均衡器**:用户不需要,整块从 egui 设置里移除(`equalizer_*` 配置字段保留,
   旧 preferences 仍能反序列化;处理链路里的 Equalizer 阶段默认不启用)。
2. **去混响**:MicYou 的 `apply_dereverb` 其实是 `x[n] - mix·x[n-10ms]` —— 一个
   固定延迟的**梳状滤波**,不是去混响。界面改名为「梳状滤波(简易去混响)」,
   提示里写清它会给音色染金属感、真正去混响需要频谱或模型方法(当前没做)。
3. **人声检测**:MicYou 的 `apply_vad` 是 RMS 门限 + 淡入淡出**噪声门**,不输出
   语音/非语音判定。界面改名为「噪声门限(不是人声检测)」,并说明低于门限只是
   衰减、不会停止录音。

结果:设置页现在只暴露真实存在且自解释的开关,不再有「看起来有、其实没做」的项。

* feat(tauri): 扬声器回环参考 + 处理前/处理后档案落盘

把 egui 侧已经跑通的两件事在 Tauri 侧补齐(Linux 上 Tauri crate 被
`compile_error!` 拦住,只能靠 CI 的 macOS/Windows job 真编,这里做了 rustfmt
语法校验 + 前端 tsc/vite/144 个前端测试)。

一、AEC 的扬声器参考(`start_reference_stream`)

- 与 egui 同样的思路:拿宿主**默认输出设备**开一条 capture 流 —— Windows 的
  WASAPI 允许在 render 设备上做 loopback,Linux 的 PulseAudio/PipeWire 把输出
  映射到 monitor 源,macOS 走 CoreAudio 同一设备。
- 参考帧按设备原生采样率/通道推入 `openless_core::FarEndQueue`,由 Core 重采样
  后交给 AEC(轻量 NLMS 与 aec7 模型都吃这一路)。
- 没有回环源时只记一条 warn,录音照常进行;移动端 stub 直接返回不可用。
- 参考流挂在 `Recorder` 上,跟录音同生命周期,Drop 即停。

二、处理前/处理后档案

- 新增 `ArchiveTargets { processed, raw, write_processed }` 与
  `Recorder::start_with_archive_targets`。
- 关键约束:原始 PCM 只存在于 DSP 工作线程里,而 staged 路径的档案是
  `commit` 时才创建的。所以引入 `RawTap`:`CaptureSink::promote` 把 tap 写进
  共享槽,DSP 线程每帧读一次槽。
  - 两份都要 → 主档案写处理后,另存 `<session>.raw.wav` 写原始;
  - 只要处理前 → 主档案直接写原始 PCM(回退/播放功能不变,且不多出文件);
  - 都不要 → 不落盘。
- `ForwardTarget.archive` 在 `write_processed == false` 时为 `None`,避免同一
  段 PCM 被写两次。

三、Tauri 设置页

- 调试页在「保留原始录音(调试)」下面恢复两个开关(保存处理后的音频 / 另存
  一份处理前的音频),打开主开关且两项都空时自动勾上「处理后」。

* chore(linux-egui): 2.0.2-Beta.3-149

* fix(tauri): 测试里的 promote 调用补齐 raw_tap 参数,按 rustfmt 收尾

* fix(ci): recorder 模块测试的 shim 补上新增的 core 导出

scripts/test-recorder-module.py 会生成一个只导出少量 core 符号的临时 crate,
 /  /  没加进去,Windows 的
`openless-recorder-tests` 编译因此失败(7 个 E0425/E0433)。本地 `--check`
与完整跑 60 个测试均通过。

* fix(audio): close PR 1258 processing and archive gaps

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
* ci(release): tag 推送后自动补鸿蒙与 Linux 包

`Release HarmonyOS` 与 `Release Linux egui` 原本是 dispatch-only,但没有任何东西
把它们和 `v*-tauri` 标签连起来:两个 workflow 各自的运行次数是 0,于是 2.0.x 每个
Release 都没有 `.hap`、`.deb`、`.rpm`,AppGallery 上传也从未发生,而且整个过程没有
任何报错——Silent miss。

在 `release-tauri.yml` 末尾加 `dispatch-platform-releases`:桌面/Android 矩阵结束后
用 `GITHUB_TOKEN`(`workflow_dispatch` 是文档里允许的两个例外之一,不需要 PAT,只要
`actions: write`)把同一个标签派发给这两个 workflow。之所以挂在标签构建尾部而不是给
它们加 `push:` 触发,是因为它们的 attach 步骤只等 10 分钟,而桌面矩阵要跑 40 分钟;
等到矩阵结束再派发,Release 一定已经存在。

- 鸿蒙:`channel` 取 `vars.HARMONY_AUTO_CHANNEL || 'none'`。默认走仓库分发路径(未签名
  HAP 挂到 Release,不碰商店);把仓库变量设为 `release` / `test` 即自动上传签名包作为
  AGC 候选包(该上传不会提交审核,提交仍是人工决定)。
- Linux:`publish=true`,与鸿蒙一致自动挂到同一个 Release。因此
  `docs/linux-egui-handoff/07-acceptance.md` 的真机验收从「打包后」前移为「打标签前」
  的 gate,workflow 与 RELEASING.md 都已写明。
- 两个 workflow 仍然是 dispatch-only(契约测试要求),人工 dispatch 保留为修复路径。
- 补契约测试:`harmony-release-contract`、`linux-egui-release-contract` 断言尾部派发
  存在、只用标签推送触发、默认不带签名渠道。

* fix(harmony): release 流水线补 OHOS target 与 npm 依赖

第一次真正 dispatch `Release HarmonyOS` 就暴露了两个从未被发现的问题——这个 workflow
在有史以来的运行次数是 0,所以谁都没发现它其实跑不通:

```
error[E0463]: can't find crate for `core`
= note: the `aarch64-unknown-linux-ohos` target may not be installed
```

`build-harmony-backend.sh` 用 `cargo build --target aarch64-unknown-linux-ohos`,
`build-harmony-frontend.sh` 用 `npm run build`,而 hosted runner 既没有 Tier-2 OHOS
target std 也没有 `node_modules`。`ci.yml` 的 `harmony-check` 两者都装了,release 这条
路径漏了,于是签名、证书校验、SDK 下载全部成功后卡在编译阶段。

按 `ci.yml` 的写法补齐:`cache-npm` + `npm ci`、`dtolnay/rust-toolchain@stable` 带
`targets: aarch64-unknown-linux-ohos`、`swatinem/rust-cache`(复用 `harmony-check` 的
`key: harmony-host`,标签构建只读不写)。

契约测试补断言:OHOS target 必须安装、必须 `npm ci`,且 target 列表要与 `ci.yml` 里
真正打 HAP 的那个 job 保持一致——只在一边加 target 是这次 bug 的形态。

* fix(harmony): AGC 上传步骤的折叠标量把命令注释掉了

上一个 run(40337344929)构建、签名、校验、挂 Release 全部成功,只有 AppGallery
上传失败:

```
/home/runner/work/_temp/...sh: line 2: --channel: command not found
##[error]Process completed with exit code 127
```

原因是那一步用的是 `run: >-` 折叠标量,且以注释开头。折叠后第一行变成

```
# Only the signed package is uploaded; ... node scripts/upload-appgallery.mjs \
```

整行都是注释,命令连同续行反斜杠一起被吃掉,第二行 `--channel` 成了独立命令。
改成 `run: |` 字面块,注释移到 YAML 层(`#` 开头不会被折叠进脚本)。

契约测试加断言:`release-harmony.yml` 里任何 `run: >-` 之后都不允许紧跟注释行,
以及上传命令必须是可执行的 `node scripts/upload-appgallery.mjs \` 形式。

* docs(release): 记录 HARMONY_AUTO_CHANNEL 变量

* fix(release): pin platform dispatch and release checksums

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
* fix(textstream): keep the authoritative text when a decode rewrites a stable prefix

* fix(textstream): keep the authoritative text when a decode rewrites a stable prefix
* feat(updater): 更新清单改由 CDN 提供,客户端不再读 release 里的 json

窗口内的更新器原先从 GitHub Release 资产取清单
(releases/latest/download/latest-{target}-{arch}[-beta].json),于是每个 release
要挂 14 个 latest-*.json(7 个 target/arch × mirror 双份),且 Stable 走滚动
指针 → 每次发版都要把同名文件重新传一遍。

改成读项目自己的 CDN:

  https://dl.aeoform.com/updates/stable/latest-{target}-{arch}.json
  https://dl.aeoform.com/updates/beta/latest-{target}-{arch}-beta.json

- 每个平台/渠道只有一个滚动文件,发版覆盖即可,不再往 release 累积副本。
- 渠道隔离从「文件名后缀」变成「URL 路径」,Stable 客户端拿不到 Beta 清单。
- 清单格式不变(version/pub_date/url/signature),plugin-updater 继续负责下载、
  minisign 校验与安装;清单里的下载地址也指向 CDN,国内下载不再经过 github.com。
- Beta 不再需要拉 releases.atom 找 tag,整条链路去掉一个 github.com 依赖。
- Android 更新检查同步改为 CDN 单地址;SSRF 白名单加入 CDN,同时保留旧
  GitHub/fastgit 前缀,已缓存的旧清单仍可安装。

release 侧发布流程与既有资产完全不动。

* ci(release): 清单不再挂到 Release,改为 workflow 产物供 CDN 同步

客户端已经改读 dl.aeoform.com 的 /updates/{stable,beta}/ 清单(上一提交),
release 里那 14 个 latest-*.json 没有任何客户端再读,继续上传只会让每个
release 多背 14 个几乎相同的文件。

- release-tauri.yml:softprops files 去掉 bundle/latest-*.json。桌面清单仍然
  通过既有的 openless-macos-<arch>-updater / openless-windows-x64-updater
  产物离开 workflow(CDN 同步从那里取)。
- android-apk.yml:release 文件列表去掉 latest-android-*.json;新增
  openless-android-updater-manifests 产物,否则 Android 清单会随删除一起丢失
  (原来它只存在于 release 资产里)。顺带把 release 正文里的清单说明改成 CDN
  口径。
- 契约测试跟着收紧:
  - release-publication-contract:两个 workflow 的发布者 files 不得含
    latest-*.json,且必须有且仅有一个 upload-artifact 携带这些清单。
  - android-apk-workflow-contract:把清单文件放进 fixture,断言它们不会进
    release 资产,并断言清单产物步骤存在。
- README / README.zh / RELEASING:说明清单由运行产物手动同步到 CDN,以及
  改动前的客户端读的是旧 GitHub 路径、因此需要一次手动下载。

* fix(updater): use CDN artifacts and validate Android URLs

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
* fix: stabilize Sherpa offline decode diagnostics

* fix: keep Android Sherpa threading unchanged
* fix(capsule): isolate mirror events for live display switching

* fix(windows): restore transcript after mirror reuse

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
* ci: 按 Cargo 依赖图判定平台范围,砍掉重复跑与独立胶囊 job

一次 PR 要拉起 12 台 VM,且 feature 分支的 push 和它的 PR 各跑一遍完整矩阵。
实测最近 42 次 CI run:平均每次 86 VM-min、峰值 12 台并发;42 次里 8 次是
同一 commit 的重复 push run。

1. push 只在默认分支跑(main/beta)。feature 分支由它的 PR 覆盖;默认分支的
   push run 仍保留,因为它是基线证据、也是 Rust/Playwright 缓存的唯一写入方。
   文档本来就写着「push → main/beta」,实现是 ['**'],这次对齐。

2. 平台范围判定从手写路径表换成 Cargo 自己的包图:
   scripts/ci-impact.mjs 用 cargo metadata --no-deps --offline 读出每个 workspace
   包的目录、仓内依赖边和边上的 cfg(...) 守卫,从各平台根包走闭包 —— 文件在范围
   内当且仅当拥有它的包在该闭包里。于是:
   - iOS 工程层(ios-device)只看 openless-all/ios/**:共享 crate 已由 iOS 的
     test job 编译校验,device-build(13min × 10x 计价的 macOS runner)不再被
     crates/openless-core 的改动无谓拉起;
   - ArkTS 只跑鸿蒙;linux-egui 只跑 Linux;纯前端 bundle 改动不再跑 Linux 构建
     (Linux 宿主是原生 egui,不读 dist/)也不再跑 Android cargo check;
   - 契约测试(*.test.mjs)只跑桌面两个 runner,不触发 Playwright 动效套件;
   - 新增 frontend / android / ios-device 三个区域,判定全部 fail-open。
   结果:src-tauri 改动 → 桌面+Android;core 改动 → 五个平台(真的都依赖它);
   纯 TSX → 桌面+鸿蒙+动效,Linux/iOS 跳过。

3. ui-motion(15.5min)改由 frontend 区域门控。

4. native-capsule 的 macOS/Windows 双 job(payload 只有 ~40s 的 cargo check)
   并入 cross-platform:那个 job 本来就为 generate_context! 构建同一个 app
   workspace target 目录,增量编译几乎免费,省掉两个 VM 分配和一个缓存前缀。

契约测试同步:ci-changed-areas 用真实 git diff 双向断言新区域;
ci-cache-sharing 断言胶囊命令在 cross-platform 内且不再有独立缓存键;
workflow-concurrency 断言 push 范围;docs/ci-trigger-and-cache-policy 更新判定表。

* ci: 把区域判定结果打进 job 日志

跳过某平台时,事后要能一眼分清「改动够不到」和「读不到差异」。两处 detect 步
骤现在都把 `<area>=<true|false>` 打到日志,同时仍写进 GITHUB_OUTPUT。

* ci: 胶囊 job 保留但按区域门控(实测否决合并方案)

run 40510306688 的真实 step 计时:
  Windows checks 里三步 capsule = 43 + 53 + 21 = 117 s(关键路径 job,直接 +2 分钟墙钟)
  macOS checks 里三步 capsule = 40 + 41 + 6 = 87 s
独立 job 原本平均 1.4 / 0.7 分钟——它有专属 warm cache,且只编 capsule 自己。
合并省了 2 台 VM,却把 2 分钟加到最慢的 job 上,加权分钟反而更贵。

改成:native-capsule job 保持原样,新增 `capsule` 区域,只有拥有改动文件的包是
openless-capsule / openless-native-capsule 时才跑。常见 PR 因此少 2 台 VM 且不增加
任何墙钟时间;胶囊相关 PR 与今天一致。

契约测试同步:胶囊 job 必须带自己的区域门控;ci-changed-areas 场景新增 capsule 列。

* test(e2e): 修掉 switch-motion 的采样竞态(CI 14% 误报)

失败快照给出了确定答案:controlledXs 是空数组(Expected length 3 / Received
length 0 / Received array []),而同一 case 前面几条断言都过了——开关确实动了,
只是采样钩子没挂上。

原因:flip() 用 control.evaluate() 派发采样函数(在里面 addEventListener
'transitionrun'),但没有 await 就发 mouse.down()/up()。evaluate 和 click 是两条
独立的协议消息,CI 上负载高时 click 先到页面,监听还没挂上 → 抓不到 3 个受控
采样点。这就是最近 43 次 CI run 里失败 6 次(14%)、落在 5 个互不相关分支上
(含 beta 自身)的那条 `语音编辑 exposed its transform transition`。

改法(不放宽断言):
- 把「挂监听」拆成一次 await 的 evaluate,采样结果存进页面侧 WeakMap(按控件
  元素键),确认挂好后才点击;
- 采样循环仍在点击时启动、750ms 后收尾,所以 xs.at(-1) 依然是静止位置;
- 三条断言(状态翻转、滑块位移、transform 过渡跑完、3 个受控中间位置)全部
  保持原样,重挂载仍会因为 xs 为 NaN 而失败。

本地验证:webkit project 全文件 --repeat-each=6 → 30/30 通过(8.7 分钟),
即那条 flaky case 连跑 6 次全绿。tsc --noEmit 干净。

* test(e2e): 采样循环改到点击之后启动,去掉对点击延迟的依赖

上一版修掉了监听竞态,但采样循环仍在点击前就开始了 45 帧(~750ms);
如果 CI 把 click 延迟到 800ms,end 会取到动画前的位置。改成
挂监听(await)→ 点击 → 采样循环,45 帧仍覆盖 180ms 的滑动并收尾,
xs.at(-1) 恒为静止位置。

本地 webkit:全文件 5/5 通过;含 --repeat-each=4 的 20/20。

* fix(ci): keep platform gates for native consumers

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
#1245)

Ad-hoc builds still reset Accessibility / Microphone on every install, since
their cdhash-based requirement changes each build. A stable signing identity
keeps the same designated requirement, so its grants now survive reinstalls.


Claude-Session: https://claude.ai/code/session_01YJvDYNu6QoSEhJ4yGSuRqk

Co-authored-by: DragonNameLeo <15801369+DragonNameLeo@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…eed (stroke/English, rebased on Rime) (#1230)

* feat(android-ime): candidate, learning and speed improvements for stroke and English

Rebased onto the Rime pinyin backend (#1196): the LitePinyin changes
(full-pinyin phrase index, combo learning) are dropped; what remains
applies to the stroke and English keyboards and to the pinyin row's
association and candidate plumbing that Rime still uses.

Associations
- Drop the phrase the context already completes from the association
  results, so the row clears once the last association is picked.
- Clear associations (and the stroke code row) when the host empties the
  field, e.g. a chat app's send button.
- Rank associations the user picked before ahead of unpicked ones; typing
  an offered association out by hand counts as picking it (stroke, and
  Rime commits through RimePinyinController's association context).
  Replaces the stroke commit record whose key never matched an
  association lookup.

Stroke
- Mark, digit, space and Return keys drop the half-typed code, its
  segments and the code row before committing; a new input session clears
  the code row and candidates on screen too.
- Keep recording picks of a character that is on top only because of
  earlier picks, so its score stays fresh.

English
- Sentence punctuation replaces the space a candidate pick added.
- A quick double space after a word becomes ". ".
- Capitalize at the start of a sentence, line or field (not in email,
  URL, password or number fields); Shift turns an automatic capital off.

Speed
- Stroke dictionary sorted by frequency once at load; a query puts the
  user's picks first and stops at 36 matches instead of scoring and
  sorting up to ~48k entries (first stroke now 0-2 ms on a OnePlus).
- Usage stores parsed into one process-wide in-memory cache, kept current
  by a preference listener.
- Shift changes relabel the English keys in place instead of rebuilding
  the keyboard; the English/pinyin candidate row reuses its views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(android-ime): skip the field-emptied check while the draft box is open

With #1187's draft bar open, typing goes to the draft box, so an empty
host field must not clear the stroke code row or the associations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(android-ime): bind automatic edits to caret and learn split associations

Guard candidate spaces and double-space full stops with editor and selection anchors. Retain typed continuation boundaries so separately committed characters train the originally offered association. Add JVM and Android emulator regressions plus safe CI evidence collection.

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

* fix(android-ime): reset pinyin learning context on editor deletion

Invalidate confirmed association text and continuation history when Pinyin Backspace deletes editor text without an active composition. Add an asynchronous service regression for stale-context learning.

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

* test(android-ime): initialize Rime before backspace queue deadline

Complete cold dictionary deployment on the instrumentation test thread before exercising the editor fallback, matching the existing Rime integration test setup.

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

* fix(android-ime): preserve learning and capitals

---------

Co-authored-by: DepengWang <2818245+DepengWang@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: HKLHaoBin <we3q@qq.com>
Co-authored-by: Chris233 <h-chris233@outlook.com>
Tauri handles share the event loop through a non-atomic Rc
(tao EventLoopWindowTarget::runner_shared). The capsule path ran at
~30 Hz from the cpal callback and the backend event task, and on every
frame cloned AppHandle / TauriCapsuleWindow there and dropped the
closure on the main thread. The racing refcount eventually freed the
EventLoopRunner while it was running: WinDbg on a full dump shows
HEAP_CORRUPTION 0xc0000374 in Rc::drop_slow<EventLoopRunner> under
emit_capsule_payload_locked, matching the silent exits during
streaming dictation.

emit_capsule_payload_locked now keeps only the handle-free work
(epoch, Esc exclusivity, snapshot commit) on the caller and posts the
presentation through TauriCoordinatorHost::on_main_thread, which borrows
the bound AppHandle to queue the task instead of cloning it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(llm): make Gemini validation work with bare base URL and current models

- Default a bare https://generativelanguage.googleapis.com to /v1beta for
  generateContent / streamGenerateContent, matching model listing and the
  Gemini ASR path (validation previously 404'd while model listing worked).
- Retry once without thinkingConfig when the model rejects the channel-level
  thinking-off config with a 400, instead of failing the call.
- Switch the LLM / Omni default model from gemini-2.5-flash (no longer
  available to new users) to gemini-3.5-flash-lite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJvDYNu6QoSEhJ4yGSuRqk

* fix(llm): ask to turn thinking on when Gemini rejects thinking turned off

gemini-3.5-flash-lite (the Gemini channel default) answers the
thinking-off config (thinkingBudget 0) with the generic
"Request contains an invalid argument.", so the earlier retry that
looked for "thinking" in the error body never fired and validation
kept failing with a bare 400.

- On a 400 for a request that carried thinkingConfig, re-send it once
  without thinkingConfig. If that passes, fail with llmThinkingRequired
  instead of the HTTP error; any other 400 keeps its original error.
- Pass the code through validation (polish failures arrive as
  "LLM provider failed: parse error: <code>") and show it in the
  provider settings and the channel list as "turn Thinking on", in all
  eight UI languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Abbfv5hvANpC7Sku5KEXJ3

* fix(gemini): complete thinking controls and cancel probes

* test(ci): align emulator mocks with bounded shutdown

---------

Co-authored-by: DragonNameLeo <15801369+DragonNameLeo@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Chris233 <h-chris233@outlook.com>
* docs(sponsors): 更新 Mivra 赞助商信息

* docs(sponsors): 统一使用 Mivra 品牌名称
…bclass (#1267)

* fix(windows): stop trusting recycled HWNDs in the capsule hit-test subclass

`install_capsule_hit_test_proc` keys its bookkeeping on the numeric HWND, but
Windows recycles HWND values. Once a capsule/card window is destroyed and its
handle value is handed to a new window, a lookup by that key still finds the
dead window's entry, and three things go wrong:

- the fast path reuses the stale entry and returns early, so the *new* window
  never gets the subclass procedure installed -- hit-testing silently stops
  working for it;
- `WM_NCHITTEST` hit-tests the live window against the dead window's geometry;
- message forwarding can call a `previous_proc` that belonged to another window.

The only reliable identity check is whether *our own* procedure is still
installed on the handle: a recycled HWND carries whatever procedure its new
owner installed. Every read of the map now proves that first, and an entry that
fails the check is dropped and re-installed instead of trusted.

Two supporting fixes while in here:

- `install_capsule_hit_test_proc` now refuses to record itself as the
  predecessor. When the map has been wiped but the procedure is already on the
  window, `SetWindowLongPtrW` returns our own address; storing it would make
  every forwarded message recurse into this same procedure.
- `WM_NCDESTROY` only retires the entry this window owns.

Adds `reinstall_keeps_the_real_predecessor_and_never_self_wraps`, which uses a
real `HWND_MESSAGE` window because the identity check reads the live
`GWLP_WNDPROC` and cannot be mocked.

Note on running tests: `cargo test --lib` exits with `0xC0000139`
(STATUS_ENTRYPOINT_NOT_FOUND) on Windows before any test runs. The crate
imports `comctl32!TaskDialogIndirect`, which lives in Common-Controls v6, and
v6 is bound only through the `build.rs` `/manifestdependency` link arg -- the
cargo test harness links separately and does not get that manifest. Embedding a
v6 manifest in the test binary makes it run:

    mt.exe -nologo -manifest cc6.manifest "-outputresource:<test-exe>;#1"

With that, the new test passes (1 passed; 0 failed), as does `cargo check`
(0 errors) and `cargo build --release`.

* fix(windows): preserve capsule subclass chains

Track hit-test state with the window's system subclass, not the current outer WndProc. Preserve message forwarding and cleanup when another callback wraps the capsule, and keep recycled HWNDs state-free.

Run real-window regressions in the standalone backend tests with a Common Controls v6 manifest.

Refs #1267

---------

Co-authored-by: Zhongjie Li <lizhongjie1199@outlook.com>
Co-authored-by: Chris233 <h-chris233@outlook.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T06:24:07.874027Z 93704bf Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@appergb appergb added branch-retained Keep source branch while the pull request remains open needs-review Needs review labels Oct 30, 2026
@appergb

appergb commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bafb73cc9c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/android-apk.yml Outdated
Comment on lines +530 to +536
- name: Upload Android updater manifests
uses: actions/upload-artifact@v4
with:
name: openless-android-updater-manifests
if-no-files-found: error
path: |
${{ steps.stage.outputs.out_dir }}/latest-android-*.json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Publish updater manifests to the CDN before removing release assets

The workflow now removes latest-*.json from the GitHub Release and only uploads the generated files as ordinary Actions artifacts. I checked all workflows in .github/workflows; none downloads these artifacts or publishes them to dl.aeoform.com, so the CDN paths consumed by desktop and Android clients will remain missing or stale after every release and in-app updates will fail. Add an actual CDN publication step (or retain the release assets) before switching clients to these endpoints.

Useful? React with 👍 / 👎.

Comment on lines +217 to +224
const workflowOrTooling = (file) =>
file.startsWith('.github/') ||
file === 'Cargo.toml' ||
file.endsWith('/Cargo.toml') ||
file.endsWith('/Cargo.lock') ||
/(^|\/)rust-toolchain(\.toml)?$/.test(file) ||
file.startsWith('openless-all/app/contract/') ||
file.startsWith('scripts/');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify the CI impact scripts as fail-open tooling

Changes to openless-all/app/scripts/ci-impact.mjs or ci-changed-areas.sh are not matched by workflowOrTooling (which only recognizes scripts/ at the repository root), so classify falls through to the openless package and marks only the desktop/Android closures. A PR changing the classifier itself can therefore skip the frontend, Linux, and Harmony jobs even though this code controls all of their gates, hiding regressions in the gate implementation. Treat the app's scripts directory (at least these impact/gate scripts) as tooling and fail open.

Useful? React with 👍 / 👎.

@appergb

appergb commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 93704bf302

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@appergb
appergb merged commit 17a442f into main Oct 30, 2026
16 checks passed
@appergb
appergb deleted the appergb/branch-consolidation-20261030 branch October 30, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

branch-retained Keep source branch while the pull request remains open needs-review Needs review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants