Skip to content

feat(linux-egui): 重做日志后端——级别可设、噪声清零、盲区补齐 - #1259

Open
aeoform wants to merge 7 commits into
Open-Less:betafrom
aeoform:chore/egui-logging
Open

aeoform wants to merge 7 commits into
Open-Less:betafrom
aeoform:chore/egui-logging

Conversation

@aeoform

@aeoform aeoform commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

为什么

日志是 Linux 用户唯一能交回来的证据(没有控制台、不会跑 gdb)。当前实现有三个结构性缺陷,实测你机器上那份 5 天、39,111 行的日志:

93%   36,485 行  [pump] heartbeat + [ui-client] ipc round-trip   ← 纯噪声
 0.9%    361 行  其余
 79 行 ERROR 里 66 行不是错误(e2ee adapter 把 stage= 进度写成了 error)
缺陷 后果
LevelFilter::Info 写死,不读 RUST_LOG 出问题只能重新编译一个 debug 版给用户
周期心跳写 info 2 MiB × 2 份配额一天滚完,事故现场第二天就没了
error 被当 tracer 用 真错误淹在假错误里

做了什么

1. 级别可调,且能持久化

OPENLESS_LOG=debug openless
OPENLESS_LOG=info,openless_core=debug openless
  • OPENLESS_LOG(兼容 RUST_LOG),语法 <level>[,<模块>=<level>],按 :: 边界最长前缀优先
  • 无法识别的片段退回 info,不静默关掉
  • 设置 → 高级 → 调试 → 日志级别(info/debug/trace),写 <data_dir>/logs/level,重启生效
  • 优先级:环境变量 → 设置文件 → 默认 info
  • 宿主本地设置,不进 Core 偏好:不上云、不影响 Tauri 与移动端,不动 contract/backend-2.0.json

2. 噪声清零,但判据保留

心跳的价值不在「每 10 秒一条」,而在状态翻转:

状态变化(窗口进程/托盘/录音)→ 立刻 info
状态没变                     → 每 5 分钟一条 info 存活证明
其余                        → debug

同时降级:[ui-client] ipc round-trip、[transcript] partial emitted(每个流式分片)、[hotkey] local edge(每次按键)、[ui-client] actions from the renderer。

3. error 只写失败

cloud_sync_e2ee/adapter.rs 的 stage=gate_open / stage=store_ok / build start 等进度 tracer 降到 debug;真正的失败分支保持 error。

4. 轮转与格式

项 之前 现在
单文件 5 MiB 2 MiB
保留 .1 一份 .1–.3(8 MiB)
谁轮转 两个进程都轮转 只有宿主(UI 进程改名会让宿主的句柄指向旧 inode,宿主管日志写进 .1)
时间戳 UTC(Z) 本地时区(+08:00)
模块名 不打印 一直打印
线程/行号 无 debug 及以上

5. 导出带全部历史

export_error_logs 按 .3→.2→.1→当前 正序拼接,文件头写明版本/系统/级别/时间。只导当前文件恰好丢掉用户报的那个现场。

6. 补上六处盲区

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

验证

cargo test -p openless-linux-egui --lib     259 passed / 1 ignored
cargo test -p openless-core --lib dictation_engine::tests   54 passed
cargo clippy --all-targets -D warnings      干净
node scripts/egui-logging-contract.test.mjs passed(9 组断言)
node scripts/frontend-test-runner.mjs       全部通过
本地打包 143:deb/rpm verify PASS

实跑确认(临时数据目录,不打扰运行中的实例):

2026-10-23T00:11:21.88+08:00 [INFO] openless_linux_egui::logging: Linux egui logger ready: level=debug rotated=false path=…

契约测试 scripts/egui-logging-contract.test.mjs 守住:必须读 OPENLESS_LOG、级别必须可持久化、导出必须含历史、心跳不许无条件 info、EPC 探针必须 debug、error 里不许出现 stage=、capture 成功必须记后端、听写起点与单实例结果必须留痕、以及拒绝形如 api_key=/token=/password= 的日志参数。

新增文档 docs/linux-egui-logging.md:级别语义(含「什么才算 error」的判据)、开关用法、轮转、前缀表、「一次听写在日志里长什么样」的串读示例、以及不记录的东西。

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

改为:掉线只记警告,继续用已捕获音频完成转写;是否真的没有内容交给下游
「未收到有效音频」守卫判断。
- core: mark_dictation_failed 记录 code + message。此前只把 errorCode 写进 history,
  日志里看不到失败发生在哪一步(录音停止 / 转写 / 润色)。
- linux-egui: 输入设备精确匹配失败时打印可选设备列表,并记录最终选用的设备名。
  之前只有一句「using the default device: <填的名字>」,无法判断实际用的是哪只麦。
`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 检查。
日志是 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` 写清级别语义、开关、轮转、前缀表与不记录的东西。
第二轮,把上一版日志「能看」提升到「能定位」。

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

- 写 `<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` 更新:设置入口、导出行为、新增的「一次听写在
  日志里长什么样」串读示例。
localization_contract 会把 linux-egui 源码里的裸简体中文当成用户可见文案拦下来。
我上一版在 logging 的轮转单测里写了中文断言消息("当前文件应被移走"),CI 报
two new raw Simplified-Chinese literals。断言消息改成英文即可。
@aeoform
aeoform force-pushed the chore/egui-logging branch from cf9b0e7 to 9f5a326 Compare October 22, 2026 17:00
@aeoform

aeoform commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

已按「#1255 先合」重排了基线与顺序,避免在他合进来之后出现冲突。

做法

#1255 的最新头(486143c3)之上 cherry-pick 我这三个 commit,无冲突,然后 force-push:

486143c3  test(core): 录音流 stop 失败不再算转写失败        ← 他的分支头
0e1cd67d  feat(linux-egui): 重做日志后端……
c43b6739  feat(linux-egui): 日志级别可设、导出带历史、补上六处盲区
9f5a3260  test(linux-egui): 单测断言消息改英文,别触发本地化契约

这样 #1255 合进 beta 之后,本 PR 的合并基就正好落在他的改动之上,不会出现「两边都改了 dictation_engine.rs / api.rs / audio.rs」的冲突。

重叠的自查

我们两边都碰了同样三个文件,逐处确认过是不同位置、互不覆盖:

文件 他的改动 我的改动
crates/openless-core/src/dictation_engine.rs recording.stop() 失败降级为 warn、继续转写 [transcript] partial emitted 降到 debug;新增 [asr] 耗时行
crates/openless-core/src/api.rs mark_dictation_failed 里补 code/message 日志 新增 [dictation] start 边界日志
linux-egui/src/audio.rs select_input_device 列可用设备、记最终设备 try_start_audio_stream 成功后记 backend/device/format/rate/channels

audio.rs 里那两条会同时存在:他记的是「preferred 没找到、退回默认设备」,我记的是「实际开流成功时用的后端与格式」,取值路径不同,不是重复。

重排后的验证

cargo test -p openless-core --lib                          1381 passed / 0 failed / 3 ignored
cargo test -p openless-linux-egui --lib logging::             6 passed
cargo test -p openless-linux-egui --test localization_contract 5 passed
cargo clippy -p openless-core -p openless-linux-egui --all-targets -D warnings   干净
node scripts/egui-logging-contract.test.mjs                  passed

(openless-core 1381 是因为基线上多了他新加的那条断言逻辑,数目与 beta 不同属正常。)

CI 已在新头上重跑。等他的合了之后我这个可以直接快进。

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.

2 participants