Skip to content

fix(llm): omit temperature for OpenAI GPT-6 family (#1101) - #1115

Merged
H-Chris233 merged 2 commits into
Open-Less:betafrom
HKLHaoBin:fix/1101-gpt6-temperature
Sep 27, 2026
Merged

H-Chris233 merged 2 commits into
Open-Less:betafrom
HKLHaoBin:fix/1101-gpt6-temperature

Conversation

@HKLHaoBin

Copy link
Copy Markdown
Collaborator

Summary

Test plan

  • cargo test -p openless-core --locked --lib omits_temperature
  • cargo test -p openless-core --locked --lib keeps_custom_temperature_for_gpt
  • cargo test -p openless-core --locked --lib keeps_default_temperature_for_openai_non
  • Bugbot review: no bugs
  • Fork CI + Release Tauri + Android APK (debug) workflow_dispatch on this branch: all success
  • Manual: enable system proxy, polish with GPT-6 Astra — request succeeds (no 400 on temperature)

Official openai channel already skipped temperature for gpt-5* (Open-Less#857);
GPT-6 Astra/Sol/Luna reject non-default temperature the same way. Extend
the omit rule and cover display-name / prefixed model ids in unit tests.
@H-Chris233 H-Chris233 self-assigned this Sep 27, 2026
@H-Chris233
H-Chris233 merged commit 5b39599 into Open-Less:beta Sep 27, 2026
6 checks passed
aeoform added a commit to aeoform/openless that referenced this pull request Sep 28, 2026
现场:一个跑了一晚的 UI 进程 RSS 5.8 GB(VSS 7.2 GB),但 21 小时只用了
60 秒 CPU、GPU 显存 47 MiB —— 涨的是纯内存,不是渲染,也不是泄漏式的匀速增长。

成因是三段代码凑在一起:

1. 宿主 `publish_view_model()`:内容没变也**每 2 秒发一份保活快照**。
2. UI 侧收件通道是无界 `mpsc::channel()`(`UiBridgeClient::connect`)。
3. 窗口被遮挡/不可见时,eframe 会**完全跳过 egui pass**
   (eframe 0.36 `wgpu_integration.rs` 的 `if !show_ui { /* 不做任何 egui pass */ }`),
   `UiClientApp::ui()` 根本不会被调用,也就没人排空那个通道。

于是「窗口不可见」的每一秒都在堆快照:21 小时 ≈ 3.7 万份 × ~154 KB ≈ 5.8 GB,
且窗口恢复可见后要先逐份消化积压再回到正常帧,看起来就是卡死。
旁证:健康实例每 40 秒左右会写一条 `[ui-client] ipc round-trip ...`(UI 每 2 秒
一次 ping),而那个实例 21 小时里一条都没有 —— 它的 egui pass 一直没跑。

改法:快照不再走通道,只进 `UiBridgeClient` 的**单一槽位**(`LatestSnapshot`)。
旧快照本来就会被后一份覆盖,中间那些没有任何价值;窗口每帧 `take_snapshot()`
一次即可。内存因此与「窗口被遮挡多久 / 宿主发过多少份」无关。
控制帧(Ready/Hotkeys/Pong/Focus…)照旧走通道:它们由事件驱动、体积小。

回归测试 `an_undrained_window_keeps_only_the_newest_snapshot`:连发 40 份快照、
一份都不取,断言只有序号 40 留在槽位、取走后清空、通道里一句快照都没有
(把快照改回走通道时该测试立刻超时失败,已验证)。

另外这次同步把 origin/beta 的两个提交(Open-Less#1113 空格/鼠标热键、Open-Less#1115 GPT-6 温度)
合入本分支,无冲突。

package revision 120。
H-Chris233 added a commit that referenced this pull request Oct 20, 2026
* chore(linux-egui): advance the package revision to 103

Beta.3 merged as 913279c; this revision carries the review fixes (fcitx5
startup barrier, UI bridge protocol v2, field-patch settings saves, history
cache) into the deb/rpm channel.

* docs: add the Beta.3-103 device acceptance checklist

Lists the observable behaviour each review fix introduced: the fcitx5 startup
barrier and its failure/recovery path, the UI bridge handshake and reconnect,
field-patch settings saves under conflict, history delete identity and caching,
plus the previous round's parity regressions and the install/upgrade checks.

* fix(linux): stop the fcitx5 startup barrier failing on a session-managed daemon

The barrier required the `org.fcitx.Fcitx5` owner name to change after a reload,
but `restart_fcitx5_via_dbus` reported success whenever the D-Bus call was
dispatched. A compositor-launched fcitx5 (`fcitx5-wayland-launcher --reopen`)
sets `CanRestart` false and then silently ignores `Restart`, so the owner never
changed: every start burned the full 15 second budget and died in the startup
error window even though the running addon answered and hotkey registration
would have worked.

- Readiness is now the addon interface, not a replaced daemon. A reload that the
  daemon does not perform is a warning (`reload_wait`, `ReloadPlan`).
- `CanRestart` is consulted before `Restart`, and a session-managed daemon keeps
  its running addon copy instead of having a competing `fcitx5 -r` spawned next
  to it. Only a refused controller request uses the CLI fallback.
- `parse_maps_plugin_path` no longer treats the kernel's " (deleted)" suffix as
  "fcitx5 is not running", so the stale-copy case is reported and judged
  correctly.

* docs: record the session-managed fcitx5 case in the acceptance checklist

* chore(linux-egui): advance the package revision to 104

* chore(linux-egui): keep clippy quiet on the declared toolchains

The host's MSRV is 1.95 while CI lints with the current stable (1.98), and
clippy 1.95 flags these two dedupe decisions as `nonminimal_bool`. Rewriting them
as `is_none_or` (already the shape used in the history cache) is equivalent —
signal/local is None, or the stamp is outside the dedupe window — and is clean on
both toolchains. Package revision advances to 105 for the rebuilt deb/rpm.

* fix(linux-egui): degrade the UI snapshot instead of blinking the window

The bridge refused any snapshot over 16 MiB by dropping the window connection.
The window treats a dead host as "the host is gone" and exits successfully, and
the host only records a window error on a non-zero exit — so an oversized view
model turned into: connection dropped, window exits, host respawns after 800 ms,
same oversized snapshot, forever. The window blinked about once a second and the
app was effectively unusable (it takes ~14k entries at the current average
transcript length to get there, but history has no entry cap by default).

Now the host trims the oldest history before sending: a ladder of 4000/1000/200/0
entries, newest first, until the payload fits a 12 MiB soft limit. The history
list is refilled from the real cache every tick, so trimming only affects the
frame being sent, and both sides keep indexing the same list. The count of hidden
entries travels in `FrontendViewModel::history_truncated` and the history page
paints a warning with that count plus the setting that actually shrinks history.
If even the empty history does not fit, the frame is skipped and the connection is
kept: the window keeps showing the last good state instead of dying in a loop.

`OPENLESS_UI_SNAPSHOT_LIMIT=<bytes>` lowers the soft limit so the degraded path
can be exercised on a device without accumulating 12 MiB of history.

Verified: with the limit forced to 60 kB the host logged the trim once, the window
process was spawned exactly once and never exited, and steady-state publishing
stayed at zero frames (the fingerprint gate still works). Package revision 106.

* feat(linux-egui): page large frontend collections

* ci: align Linux egui workflow name

* fix: degrade gracefully when fcitx5 is unavailable

* chore: remove unused Linux desktop helpers

* fix: keep Linux popup panels inside X11 work area

* fix: match Linux capsule styles to Tauri geometry

* fix: keep typeless capsule at the OpenLess size

* refactor: remove Linux egui OpenGL rendering

* fix(linux-egui): remove standalone startup error window

* fix(linux-egui): pad Siri WGPU uniform buffer

* fix(linux-egui): restore transparent Siri rendering and previews

* fix(linux-egui): match Siri stage size and spectral colors

* fix(linux-egui): animate Siri preview and show thinking label

* perf(linux-egui): stop wgpu reserving ~180 MiB of VRAM per process

wgpu 28 把 `MemoryHints::Performance` 定为默认值,于是 wgpu-hal 的 Vulkan 子分配器
按 device 128–256 MB 的内存块向驱动申请。每个渲染进程(主窗、胶囊、每个弹窗)都为
此付出固定代价:建 device 后显存 195 MiB,且与窗口大小、UI 内容无关 —— 460×180 的
胶囊要 197 MiB,而裸 Vulkan(vkcube,含交换链)只有 5 MiB。

现在 eframe 窗口与原生 layer-shell 胶囊共用一份设备描述,统一改要
`MemoryHints::MemoryUsage`(8–64 MB 块)。

本机实测(GTX 1660 SUPER / NVIDIA 615.71.09 / 1920×1080@120):
  胶囊 460×180,Siri 录音态:      198 MiB → 18 MiB,29.8 fps 不变
  设置面板打开(背板 4×MSAA + 模糊 + 动画预览):
                                   209 MiB → 47 MiB,72.7 fps 不变

顺带加入 `frame_stats`:`OPENLESS_FPS_LOG=1` 每秒写一行帧统计,
`OPENLESS_FPS_SETTINGS=1` 启动即打开设置面板,用来量上面这两组数字。

* fix(linux-egui): assert the Vulkan runtime deps in the release contract

WGPU 迁移把包依赖从 libegl1 / libwayland-egl1(rpm: libglvnd-egl /
libwayland-egl.so.1)换成了 libvulkan1(rpm: vulkan-loader)+ Recommends
mesa-vulkan-drivers,但 linux-egui-release-contract.test.mjs 还在断言旧的 EGL
依赖,于是 CI 的 macOS/Windows 契约步骤从迁移那次提交起就红:

  AssertionError: deb must require libegl1
  at scripts/linux-egui-release-contract.test.mjs:98

现在断言 Vulkan loader 在 deb/rpm 依赖里、EGL 依赖不再回来(回来了说明 GL 渲染
路径又被引回来),并断言 Recommends 里有 Vulkan 驱动。

顺带更新 docs/linux-egui-dependency-upgrade.md:layer-shell 胶囊已经走
wgpu/Vulkan,「仍存在的 GL 路径」一节以及验收清单里「该 surface 仍走 GL」的说法
都过期了;补上 MemoryHints::MemoryUsage 的显存结论。

* fix(linux-egui): keep the device descriptor test clippy-clean

`cargo clippy -p openless-linux-egui --all-targets -- -D warnings` 会把
`assert!(MAX_TEXTURE_DIMENSION_2D >= 3840)` 判成 assertions_on_constants。
挪进 const 块,仍然是编译期拦截「把 2D 纹理上限调小」的改动。

* fix(core): prune history with the injected clock, not the wall clock

历史保留用的是两套时间:`created_at` 由注入的 `Clock` 生成,而保留截止
(`retain_with_policy`)和「最近 N 分钟」(`recent_within_minutes`)自己读
`chrono::Utc::now()`。注入固定时钟的后端(测试、回放)因此在真实时间越过
「固定时间 + retention」之后,会把刚写入的条目在写入瞬间判成过期删掉:

  api::tests::completed_dictation_persists_history_and_activity_from_the_session_snapshot
  assertion `left == right` failed  left: 0  right: 1   (api.rs:11048)

本 PR 12:13Z 那次 CI 还是绿的,12:55Z 起每次都红在同一行 —— 这个测试注入
2026-08-28T12:34:56Z、retention 30 天,真实时间在 2026-09-27T12:34:56Z 越线。

改法:`HistoryStore` 持有唯一时钟来源,后端组装时用 `attach_clock` 把注入的
`Clock` 交下去;保留裁剪与 `recent_within_minutes` 都改用它。功能、默认值
(7 天)、`0 = 不按天数清理`、只在写入时裁剪的行为一律不变;生产注入的本来
就是 `SystemClock`,行为零变化。

回归测试 `retention_follows_the_injected_clock_instead_of_the_wall_clock`
(固定时钟 + 30 天保留:新条目留住、超期条目照删;把 cutoff 换回
`Utc::now()` 时该测试立刻失败,已验证)。

package revision 118。

* fix(linux-egui): bound the UI snapshot inbox, stop the 5.8 GB freeze

现场:一个跑了一晚的 UI 进程 RSS 5.8 GB(VSS 7.2 GB),但 21 小时只用了
60 秒 CPU、GPU 显存 47 MiB —— 涨的是纯内存,不是渲染,也不是泄漏式的匀速增长。

成因是三段代码凑在一起:

1. 宿主 `publish_view_model()`:内容没变也**每 2 秒发一份保活快照**。
2. UI 侧收件通道是无界 `mpsc::channel()`(`UiBridgeClient::connect`)。
3. 窗口被遮挡/不可见时,eframe 会**完全跳过 egui pass**
   (eframe 0.36 `wgpu_integration.rs` 的 `if !show_ui { /* 不做任何 egui pass */ }`),
   `UiClientApp::ui()` 根本不会被调用,也就没人排空那个通道。

于是「窗口不可见」的每一秒都在堆快照:21 小时 ≈ 3.7 万份 × ~154 KB ≈ 5.8 GB,
且窗口恢复可见后要先逐份消化积压再回到正常帧,看起来就是卡死。
旁证:健康实例每 40 秒左右会写一条 `[ui-client] ipc round-trip ...`(UI 每 2 秒
一次 ping),而那个实例 21 小时里一条都没有 —— 它的 egui pass 一直没跑。

改法:快照不再走通道,只进 `UiBridgeClient` 的**单一槽位**(`LatestSnapshot`)。
旧快照本来就会被后一份覆盖,中间那些没有任何价值;窗口每帧 `take_snapshot()`
一次即可。内存因此与「窗口被遮挡多久 / 宿主发过多少份」无关。
控制帧(Ready/Hotkeys/Pong/Focus…)照旧走通道:它们由事件驱动、体积小。

回归测试 `an_undrained_window_keeps_only_the_newest_snapshot`:连发 40 份快照、
一份都不取,断言只有序号 40 留在槽位、取走后清空、通道里一句快照都没有
(把快照改回走通道时该测试立刻超时失败,已验证)。

另外这次同步把 origin/beta 的两个提交(#1113 空格/鼠标热键、#1115 GPT-6 温度)
合入本分支,无冲突。

package revision 120。

* feat(linux-egui): match the Tauri AI-services page

以 Tauri(`src/pages/settings/`)为唯一标准,把「AI 服务与模型 → 语言模型」
这一页补到对齐。用户在 Linux/Windows 两边截图对比后指出「差的有点多」。

新增(Tauri 有、我们以前没有):

1. **识别管线**(`ProvidersSection` 的 pipelineMode 行):`识别管线 ⓘ` +
   `[传统模式][多模态模式]` 分段控件 + 「两种模式使用完全独立的凭据配置…」
   说明。只在实验性开关 `multimodalPipelineEnabled` 打开后出现 —— 与 Tauri 的
   `if (prefs?.multimodalPipelineEnabled !== true) return null` 同条件。
   切换只改偏好(`pipeline_mode`),两套凭据都留在凭据库里。

2. **渠道卡片**(`ChannelList` 的行)改成 Tauri 的结构:
   - 圆角 12 卡片,仅「当前使用」的渠道有中性灰底 + 0.5px 描边(Tauri 特意
     不用蓝底竖条);第二行 `provider · model`(模型等宽),名字为空时标题
     回退成供应商名(`channel.name.trim() || providerLabel`)。
   - 状态块:`上次验证` + 通过(+耗时)/ 失败(+错误 + 「验证失败不会自动停用…」)
     / 尚未验证,再跟「多久以前」,超过 24 小时追加「结果已超过 24 小时」。
   - 右侧只留「重新验证 / 启用 / 编辑 ›」(编辑按钮带铅笔与右尖括号图标)。
   - 换顺序靠左侧拖拽手柄(新图标 `Grip`,走 lucide 同步管道),不再用 ↑↓。
   - 点「验证」后按钮立刻变「正在验证…」并禁用,结果回包或超时后恢复
     (Tauri 的 `testingIds` 也是界面本地状态)。
   - 供应商选择与「删除渠道」(两步确认)移进编辑面板 —— Tauri 把它们放在
     ChannelModal 里,卡片上不出现;卡片下方保留「凭据保存在系统凭据库中。」

数据管道:`SettingsChannel` 补 `id`,并把原来宿主预格式化的 `last_check` 字符串
换成结构化字段(`last_ok` / `last_error` / `last_latency_ms` /
`last_check_age_seconds`),本地化仍全部走 i18n 目录。

文案:新增 15 个 key(识别管线 5 个、渠道状态 10 个),值取自 Tauri 五语目录,
`sync-egui-i18n.mjs` 报告全部 in sync。

测试:`the_pipeline_mode_row_only_shows_with_the_experimental_switch`、
`a_failed_channel_card_explains_the_failure_and_the_stale_result`、
`a_verify_click_marks_the_channel_as_testing_until_the_result_arrives`;
识别管线那条做过负向验证(去掉开关判断即失败)。

package revision 121。

* feat(linux-egui): embedded channel editor, flush settings rail, right links

三处对齐 Tauri(用户逐项指出):

1. **渠道编辑器改成右栏覆盖**(`ChannelModal` 的 embedded 形态)。
   之前是卡片下方展开的内嵌面板,后来我先做成了全窗口遮罩 + 居中卡片,用户
   指出「应该是在右侧覆盖,不是独立的覆盖层」——Tauri 桌面端确实是
   `createPortal` 进右侧 `.ol-settings-content-pane`,用
   `.ol-channel-dialog-embedded { position: absolute; inset: 0; background:
   var(--ol-sidebar-bg) }` 整栏盖住。现在照此实现:
   - 头部:返回按钮(`chevLeft`,新图标走 lucide 同步管道)+ 「编辑渠道」+
     渠道种类徽章(文字处理 / 语音识别)+ 自动保存说明;
   - 中部滚动区:白底圆角表单卡(`服务连接` → 供应商 → 渠道名称(可选)+ 说明
     → 凭据 → 模型),字段照 Tauri 是整列宽、38px 高(`layout::text_input_sized`);
   - 底部:左侧「删除渠道」(垃圾桶图标 + 两步确认),右侧自动保存提示 +
     保存字段/清除 Secret + 「完成」(蓝色主按钮)。
   `SettingsProviderEditor` 增 `is_asr`(宿主 `ProviderEditorForm.kind`)决定徽章。

2. **设置弹窗左栏几何**:底色以前画在「内容高度」的 Frame 上,内容短时下半截
   露出内容区底色;内容宽度又比 Tauri 少了 24px(选项 170 vs 190)。改为按整栏
   高度铺底色 + 右侧 0.5px 分隔线(Tauri 的 `borderRight`),内容走 12/16 内边距,
   选项与「帮助中心」上方的分隔线因此与 Tauri 同宽同位置;不再用 `ui.separator()`
   以免它自己的宽度把右栏挤窄。

3. **外链指错了组织**:帮助中心/发布日志/GitHub/反馈都指向
   `github.com/earendil-works/openless`,Tauri 用的是 `Open-Less/openless`
   (帮助中心还要带 `#readme`)。抽成常量并加回归测试。

测试:`the_channel_editor_covers_the_content_column_with_tauri_chrome`(断言编辑器
画出编辑渠道/服务连接/渠道名称/删除渠道/完成,且设置页正文不透过)、
`settings_links_match_the_tauri_targets`;i18n 新增 6 个键(值取自 Tauri 五语目录)。

package revision 122。

* feat(linux-egui): auto-save the channel editor and add channels through it

按 Tauri 补齐两处行为(用户要求「都要同步」):

1. **字段修改后自动保存**(Tauri `CredentialInput` 的 300ms 防抖 + 失焦/离开前
   flush)。UI 侧记一个「防抖截止时刻」在 egui 临时数据里:任一字段(名称、
   Endpoint、模型、Resource ID、Auth 模式、密钥、供应商切换、模型预设)改动即标脏,
   到点推一次 `SettingsProviderSave`;`完成`/返回先 flush 再关。
   因此底部去掉「保存字段/Secret」按钮,只剩「清除 Secret」与「完成」。
   ——保留「清除 Secret」是有意的差异:Tauri 的密钥字段会读回已存值(清空输入框
   即删除),而我们的密钥草稿始终是只写的(已存密钥不进 UI 进程),没有这个按钮就
   无法擦除已保存的密钥。若要完全对齐,需要把密钥读回界面,安全性上是降级的。

2. **「添加渠道」走同一个编辑器**(Tauri `startCreate`):点按钮先在后台建一张空
   渠道(名字留空、供应商取预设第一项),宿主 `ChannelCreated` 后自动打开编辑器并
   标记为草稿 → 标题是「添加渠道」;用户完全没交互就关闭时把这张空渠道回收掉
   (Tauri `shouldRecycleDraft` + `deleteChannelIfBlank`)。原来的内嵌小表单
   (`add_channel_form` + `SettingsChannelFormOpen/Provider/Name/Create` 四个 action
   与三个 VM 字段)整块删除。

测试:`editor_auto_save_debounces_and_flushes_before_closing`(防抖窗口内不保存、
到点只保存一次、关闭前 flush、干净关闭不保存)、右栏覆盖测试新增草稿标题断言、
`typed_text_survives...` 里那段内嵌表单断言改成编辑器字段断言。

package revision 123。

* fix(linux-egui): the channel editor must not hijack the settings sheet

用户报「打开以后只能显示模型编辑页面,不能改其他设置了」——上一条把渠道编辑器改成
右栏整栏覆盖时漏了两道闸,两处都补上:

1. **别自动弹编辑器**。面板加载后宿主会为「选中渠道」把编辑器拉起来(`selected_channel_id`
   的兜底是 active provider 或第一个渠道),这在编辑器以内嵌小面板呈现时无害,改成
   整栏覆盖后就变成「一进设置右栏永远是模型编辑器」。新增 `editor_requested`:只有用户
   点了「编辑」(`SettingsChannelSelect`)或刚建出草稿(`ChannelCreated`)才允许打开,
   面板刷新(保存 / 验证 / 切页 / 保活)不再顺手打开。

2. **覆盖范围跟着分区走**。`channel_editor_panel` 现在只在「AI 服务与模型」这一页替代
   `panel()`;切到别的分区时宿主还会主动收起编辑器(Tauri 的 modal 宿主挂在服务页里,
   切页即卸载),未交互的草稿照旧回收。

顺带修好 QA 开关 `OPENLESS_FPS_SETTINGS=1`:它以前只在窗口初始化时置一次
`settings_open`,随后被宿主快照覆盖,等于失效;改成每帧采纳快照之后再压一次。

测试:`the_channel_editor_never_covers_other_settings_sections`(通用/外观/关于三个
分区开着编辑器也不许出现「编辑渠道」)。

对照用户数据确认:这次误判没有造成删除——`credential-metadata.json` 仍列出
`llm: deepseek`,mtime 未变,日志无删除记录。

package revision 124。

* feat(linux-egui): 思考光点的转速跟着流式输出速率走

用户诉求:「润色时能不能根据大模型的文字输出速度转圈」。审计结论:Tauri 版没有这个反馈
(`VoiceOrbStage` 只按状态给固定 speed:thinking 1.5 / 收尾 1.0;`PolishDelta` 在
`tauri_events.rs` 里甚至被显式忽略,胶囊 payload 也没有任何速率字段),所以这里按用户
要求把它做成一个**两侧都没有过的新行为**,但基线刻意对齐 Tauri。

- 新增 `ui/frontend/stream_rate.rs`:
  · `StreamRate`:宿主侧把流式增量的字符数按 0.5s 滑动窗口折算成「字符/秒」;
  · `OrbSpeed`:胶囊侧接住宿主下发的速率、只在速率变化时刷新时间戳,然后本地按 0.45s
    时间常数衰减 —— 模型吐字快就转快,网关卡住/模型想事情就慢慢回到基线,不需要宿主发心跳;
  · `speed_for_rate`:4 字/秒以下不加成、60 字/秒到顶,映射到 [1.5, 2.6] 转/秒。
    **没有速率数据时正好是 Tauri 的 1.5**,所以默认观感与 Tauri 一致。
- 宿主(`main.rs`):新增 `stream_rate` 计量。非 final 的 `PolishDelta` 记增量字符数,
  final 时清空样本(流结束立刻回落基线);新会话(`Starting`)也清空。胶囊消息在
  `HostToPopup::Capsule` 里多带一个 `stream_rate`(可选字段,老宿主缺省即基线)。
- 胶囊窗口(`popups.rs`):orb 的 `SiriDrive.speed` 不再写死 1.3(顺带修掉与 Tauri 1.5 的
  漂移),改由 `OrbSpeed` 逐帧给出;离开思考态复位。

测试:`stream_rate` 模块 8 条(窗口折算、过期、基线、clamp、卡住后衰减、恢复后重爬)+
`the_orb_spins_faster_while_the_model_streams`(走真实渲染路径,用 Siri 时钟推进量比较
「无速率」与「60 字/秒」)+ `capsule_carries_translation_active_and_style` 覆盖协议字段。

package revision 125。

* feat(linux-egui): 撤掉转速功能,逐项对齐 Siri 胶囊的入场/收尾节奏

用户拍板「Tauri 为准」:先撤掉上一条把思考光点转速接到流式输出速率的功能
(revert 2c0d28b,只保留 1.3→1.5 的漂移修复),然后把审计出的六处差异全部对齐 Tauri。

- **扫光**:经典/typeless 的「思考中」文案现在有 `cap-shine` 扫光(同一段文字在高光带内
  用蓝色重绘一遍,三层由窄到宽),进入转写/润色头 2 秒走 0.9s 快周期、之后回落 2.4s。
- **预测式展开**:宿主量「预备 → 就绪」耗时(Tauri 的 `warmupMs`)并按同样的
  `clamp(prev*0.7 + observed*0.3, 60, 600)`、同样的 <20ms/>3s 过滤学习,落在 UI 状态文档
  (`capsule_warmup_ms`);`siri_wgpu::tick` 用它算 `warmProgress`(未就绪 cap 0.9,就绪后
  k=9 补满),光条从收拢态按这个节奏展开。
- **圆点出场 / 收尾**:`gather` 改由时钟驱动 —— 出场全聚圆心 → hold 0.3s → 1.6/s 散开成环;
  终态 `merging` 时 4/s 合回中央一颗圆,转速回落 1.0。终态胶囊因此也要画光点(以前不画)。
- **交叉淡出**:切到思考态后 wave 继续画 —— 先保持 0.55s、再用 0.6s ease-out 淡出(预乘
  alpha 的 `opacity`,新增 `SiriEffect::with_opacity`,WGSL 用 `tint.a` 同时缩 RGB 与 A)。
- **Siri 舞台不再有文案**(Tauri `VoiceOrbStage` 只有 error 才给字):删掉 orb 下方的
  「思考中」白底标签与「已插入 N 字」;错误时按 Tauri 的 `errorGlowTextStyle` 画底部红字
  药丸,文案优先用宿主给的 `message`。
- **新会话复位**:两个窗口进程常驻,Tauri 靠组件重新挂载拿到的「入场从头走」现在由
  相位时钟算出并显式 `siri_wgpu::reset`。

测试:`capsule_motion` 6 条(wave 淡出曲线、相位会话边界、扫光快/慢周期)+
`siri_wgpu` 4 条(预测式展开、gather 生命周期、reset 重放入场、opacity 写入 tint.a)+
`ui_state` 2 条(学习上界与 EMA 收敛)+ 界面级 3 条(扫光颜色、wave→orb 双回调、
Siri 只在出错时显示宿主的 messenger 文案);另修正 3 条既有测试的期望(终态 Siri 仍画光点、
Siri 无文案、已插入字数只在经典药丸)。

package revision 126。

* test(linux-egui): 把「窗口不排空 + 保活快照」的 5.8 GB 事故钉成回归测试

现场:窗口被遮挡时 eframe 完全跳过 egui pass(`if !show_ui`),`ui()` 一次都不跑,
`take_snapshot`/`try_recv` 也就没人调;宿主仍每 2 秒发一份保活快照。旧代码把快照塞进
无界通道 —— 21 小时≈3.7 万份全留在内存里(系统账本:21.1 小时那次会话峰值 5.70 GB,
折算 78.8 KB/s;同长度会话另有 0.96/1.30 GB 两个),窗口恢复可见后还要逐份消化,表现
为卡死。单槽位 `LatestSnapshot`(rev 120)修掉了语义,但没有任何测试能证明「内存不再
跟着份数涨」—— 原来那条只断言槽位里只有一份。

新测试 `an_undrained_window_survives_hours_of_keepalive_snapshots` 直接量 RSS:拿裸
socket 当宿主(绕过宿主侧合并槽这另一半修复),连发 1000 份 32 KiB 快照、一份不取,
只在等待「最新一份」可用时顺便断言驻留内存不超过 8 MiB。

验证过它能红:把读线程改回「进无界通道」,0.73 秒即失败
(`300 sent, RSS grew 10 MiB`);装回单槽位后 1.9 秒通过。踩到的两个坑写在注释里 ——
等待 `take_snapshot` 会让旧代码先以超时失败、掩盖内存断言;突发式发送会被宿主侧合并槽
吞掉中间帧,也复现不出客户端堆积。

* fix(linux-egui): 首选麦克风不再把音响/HDMI 监听列进来

用户报障「麦克风选择变成了音响选择」。本机实测 `cpal::input_devices()` 给出 8 个节点,
设置页的「首选麦克风」里排着 `TU116 High Definition Audio Controller Pro`(显卡 HDMI
输出的监听)、`内置音频 Pro`(内置扬声器的监听),而真实采集源 `内置音频 Pro 2` 反而在
后面 —— 选中监听就是录到扬声器回环(静音或系统声音),转写自然出不来东西。

成因:PipeWire 给每个 sink 都造一个 monitor source;Pulse 给它的描述是
`Monitor of <sink 描述>`,而 cpal 的 Pulse 后端把 `Monitor of ` 前缀去掉后原样当成设备
名。`pactl list sources` 的 monitor 项带 `Monitor of Sink:` 字段,据此把描述精确还原成
cpal 会显示的名字即可剔除,不需要猜命名规律 —— 真实采集源的描述不带前缀,永远不会被误删。

- `pulse_sources()` 读 `pactl list sources` + `get-default-source`;判定合到纯函数
  `keep_microphone()` 里,`enumerate_microphones()` 只负责 cpal 侧枚举。
- 顺手修 `is_default`:`cpal::default_input_device()` 拿到的是 PipeWire 的 `default_input`
  合成节点,永远不等于真实设备名,所以「默认」标记此前从来没标上过;改用 pulse 的默认源。
- `pactl` 缺失/失败时返回空值、退回旧行为:宁可多列,也不误删用户的麦克风。
- 以前误选过监听的名字不会再匹配到任何设备,`audio.rs` 会回落到系统默认麦克风并记一条
  warning —— 升级后不用手动改,录音即恢复。

真机核对:修复后列表只剩 `内置音频 Pro 2`(is_default=true),5 个监听全部消失。
测试:`monitor_sources_are_not_offered_as_microphones`(按现场 pactl 片段核对剔除集与
默认源)、`real_capture_sources_survive_the_monitor_filter`(现场 8 个节点逐个核对)、
`without_pactl_no_device_is_dropped`。

package revision 127。

* chore(linux-egui): 跟上 Beta.4 的版本线与文案

merge origin/beta(`6f9af895`,零冲突)后:
- `package-revision` → 128,包名进到 2.0.0-Beta.4-128;
- `sync-egui-i18n.mjs` 同步 4 条上游改过的文案(settings.selection_workspace.hint、
  vocab.filter_auto、modal.descriptions.advanced、marketplace.upload.hint)。

注意 `vocab.filter_auto`:上游这版是「确认收集」(Beta.4 的 confirmation-based vocabulary
learning),egui 侧目前还是「自动添加」的行为 —— 标签已对齐,**行为还没对齐**,单独跟进。

* feat(linux-egui): 对齐 Beta.4 的确认式词库学习(胶囊里的「要记住这个词吗」卡片)

上游 Beta.4 把学词改成了「确认收集」:手改产生的候选只进 Core 的待确认队列,用户点
「记住」才落库。Linux egui 侧完全没有这个确认入口 —— 事件被忽略、面板也不画,结果是
**自动学词永远落不了库**(候选到期自己消失),词库页的「确认收集」分段因此永远是空的。

按 Tauri `VocabSuggestionCard` 补齐:

- 协议:`HostToPopup::Capsule` 增 `suggestions`(`CapsuleSuggestion{id,pattern,
  replacement}`,`serde(default)` 兼容旧宿主);新增 `PopupToHost::VocabSuggestion{id,
  accept}` 承载一次「记住 / 不用」。
- 胶囊:有候选时整窗只画卡片 —— 标题「要记住这个词吗?」+ 每行「改前 → 改后」+ 两颗
  28×28 圆钮(Check/Close,与胶囊确认/取消同一套画法),几何抽成 `vocab_card_layout`
  纯函数(行多到装不下时压行高并缩按钮,保证 5 条上限内不重叠、不出舞台)。
- 宿主:`VocabularySuggestionsChanged` → 重算并顶卡;卡片只在 `vocab_card_allowed`(没有
  会话)时出现 —— 卡片与录音胶囊共用一个窗口,会话在飞时必须让路(照 Tauri 那道闸);
  会话一开始就丢掉候选;过期候选每秒重算自己消失(Core 只按需清理、不发事件)。
- i18n:`vocabCard.title/accept/reject` 三键五语,值与 Tauri 目录逐字一致。

测试:卡片渲染 + 点中「记住/不用」各自带回对应候选 id、5 条满卡不重叠不出界、候选
经协议落地且旧宿主无 `suggestions` 仍可解析、闸门只放 Idle。

package revision 129。

* [pi] Work in progress

* [pi] Work in progress

* fix(linux-egui): wire Omni settings and correct tray submenu protocol

* ci: reuse Linux TLS build outputs and cache Playwright browsers

* fix(ci): align UI assertions and repair cross-platform dependency boundaries

* ci: make PR caches read-only and maintain shared caches on beta

* fix(ci): satisfy tray activation clippy guard

* fix(linux-egui): restore capsule pointer actions and sequence Siri contraction

* fix(linux-egui): handle native touch and smooth Siri point handoff

* revert(linux-egui): back out the last two Siri animation changes

* fix(linux-egui): rebuild Siri morph on a single timeline and fluid distance field

* fix(ci): keep Core SHA-256 API compatible with current beta

* fix(linux-egui): honor capsule repaint deadlines and FIFO pacing

* fix(ci): isolate keepalive RSS regression from parallel UI allocations

* fix(linux-egui): tailor permissions and data settings to Linux session services

* fix(linux-egui): verify keyring and network state and left-align privacy settings

* fix(linux-egui): keep new provider editors open and order model and channel mutations

* fix(linux-egui): keep QA panel actions alive and pass the public-surface gate

Port the still-relevant parts of #1136: empty-state QA turns submit through Core context, failures bind to the window generation, closing the panel ends its process, and background QA hides cannot close the current polish preview. Also render Linux modifier labels as the keys actually registered and assert every settings section tessellates to finite vertices.

The provider fixture used a "/v1" URL that the Linux public-surface gate forbids in the UI crate; keep provider defaults owned by Core descriptors only.

* fix(linux-egui): preserve vault identity and align channel drafts with Tauri

* fix(linux-egui): keep provider drafts and skip list loading state on refresh

* feat(linux-egui): port the encrypted cloud sync card and reading typography

对齐 Tauri 的两块缺失 UI(beta 上已有、Linux 端完全没有):

- 隐私 → 云端同步:状态/开关/登录/解锁/同步/恢复(含冲突逐条选边)/改密码/
  删除远端/自定义服务器 + 同意与协议两屏,全部走 Core 的
  cloud_sync_e2ee_* 接口;事件(state/conflict/restored)也接进 UI。
- 外观 → 排版:字族(7 种,走 fontconfig)、字号(90/100/110%,
  egui zoom factor)、阅读宽度(收窄并居中主内容列)。
- Beta 通道按 Tauri 规则在 Linux 上不出现(supportsAutoUpdate=false)。

* fix(ci): refresh the Tauri lockfiles after the beta merge

合并时整份取了 beta 的 Cargo.lock,但合并后的 Cargo.toml 是两边并集
(Core 走的是本分支的依赖升级),CI 的 --locked 因此全部失败:
macOS/Windows/Android/Linux 都报 "cannot update the lock file"。
这里用最小更新重建两个 lock:只补并入所需的条目,不整体抬版本。

* [pi] Work in progress

* feat(linux-egui): render settings as a full page, and pin aes for the 1.88 MSRV

- 设置不再是居中悬浮卡片:去掉磨砂遮罩与落影,整页铺满内容区
  (左栏底色与右栏内容贴边),对话框(云同步)仍是模态。
- 两个窗口尺寸回归测试改成断言「整页贴边」,不再断言居中。
- aes 0.9.3 要求 rustc 1.89,CI 的 MSRV job 是 1.88:钉回 0.9.2。

* chore(linux-egui): bump package revision to 106

* feat(linux-egui): match the new shortcuts page and settings header

按 Tauri beta 4+ 的 SettingsModal / ShortcutsSection 对齐:

- 快捷键页改成「搜索 + 计数 / 分类与分配状态筛选 / 命令行」:
  命令行左侧标签(+说明),右侧内联录制器(键帽 / ↻ 重置 / ✕ 清空),
  不再用 chevron 菜单;补上划词润色行、只读 Esc 行与「+ 添加风格快捷键」。
- 新增 ShortcutReset:默认绑定取 UserPreferences::default(),不另抄键位。
- 关闭设置从右栏顶栏的 ✕ 移到左栏顶部「‹ 返回应用」(Tauri ol-settings-return),
  右栏顶栏只留「修改后自动保存」。
- 设置整页恢复 14px 圆角(左栏左侧圆角、内容区整体圆角)。

* chore(linux-egui): bump package revision to 107

* fix(linux-egui): translate the shortcut filters, order keycaps, inset the sheet

截图里暴露的三个问题:

- 分类/分配状态两组筛选的文案直接显示成 key:这些 key 是以数组字面量传进
  filter_group 的,i18n 同步脚本扫不到。改成 tr_l10n 直调后补齐 7 条翻译。
- 键帽顺序反了(A Alt):改为左→右、修饰键在前(Alt A),与 Tauri KbdGroup 一致。
- 只读 Esc 行错位(走了 200px 标签列):换成与命令行同构的左标签/右键帽行。
- 设置页恢复 14px 圆角:整页保留 8px 内缩,圆角才不会被窗口边切掉。

* fix(linux-egui): make the shortcut filters local, box the recorder, drop the white edge

截图里的四个问题:

- 连点筛选卡顿:筛选与搜索词原本走 FrontendAction 回宿主再整份快照推回,
  现在改成 egui 自己的记忆(纯 UI 状态),点一下即时生效;同时删掉因此
  不再使用的三个动作和三个视图模型字段。
- 键帽/按钮没有边框:键帽按 Tauri Kbd 重画(21 高、r5、0.5px 描边 + 底边阴影线),
  ↻/✕ 改成有描边的图标按钮(0.5px / r6 / 悬停填色)。
- 搜索框样式不对:按 .ol-settings-search-field 重做(36 高、1px 描边、r8、13px)。
- 上/右多出一条白边:设置页不再内缩 8px(那是为了让圆角露出来),贴满内容区;
  圆角只留在左栏左上/左下,底色由 CONTENT_BG 填充,不再透出白边。
- 命令行结构简化成 Tauri 的两列(标签 / max 260 的录制器)+ 一条细分割线,
  去掉了多余的一层套一层的布局。

* fix(linux-egui): make the filters actually click, white content pane, rounder text and icons

- 「点全部没反应」的真因:分类与分配状态两组各有一个「全部」,筛选按钮的 id
  只按文字区分 → 两个按钮抢同一个 id(日志里的 Second use of widget ID),
  点击落到另一组;再加上对同一矩形二次 interact,点击会被丢掉。改用
  push_id(分组, 选项) + allocate 自带的 response,并补一条真实的点击回归测试。
- 内容区改白底:左栏灰(rail-bg)、右栏白(surface)。之前整页统一 CONTENT_BG,
  白底上的灰色选中块看起来像灰底。
- 字号与图标更圆润:Proportional 字体链最前面插入 Medium 面(Tauri 是 500 字重,
  Regular 的 Noto Sans CJK 在 11–13px 又细又尖);Lucide 图标 alpha 做 0.75 次幂,
  补回缩到 13px 后丢失的笔画厚度。

* fix(linux-egui): right-align the shortcut recorder with explicit rects

allocate_ui_with_layout + Layout::top_down 会按内容收缩:标签列只占文字宽度,
录制器(键帽 + 重置 / 清空)因此停在中途,右边缘空出一大截。

改成显式矩形布局:一行切成「标签区 / 20px 间距 / 260px 录制器区」,
两个子 Ui 用 new_child(max_rect) 定位,录制器贴内容区右边缘。
风格包行与只读 Esc 行同样处理。

补一条回归测试:录制器右边缘必须与行右边缘重合,且宽度上限 260。

* feat(linux-egui): click-outside cancels recording; inline recorder everywhere

- 录制中点击别处即取消:录制面板把矩形记进 egui memory,主循环在"武装"一帧后
  判断按下位置是否落在面板外,是则 ShortcutRecording(None)(Tauri 的 Popover
  点外部关闭)。
- 录音与输入页的主快捷键、选区助手的润色键、速记页的快捷键卡片都换成同一套
  内联录制器(键帽 + 重置 / 清空),不再用 chevron 菜单;顺带删掉
  ShortcutMenu 动作、shortcut_menu 字段与 chevron 绘制、ButtonKind::BlueSoft。
- 无标签的紧凑行(速记卡片)用 44px 行高,避免撑出卡片可视区。

测试:点击外部取消录制、内联录制器的键帽顺序、速记卡片不再溢底。

* fix(linux-egui): stop the typography card from painting raw i18n keys

「字体与排版」的字体系列 / 阅读宽度下拉把 `uiRefinement.native`、`.comfortable`
原样画了出来:标签是 `UiFont::label_key()` 动态取到的,而 i18n 同步脚本只扫
`tr_l10n(lang, "字面量")`,这几个 key 从没进过 egui 目录,`tr_l10n` 于是回吐 key。

- 标签改成字面量调用;字体名与 Tauri 的 FontCatalog 对齐(只有「系统默认」本地化,
  其余显示字体本名)。
- 删掉不再使用的 `label_key()`。
- 补 4 个 key 到目录,并加一条「设置页任何分区都不得画出 i18n key」的回归测试
  (扫绘制出来的文字,点号后带大写字母即判定为 key)。

* feat(linux-egui): align the recording & input page with the Tauri client

逐行对照 Tauri `RecordingInputSection` / `SelectionWorkspaceSection`:

- 行序改成与 Tauri 一致:快捷键 → 录音方式 → 静音自动停止 → 麦克风 →
  音频增强 → 胶囊 → 胶囊样式/字幕 → 稳定模式 → 录音期间静音 → 提示音。
  (音频增强原先被挤到最后一行,稳定模式跑到了静音自动停止前面。)
- 录音方式行补上 Tauri 的 `modeDesc` 说明(原文是空串)。
- 胶囊字幕与字号不再跟「显示胶囊」联动,与 Tauri 一致。
- 启动组按 Tauri 顺序排(开机自启 → 启动时静默运行 → 自动检查更新),
  并补上三行的说明文案。
- 新增「选择与工作区」卡片里的「润色送达」(直接替换 / 预览确认)——
  字段和 core 消费逻辑早就有了,只是一直没有入口。
- 字体下拉改为 Tauri FontCatalog 的名字(只有「系统默认」本地化)。
- 本地化 baseline 补上快捷键筛选测试的夹具文案。

不补的两项:Linux 没有更新器(supports_auto_update=false),
「自动检查更新」开关点了没用,故去掉;pasteShortcut 在 Linux 没有
粘贴模拟路径,加了也是空开关。

* feat(linux-egui): full 选区助手 card (voice edit, intent, edit plan, prompt)

用户指出「选区助手」卡片只做了一行「结果处理」。补齐 Tauri
`SelectionWorkspaceSection` 的全部行为设置:

- 语音编辑开关(`selectionVoiceEnabled`,desc 带当前录音快捷键)
- 结果处理(直接覆盖 / 预览确认),开启语音编辑时补上 `voiceDeliveryDesc`
- 自动判断意图(`selectionVoiceIntentMode`:Auto ↔ Heuristic)
- 额外问句线索(多行,一行一条,`selectionVoiceEditKeywords`)
- 编辑方案格式 XML / JSON(`selectionVoiceEditPlanFormat`)
- 编辑方案系统提示词(多行 + 恢复默认,`selectionVoiceEditSystemPrompt`)

这些都是活的:core 的 selection_voice_service / selection_service 直接读这些偏好。
VM 增加 5 个镜像字段、2 个 SettingsField、1 个 combo、2 个文本动作和
「恢复默认」动作,宿主侧全部落到 UserPreferences。

另:新增 text_area_row 助手;两条设置页绘制测试的视口放大到整页高度,
之前 ScrollArea 只渲染首屏,下面的分区其实没被检查到。

* chore(linux-egui): package revision 128

代码回到 115(2e9e7583)的状态——IBus、portal、X11 抓键、原生剪贴板、uinput
兜底、插件 API 全部撤掉。只有版本号前进,避免 apt 把新包当成降级。

* fix(linux-egui): stop reporting a healthy fcitx5 plugin as unavailable

「权限与数据」页最刺眼的那条报错其实是个假警报:`input_interface_ready` 用
`roxmltree::Document::parse` 解析插件的 introspection 回复,而 fcitx5 的回复
开头是标准的 D-Bus DOCTYPE,roxmltree **默认拒绝 DTD**,解析一失败就返回 false。
于是只要插件正常工作,页面也会显示「选区与文字插入 不可用」,并且因为
`hotkey_ready` 要求这一项为 Ready,「全局快捷键」永远停在「尚未检查」。

真机确认(`probe_session`):
  修前 keyring=Ready input=Unavailable network=Ready(331ms)
  修后 keyring=Ready input=Ready       network=Ready(269ms)

- 解析改用 `parse_with_options(ParsingOptions { allow_dtd: true })`;
- 新增用例 `the_standard_dbus_doctype_does_not_break_the_probe`,用带 DOCTYPE
  的真实回复形状回归这条规则。

同时把这一页的版式对齐 Tauri:
- 卡片顺序改为 权限 → 数据与存储 → 云端同步(此前数据卡在最前,用户先看到
  留存输入框,找不到「哪里不可用」);
- 「重新检查会话与网络」按钮从卡片上方移进权限卡标题行右侧;
- 网络行标题统一成 Tauri 的「网络」,行序调整为 麦克风 → 全局快捷键 → 网络 →
  选区与文字插入(与 Tauri 的 mic / hotkey / network 一致,Linux 独有的检查排在后面);
- 新增 `privacy_page_lists_permissions_before_storage_and_sync` 锁住顺序;
- 修 `text_inputs_keep_and_show_what_the_user_types`:留存输入框现在不在首屏,
  该用例验证的是输入不丢字,改用更高的视口而不是把卡片顺序改回去。

* fix(linux-egui): stop painting raw i18n keys for cloud-sync failures

界面直接显示了 `cloudSyncE2ee.errors.service_unavailable` 这样的裸键。原因:
`cloud_sync.rs` 用 `format!("cloudSyncE2ee.errors.{code}")` 动态拼键,而 catalog
里**一条** `cloudSyncE2ee.errors.*` 都没有——i18n 校对脚本只认得源码里的字面量
键,拼出来的键永远进不了目录,于是失败时就把键名原样画给用户。

- 新增 `cloud_sync_ui::error_key(raw) -> &'static str`:按**原始代码**匹配,
  返回完整的字面量键。之前的 `error_code()` 会把不在 allow-list 里的 Core 代码
  折叠成 `unknown`,而这些恰恰最需要自己的文案。
- catalog 补 36 条:Tauri 已有的 22 条 `cloudSyncE2ee.errors.*` 直接沿用其五语
  译文;Linux/Core 独有的代码(conflict、denied、expired、locked、server、
  unsupported、invalidResponse、cloudDeleted、consentRequired、cryptoWorkerFailed、
  deleteConfirmationRequired、metadataRequired、secureRandomUnavailable、staleTask)
  补齐五语文案;`service_unavailable` 映射到 `unavailable`,并改写原来那句
  「请使用原生客户端」(对 Linux 客户端没有意义)。
- `cloud_sync.rs` 两处渲染改用 `tr_l10n(lang, error_key(..))`。
- 新用例 `every_failure_code_resolves_to_a_real_message`:把 Core 全部
  `error("…")` 字面量 + 本地代码逐个过一遍,断言不会出现裸键。

顺带修 `error_code` 的 camelCase 映射(weak_password / password_mismatch /
too_large / invalid_data / review_required / choices_required 之前统统落到 unknown)。

* fix(linux-egui): actually construct the encrypted cloud-sync service

「加密同步」卡片每个动作都返回 `service_unavailable`,用户看到的是
「加密同步暂不可用,请使用原生客户端并稍后检查服务」。上一版只修了文案,
根因没动:Core 的 `OpenLessBackend` 只有在宿主给 `MarketplaceConfig` 装配了
`EncryptedSyncConfig` 时才构造 `encrypted_sync`,否则 `encrypted_sync` 为
`None`,`cloud_sync_e2ee_status()` 直接返回 `service_unavailable`。

Tauri 在 `src-tauri/src/coordinator.rs` 里装配了它,Linux egui 的
`LinuxBackendBuilder::from_shared_providers` 只给了 `MarketplaceConfig::production()`,
没接这一步——所以整块卡片在 Linux 上从来没通过。

- `backend.rs` 改为 `.with_marketplace_config(production().with_encrypted_sync(..))`,
  `service_origin` 固定为 `DEFAULT_SYNC_SERVICE_ORIGIN`(自建源是运行时偏好,
  换服务器不该改变本地保险库标识),`app_version` 取 crate 版本。
- 新增用例 `shared_provider_builder_wires_encrypted_cloud_sync`:用过生产 builder
  真的建一次 backend,断言 `cloud_sync_e2ee_status()` 返回 Ok(新目录为 Disabled),
  防止这一步再被漏掉。
- 文案:Tauri 的 `cloudSyncE2ee.errors.unavailable` 说的是「请使用原生客户端」,
  对 Linux 客户端本身没有意义,而且共享键每次 i18n sync 都会被 Tauri 覆盖。
  改成 egui 独有的 `cloudSyncE2ee.errors.serviceUnavailable`,`error_key` 指向它。

* fix(linux-egui): fill in the cloud-sync card's step/state strings

截图里还能看到四处裸键:`cloudSyncE2ee.stepEnableTitle`、`.stepEnableDetail`、
`.states.disabled`、`.syncNow`,以及一个孤零零的 `done`。都是同一类问题:

- `cloud_sync_ui.rs` 里 `states.*` / `step*` / `syncNow` 等是静态字符串常量,
  目录里从来没有过这批条目(共 30 条);
- `notice` 存的是 `"done"` / `"restored"` / `"states.conflict"` 这种裸值,
  渲染时直接查表,于是屏幕上出现 `done`。

改动:
- 目录补 32 条:Tauri `cloudSyncE2ee` 命名空间里的 28 条(steps / states /
  protocol / conflict / syncNow / checkPending / unlockTitle / createTitle)沿用
  现有五语译文;`stepRestoreTitle`/`stepRestoreDetail`(Tauri 没有)与
  `noticeDone`/`noticeRestored` 新写五语。
- `main.rs` 里的 notice 值改成完整键(`cloudSyncE2ee.noticeDone` /
  `.noticeRestored` / `.states.conflict`)。
- 新增契约测试 `every_literal_catalog_key_exists_in_the_catalog`:扫 `src/` 下所有
  **字符串字面量**里的 `cloudSyncE2ee.*` / `settings.*`,逐个要求在 `src/i18n.rs`
  里存在。带 `{` 的格式模板跳过(编译期无法校验,只能靠 `error_key()` 那种字面量
  映射表,已有独立用例覆盖)。
- 这条新测试顺手抓出两处别处漏翻:`settings.providers.fillDefault`、
  `settings.providers.llmProtocolHeaderConflict`(提供方设置的按钮与冲突提示),
  一并补上五语文案。

* fix(linux-egui): re-seed the channel editor fields when a config arrives

「编辑已有渠道」时输入框是空的——不是 Core 读不出来(用探针确认 endpoint/model/
凭据都能读回),而是 egui 这边的字段草稿缓冲把空值锁死了:

```rust
let id = egui::Id::new(("openless-settings-provider-field", format!("{field:?}")));
let mut draft = data.get_temp(draft_id).unwrap_or_else(|| value.to_string());
... data.insert_temp(draft_id, draft);   // 每帧写回
```

两个缺陷:

1. 键里**没有渠道维度**,只按字段类型(`Endpoint`/`Model`…)存。所以先开一个
   「添加渠道」草稿(字段为空),或编辑过另一个渠道之后,再打开已有渠道,输入框
   显示的是那份旧缓冲——空值或上一个渠道的文字。
2. 缓冲一旦以空值播种就永远不再看宿主值,加载出来的配置再也进不去。

改法:

- 键改成 `(字段类型, channel_id)`,渠道之间不再串味;
- 额外记一份「播种时的宿主值」:宿主值变了就重新播种(新加载的编辑器、切换渠道、
  外部改动都会生效),没变就保留用户正在输入的内容(光标不乱跳)。

回归用例 `provider_editor_fields_reseed_when_the_channel_config_arrives` 复刻用户
路径:先开空草稿 → 再编辑已有渠道(必须显示保存的 endpoint/model)→ 切到另一个
渠道(不能残留上一个的文字)。把 `settings.rs` 回退到修复前跑这条用例会红在
"the saved endpoint must be painted"。

顺带给 `provider_field_hint` 加了 `channel_id` 参数(`too_many_arguments` 已注明)。

* fix(linux-egui): omni test/fetch sent a channel id Core rejects; hydrate secrets

两个问题,根因都在宿主这一侧:

1. 「多模态模型密钥填进去用不了,测试一下就消失」
   多模态的「测试」和「拉取模型」把 `channel_id: Some(provider)` 传给 Core,而
   `provider_service` 对 `kind == Omni && channel_id.is_some()` 直接返回
   `omni provider does not support channel id`。于是每一次测试都失败,用户以为密钥
   无效;Tauri 的 `ProviderTools` 对 omni 根本不传 provider,Core 自己按当前激活的
   服务商解析凭据。改为 `channel_id: None`(抽出 `omni_provider_request()`,带用例
   `omni_requests_never_carry_a_channel_id` 锁住)。

2. 「密钥还是显示不出来」
   `load_provider_editor` 把 `primary_secret` / `secondary_secret` 写死成空串(原注释
   是「secret fields are write-only」),`omni_settings::load` 也刻意不读 api_key。
   但 Tauri 的 `CredentialField` 就是把已存的值读进输入框再打码——所以用户看到的是
   「密钥没保存」。现在按 `AuthRequirement` 读回对应账户(volcengine 的
   app_key/access_key 或 api_key、xfyun、tencent、以及默认 api_key),自定义渠道的
   extra_headers 也一并读回;输入框保持密码掩码,并加了「显示 / 隐藏」切换
   (`settings.providers.showSecret` / `hideSecret`)。

   保存语义不变:空输入仍然表示「保留已存的密钥」,清空有专门的按钮。

测试:`the_channel_editor_hydrates_the_stored_secret`(真 backend + 内存凭据库,
断言 endpoint/model/api_key 都能读回编辑器)。

* fix(linux-egui): keep the API key in the editor after it is saved

传统模式(LLM 渠道)里填进去的密钥「几秒后被删掉、连掩码点都没有」。

保存回执的处理里有一句「擦掉已被确认的草稿」:

```rust
if form.primary_secret == editor.primary_secret {
    form.primary_secret.clear();
}
```

自动保存是输入后 300ms 触发的,所以流程是:填入密钥 → 300ms 后保存成功 →
同一份草稿被判定为「已确认」并清空 → 输入框空白。密钥其实已经写进保险库,只是
界面不再显示。

这句在密钥从不回填的年代看不出问题;现在编辑器按 Tauri `CredentialField` 的做法
把已存的密钥读回输入框(掩码显示),清空就变成了「密钥被删除」。

- 删掉三处擦除(primary / secondary / extra_headers),并注明原因;
- 写盘路径不变:空输入仍然表示「保留已存值」,清空有单独的按钮;
- 新增用例 `a_saved_secret_stays_in_the_editor`:模拟宿主提交后收到 `ProviderSaved`
  成功回执,断言表单里的密钥仍在。

* test(linux-egui): avoid a default-endpoint literal in the hydration test

`check-linux-public-surface.ps1` 明确禁止 linux UI 源码出现默认服务地址
(`ASR_PRESETS|LLM_PRESETS|OMNI_PRESETS|https://…/v1`),而新加的用例直接把
`https://api.deepseek.com/v1` 写进了 main.rs,CI 的
`Test Core, Linux host, and platform boundaries` 因此失败:

    Write-Error: Linux UI must not own provider endpoint/model/auth defaults

改用 `concat!` 拼出用例里的 URL(形状真实,但不是默认值),注释也避开同样的
字面量。

* fix(linux-egui): address PR1117 review findings

* fix(linux-egui): clear drag state without Default bound

* test(linux-egui): consume headless popup texture deltas

* chore(deps): update the whole workspace to the latest versions, including egui 0.37

`cargo update` 把整个 workspace(Linux 宿主、Core、Tauri/Android 共享)的传递依赖
推到最新兼容版本:42 个包,例如 aes 0.9.2→0.9.3、keyring-core 1.0.1、
libc 0.2.191、serde_json 1.0.154、sha2 0.11.1、tokio 1.53.3、uuid 1.28、
zip 9.0.5、zstd 0.14.3、zvariant 5.15.1。

同时把两个主版本升级做掉:

1. **egui / eframe 0.36.2 → 0.37.0**(此前用 `=` 钉死)。0.37 的 API 变化逐条收口:
   - `FontData::font` 由 `Cow<[u8]>` 改成 `Arc<dyn AsRef<[u8]>>` → `Arc::new(bytes)`
   - `ScrollArea` 的 `state.offset` 变成方法且弃用 → `clamped_offset()`
   - `Event::MouseWheel` 新增 `source` 字段 → `MouseWheelSource::Wheel`
   - `Id::new` 弃用(现为 `Id::unique` 的别名)→ 全仓 215 处改为 `Id::unique`
   - `Ui::id()` 改名 `scope_id()`、`TextEdit::vertical_align` 改名 `align(Align2)`
     → 40 + 7 处机械替换

2. **Core 的 sha2 0.10 → 0.11.1**,与宿主对齐。0.11 的 `digest()` 返回
   `Array<u8, U32>`,不再实现 `LowerHex`,5 处 `format!("{:x}", ..)` 改成
   `hex::encode(..)`。

仍然落后于最新、但由上游依赖钉死的三项(无法在本仓解决):
`base64 0.22.1`、`generic-array 0.14.7`、`matchit 0.8.4`(axum 0.8.9 写死 `=0.8.4`)。

验证:core + linux-egui 全量测试(253 lib / 220 bin / 集成)通过,clippy
`-D warnings` 干净,i18n 0 added/0 updated,打包 + 校验通过(2.0.2-Beta.1-138)。

* chore(src-tauri): refresh the Tauri 2.11 train to its latest patches

Tauri 那条线一直用 `=` 钉死,因为 `tauri 2.11.2` 允许
`tauri-runtime ^2.11`,解锁解析会挑到不兼容的版本而编译失败。

这次把整条 train 推到同一 minor 里的最新补丁版本(上游仍是 2.11.2):

- tauri 2.11.2 → 2.11.6
- tauri-build 2.6.2 → 2.6.3
- tauri-runtime 2.11.2 → 2.11.3
- tauri-runtime-wry 2.11.2 → 2.11.4
- tauri-macros 2.6.2 → 2.6.3
- tauri-utils 2.9.2 → 2.9.3

同时 `cargo update` 刷新 `src-tauri/Cargo.lock`(独立的 workspace,之前只跟着
上游的锁走,比根 workspace 旧:zip 9.0.4→9.0.5、zvariant 5.12→5.15.1 等)。

注:`src-tauri` 是独立的 workspace(根 workspace 的 `exclude` 里有它),所以根目录
的 `cargo update` 影响不到它;Tauri 宿主也只在 Windows/macOS/Android 上编译,
本地 Linux 无法验证,由 CI 的 Windows / macOS / Android 三个 job 负责。

* docs(deps): full dependency audit, and register the Linux correction-card gap

新增两份文档,回答「还有哪些能升、哪些不能」:

- `docs/dependency-audit.md`:工程里其实是四个独立依赖图(根 workspace、
  `src-tauri` 独立 workspace、npm、Tauri 生成的 Gradle 工程),逐个给出
  当前版本、最新版本、卡点。
  - 根 workspace:直接依赖全部等于最新稳定版(本轮 42 个包 + egui 0.37 +
    sha2 0.11);只剩 `base64 0.22.1`、`generic-array 0.14.7`、`matchit 0.8.4`
    三项,都由 axum/harmony/crypto 链钉死。
  - `src-tauri`:tauri train 已到 2.11.6/2.6.3/2.11.3/2.11.4/2.9.3;
    23 个主版本可独立升级,但需要改代码并靠三端 CI 验证(本地 Linux 编不了
    这个 workspace);tauri 2.12 训练线与全部插件被 `tauri-utils ^2.10`
    挡住,实测解析失败,已记录。
  - npm:`npm update` 升 13 个,剩 9 个主版本(React 19 / Vite 8 / TS 7 …)。
  - Gradle:仓库内没有构建文件,跟着 Tauri 模板走。

- `docs/linux-egui-known-gaps.md`:登记 G01「词汇纠正卡片在 Linux 上永不出现」。
  根因是 `BackendDependencies` 没设 `edit_observation`,Core 回退到
  `NoopEditObservationAdapter`,`pending_corrections()` 恒空。文档里写了补法
  (AT-SPI 文本变更订阅)与降级要求;`02-gap-register.md` 增加了指向它的链接。

顺带:`npm update` 把 `lucide-react` 带到 1.59.0,同步脚本据此重新生成了
`linux-egui/assets/lucide/` 里 11 个图标(Bolt/Clock/Feather/Help/History/
Layout/More/Overview/Sparkle/Trash/Vocab)。`egui-icons-contract.test.mjs`
校验通过:39 个 Linux 图标与 lucide-react 1.59.0 完全一致,也意味着和 Tauri
前端用的是同一批图形。

* chore(deps): lift the four exact npm dev-tool pins; document every `=` pin

npm 侧还有 4 个写成精确版本的开发工具(`npm update` 不会动它们),一并升级:

- `@playwright/test` 1.63.0 → 1.64.1
- `prettier` 3.9.6 → 3.9.10
- `prettier-plugin-sh` 0.19.0 → 0.20.2
- `tsx` 4.23.1 → 4.23.15

`npm run build`(含 tsc)与前端测试通过。

`docs/dependency-audit.md` 新增「用 `=` 钉住的 15 处」一节,把钉住分成三类,
避免把「钉住」误读成「落后」:

1. 钉住且正好是最新 —— egui/eframe 0.37.0、resvg 0.48.1、tauri 2.11.6 训练线、
   security-framework 3.7.0;
2. 钉住且确实落后 —— tauri-plugin-updater 2.10.1(需 tauri 2.12)、
   sherpa-onnx 1.13.2(Android 预编译库的 size + SHA-256 写死在脚本里)、
   foundry-local-sdk 1.2.1(winml 主版本跨越);
3. 被上游钉死 —— base64 / generic-array / matchit / tauri 2.12 全线。

npm 剩余 11 个主版本(React 19、Vite 8、TypeScript 7、framer-motion 14、
marked 18、js-yaml 5、@shadcn/react 0.3)属于一次性前端大迁移,单独一轮做。

* fix(deps): keep MSRV 1.88, align sherpa-onnx with its sys crate, refresh backend-tests lock

上一轮 `cargo update` 把 CI 全部打挂,三个独立原因:

1. **MSRV 被突破**(macOS Rust 1.88 那个 job)
   `aes 0.9.3` 与 `uuid 1.28.0` 都要求 rustc 1.89,而 Core 与 Tauri 声明 1.88:
   ```
   error: rustc 1.88.0 is not supported by the following packages:
     aes@0.9.3 requires rustc 1.89
     uuid@1.28.0 requires rustc 1.89.0
   ```
   三个锁文件(根 / `src-tauri` / `src-tauri/backend-tests`)里都钉回
   `aes 0.9.2`、`uuid 1.27.0`。

2. **sherpa-onnx 包装 crate 与 -sys 版本错配**(Windows / macOS / Android 三个 job)
   `cargo update` 把 `sherpa-onnx-sys` 推到 1.13.8,而 `sherpa-onnx` 仍被 `=1.13.2`
   钉着,于是 sys 新增的三个字段在旧包装里没有初始化:
   ```
   error[E0063]: missing field `window_shift_ratio` in initializer of
     `sherpa_onnx_sys::OfflineSpeakerSegmentationPyannoteModelConfig`
   ```
   直接把 `sherpa-onnx` 升到 1.13.8(Windows static / Android shared 两处),
   与 sys 对齐;Android 预编译库清单同步更新:
   `scripts/sherpa-onnx-android.mjs` 的版本、归档大小 46_093_321、
   SHA-256 `2ff63469…4f44`(已下载校验并解包确认 ABI 目录结构不变)。

3. **第三个锁文件没刷新**(Linux job)
   `src-tauri/backend-tests` 是**独立 workspace**,有自己的 `Cargo.lock`;CI 用
   `--locked`,于是直接失败:
   ```
   error: cannot update the lock file .../backend-tests/Cargo.lock because --locked was passed
   ```
   `cargo update` 刷新该锁并同样钉住 aes/uuid。

同时把这三个锁都写进 `docs/dependency-audit.md`——之前只记了两处,漏了
`backend-tests`。

验证:core 与 linux-egui 全量测试通过、clippy `-D warnings` 干净、
`sherpa-onnx-android.mjs` 用新校验和成功下载并解包。

* docs(deps): record the third lock file and the sherpa-onnx 1.13.8 alignment

* fix(deps): pin uuid to 1.26.1 in the Tauri locks so Rust 1.88 MSRV holds

上一版把 `uuid` 钉到 1.27.0 仍然不行:1.27 和 1.28 都要求 rustc 1.89,
只有 1.26.1 及更早声明 1.85。

```
error: rustc 1.88.0 is not supported by the following packages:
  uuid@1.27.0 requires rustc 1.89.0
```

`src-tauri` 与 `src-tauri/backend-tests` 两个锁文件都钉到 `uuid 1.26.1`
(MSRV job 只检查这两个 workspace,根 workspace 的 Linux 宿主声明的是 1.95)。

本地用 `rustup toolchain install 1.88.0` + 与 CI 完全相同的命令复验:
`cargo +1.88.0 check --locked --manifest-path src-tauri/Cargo.toml`
不再报 MSRV 冲突,编译推进到本仓自己的 `compile_error!`(Linux 不是 Tauri 目标)
才停,说明整个依赖图对 1.88 干净。

* chore(msrv): raise the desktop MSRV to Rust 1.89 instead of pinning old deps

上一版为了守住 Rust 1.88 把 `aes`/`uuid` 往回钉,等于用降级依赖换工具链。
反过来做:1.88 已经装不下当前依赖树,直接把 MSRV 提到 1.89,依赖保持最新。

- `openless-core` / `src-tauri` / `src-tauri/backend-tests` 的
  `rust-version` 1.88 → 1.89(`linux-egui` 本来就是 1.95,不动)
- 三个锁文件放开:`aes 0.9.3`、`uuid 1.28.0`(1.28 是当前最新,1.27/1.28
  都要求 1.89;1.26.1 及更早是 1.85)
- CI:`macOS Rust 1.88 MSRV` → `1.89 MSRV`,工具链与 `cargo +1.89.0`
  两处命令同步,job 名与文档里的表格一起改
- README(中/英)与 `docs/ci-trigger-and-cache-policy.md` 的最低工具链承诺
  同步到 1.89,并写明这个数字由依赖实际要求决定(aes 0.9.3 / uuid 1.28)

本地用与 CI 相同的命令验证:
`cargo +1.89.0 check --locked --manifest-path src-tauri/Cargo.toml` 与
`cargo +1.89.0 test --locked --manifest-path src-tauri/backend-tests/Cargo.toml --no-run`
都不再报 MSRV 冲突(编译推进到本仓自己的 `compile_error!`,即 Tauri 不支持
Linux 的那条)—— 说明整张依赖图对 1.89 干净。

根 workspace:core + linux-egui 编译、全量测试、clippy `-D warnings` 全部通过。

* [pi] Work in progress

* [pi] Work in progress

* chore: raise MSRV to Rust 1.99 and move the Tauri train to 2.12.4

按「全部升到最新」执行,Rust 与 Tauri 是同一件事——tauri 2.12 的最低要求
就是 rustc 1.90,索性把 MSRV 直接对齐当前 stable。

## Rust 1.99

- `openless-core` / `src-tauri` / `src-tauri/backend-tests` 1.89 → **1.99**
- `linux-egui` 1.95 → **1.99**(全仓一个数字,不再有漂移;egui 0.37 本身只要 1.95)
- CI:MSRV job 工具链与两条 `cargo +1.99.0` 命令同步,job 名 "Rust 1.99 MSRV"
- README(中/英)与 `docs/ci-trigger-and-cache-policy.md` 的公开承诺同步为 1.99,
  并写明该数字的由来(tauri 2.12 → 1.90,aes 0.9.3 / uuid 1.28 → 1.89,
  这里直接跟随 stable)

## Tauri 2.12.4

整条训练线 + 插件一起升(插件的新版本要求 `tauri-utils ^2.10`,只存在于 2.12,
所以必须成套动):

| crate | 旧 | 新 |
|---|---|---|
| tauri | 2.11.6 | **2.12.4** |
| tauri-build | 2.6.3 | **2.7.3** |
| tauri-runtime | 2.11.3 | **2.12.2** |
| tauri-runtime-wry | 2.11.4 | **2.12.2**(连带 wry 0.55.1 → 0.57.0) |
| tauri-macros | 2.6.3 | **2.7.3** |
| tauri-utils | 2.9.3 | **2.10.2** |
| plugin-shell | 2.3.5 | **2.5.0** |
| plugin-dialog | 2.7.2 | **2.8.3** |
| plugin-fs | 2.5.1 | **2.7.1** |
| plugin-updater | 2.10.1 | **2.14.0** |
| plugin-single-instance | 2 | **2.5.4** |
| plugin-autostart | 2 | **2.7.0** |

JS 侧此前 `npm update` 已是 2.12 系(`@tauri-apps/api 2.12.4`、plugin-dialog 2.8.3、
plugin-shell 2.5.0、plugin-updater 2.14.0),这次 Rust 侧追平,消除 JS/Rust 版本错配。

对照 2.12 changelog 核对过两处会打到我们的改动:
- `tauri-runtime` 的 `primary_monitor`/`available_monitors`/`monitor_from_point`
  改为返回 `Result` 并在主线程查询 —— `capsule_mirrors.rs` 与 `lib.rs` 本来就写成
  `.ok()` / `.ok()?` / `.unwrap_or_default()`,无需改。
- 新增 `tauri::Error::UnexpectedMenuKind` —— 只对我们穷尽 `match tauri::Error` 才破坏,
  我们只构造 `Error::Anyhow`,不匹配它。

注意:`tauri 2.12` 的 Android 模板把 Gradle 脚本迁到 `compilerOptions`,最低 Gradle
8.13;`gen/android` 由 CI 的 `tauri android init` 重新生成,无需改仓库文件。

本地验证:core + linux-egui 全量测试、clippy `-D warnings` 通过;
`cargo +1.99.0 check --locked --manifest-path src-tauri/Cargo.toml` 与
backend-tests 的 MSRV 门禁干净(编译推进到本仓自己的「Tauri 不支持 Linux」
`compile_error!` 为止 —— 该 workspace 无法在 Linux 上完整编译,三端由 CI 验证)。
deb/rpm 重新打包并自检:`OpenLess 2.0.2-Beta.1-139`。

* ci: repair cache pruning, stop per-push release caches, add a HarmonyOS gate

## 缓存

`Cache maintenance` 从 10/05 起每周失败,beta scope 已经涨到 10.5 GB(预算
10 GB),平台只能按 LRU 静默淘汰 —— 这就是"有缓存却总在冷编译"的根因。
失败原因不是脚本逻辑,而是 GitHub 改了接口:

    gh: Invalid request. Missing required query parameter key (HTTP 422)

- `scripts/ci-cache-usage.sh`
  - 删除改为同时带 `key` 与 `ref`(`ref` 里含 `/`,统一百分号编码);
  - 新增 `--keep-family N`:同一个(分支, key 家族)只保留最新 N 代,
    其余作为"被取代的旧兄弟"删除。key 家族 = 去掉尾部内容哈希后的前缀,
    例如 `v0-rust-cross-platform-Windows_NT-x64-3619e479-a77d3f83`
    → `v0-rust-cross-platform-Windows_NT-x64`;因为 `restore-keys` 只需要
    上一代做回退,默认保留 2 代;
  - …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[area] 使用 GPT-6 进行润色的时候,预填了错误的参数。

2 participants