Commit Graph
6 Commits
Author SHA1 Message Date
AskaEth 6f78e0cf7e fix(m174): 复审 r2 —— 写入口收敛成一个(语言入口同缺陷)+ 只读文件上保存不再谎报成功
P2-1(同一类缺陷的第三个入口):`persist_ui_language` 与 `persist` 共用同一份长生命
store、`save()` 同样整份落盘,但语言入口没先 `reload()` ⇒ 保存 AI 配置后 1 秒内切界面语言,
`ai.*` 连密钥一起被 1 秒前的旧快照盖掉,而且函数返回 true。

修法选「收敛成唯一写入口」而不是「再补一次 reload()」:
- 新文件 `presenter/src/settings_bridge/write.rs` 提供 `SettingsBridge::write_with(|store| Edit)`
  ——现读 → 改 → 落盘 → 刷 mtime,`Edit::{Changed,Unchanged,Rejected}` 表达闭包的结果,
  `WriteOutcome::{NoStorage,Unchanged,Saved,Skipped(reason),Failed}` 表达写的结果(只有 `Saved`
  算成功)。两个入口都改成传闭包,**入口自己再也拿不到「不重读的 save」**——下一个写入口不会
  再靠「记得」。r1 就是「补一次 reload」的做法,隔一个文件又漏了一次。

第二条(用户可见谎话):`ai::store::save` 注释写着「落盘后再读一遍才算数」,实际返回的是内存
doc;`SettingStore::save` 还有 `Skipped(ReadOnly/Unreadable/NotDirty)` 一档(`Ok`,不是 `Err`),
旧代码 `.is_ok()` 会把它当成功 ⇒ 在「配置文件被更新版本启动器写过」的机器上什么都没写,界面报
「已保存」,重启回旧值。现在按 `SaveOutcome` 分支(只有 `Saved` 算成功,`Skipped` 给具名原因),
并且真的 `SettingStore::load` 一次从磁盘回读。

回归用例(都做了变异验证):
- `settings_bridge_lang::tests::persisting_the_language_keeps_keys_another_writer_just_saved`
  (去掉 `write_with` 的 `reload()` ⇒ 红,输出即「语言写回把刚保存的密钥盖掉了」+ 被覆盖的文件)
- `settings_bridge_lang::tests::persisting_the_language_reports_false_when_the_file_is_read_only`
  (`persist_ui_language` 恒返回 true ⇒ 红)
- `ai::store::tests::read_only_config_is_reported_as_a_failure`(`save()` 恒返回 Ok ⇒ 红)
- `ai::store::tests::skip_reasons_are_named`
2026-11-08 17:03:13 +08:00
AskaEth 93d02ab12b fix(m174): 复审 r1 五项 —— 失败带 target、密钥消息掩码、请求体断言、桥写前重读、结构化分类
P2-1(假绿截图):「测试连接」失败类也带上被请求的 target。`ProbeOutcome` 的五个失败变体
改成 `{ target, detail }`,`ProbeResult::from_outcome` 不再把端点/模型填成空串;view 另加一条
「目标为空就整行不画」的保险。**截图种子改走 `from_outcome`**(新增
`ProbeResult::screenshot_seed`):种子只伪造「结论是哪一档」,字段形状由产品路径决定——上一版
手填字段,于是「失败态目标为空」只在产品上出现、截图永远看不见。

P2-2(明文进日志):`Message::EditKey` 的载荷从 `String` 换成 `ai::Secret`(`Debug` 掩码)。
用例 `the_key_message_never_prints_the_key` 同时钉住裸消息与整条 `pages::Message` 漏斗。

P2-3(模型没被证上线):假服务器按 `Content-Length` 读请求正文,新用例
`the_request_carries_the_configured_model` 断言正文里 `"model":"gpt-test"`。变异实测暴露出
一处真相:`ChatRequest.model` 被 `RigProvider::build_request` 静默忽略,线上模型来自
`ProviderConfig.model`(已另开 finding,并写进 probe 的注释)。

P2-4(数据丢失):`SettingsBridge::persist` 写前 `store.reload()`。`SettingStore::save` 是整份
文档落盘,不重读就会拿 ≤1 秒前的旧快照盖掉刚写的 `ai.*`。两条回归用例:
`presenter::settings_bridge::persist_keeps_keys_another_writer_just_saved` 与 app-iced 侧的
`ai::store::appearance_write_back_keeps_the_ai_keys`(保存 AI → 改主题 → 值还在)。

P2-5(子串匹配):删掉 `classify()`。分类改由 `brain::rig::map_error` 从 rig 0.45 的结构化
报告(`ErrorReport` 的 `kind` / `http_status` / `code`)翻译成新的 `ProviderFailure`
(`brain/provider/failure.rs`),`ProviderError` 因此带上 `kind`。`closed_port_is_unreachable`
不再依赖错误文本:连接被拒恒为 `ErrorKind::Http` ⇒ unreachable。防退化用例
`factory_error_carries_the_structured_classification` 断言「纯文本 401 不许被猜成 auth」。

顺带(400 行硬线):`provider.rs` / `rig.rs` 的用例各自拆到 `provider/tests.rs` /
`rig/tests.rs`(纯搬移)。
2026-11-08 16:20:34 +08:00
AskaEth 1cb36da1e2 docs(presenter): M116 r4 P3 —— settings_bridge 的「三个键」漂移对齐到四个字段
AppearanceBase 自 M116 起有四个字段(整窗 α / 标题栏三态 / 配色来源 / 窗口圆角),
load_base 的文档与测试名还写着「三个键」。只改注释与测试函数名,行为零改动。
2026-11-05 06:35:19 +08:00
AskaEth 89dcb6bdf3 feat(app-iced,i18n): M106 —— 界面跟随系统语言 + 设置页真实语言行
用户原话:「我系统语言是中文 为什么打开软件是英文 并且设置页里面显示的是 English(system)」。
两处根源:①M92 的探测链一直是对的,但 `pages::Model.lang` 在 `app/view.rs` 里写死成
`Lang::En`(`pages::render` 还把下载页写死成英文),探测结果没人用;②设置页语言行是硬编码
字面量 `("Language", "English (System)")`。

新增/改动(按层):
- i18n:新域 `catalog/general.rs`(24 条中英双份 key,装配链尾追加)+ `catalog/golden.rs`
  只追加不重排;`choice.rs`(`LangChoice`:跟随系统/手选 + D28 四级优先级真值表 + 字体优先级真值表)。
- app-iced 语言入口 `src/ui.rs` + `src/ui/lang.rs`:`LangState` = 用户档位 + 探测结论;
  启动判定 `ULL_LANG` > 配置 `ui.language` > 系统 locale > 英文;两行日志(探测/生效 + 中文策略)。
- `app/state.rs`:`App` 持有 `LangState`,切语言 = 换目录 + 落盘 + 重建视图模型;
  `app/view.rs` 的 `model()` 收 `lang`,不再写死英文。
- 设置页:语言行换真控件(跟随系统 / 简体中文 / 英文,回显「界面当前使用:简体中文」);
  页头、左列分组名、逐实例空态、导航轨标签全部走 catalog。
- presenter:`settings_bridge_lang.rs`(`load_ui_language` / `persist_ui_language`)。
  「跟随系统」= **删键**(schema 允许值只有 zh_cn/en_us),读的是 `doc().get()` 而不是
  `effective_str`(后者会把默认值 `zh_cn` 当成用户手选,英文机器一启动就被切成中文)。

验证:408 条 app-iced lib 测试 + 28 条 i18n 测试 + 全仓 `cargo test --workspace --exclude
unlucky-app` 全绿;fmt / clippy `-D warnings` 干净;出图 6 张(en/zh × 浅/深 + 下载页对照)
在 `DevDocs/artifacts/m106/`(该目录被 .gitignore 忽略,按仓内惯例作为本地验收产物)。
2026-11-04 22:50:09 +08:00
AskaEth b8ab46662c feat(app-iced): M93 —— 外观接线:整窗 α / 面板 α / 背景图 / 配色来源
把 appearance.* 那批键接到窗口与像素上(M91 只落了键与判据,零调用方)。

整窗 α(D34/D36)
- `ensure_layered(hwnd)` 落在唯一 unsafe 文件 `platform/win32/transparency.rs`
  (`WS_EX_LAYERED` 只有 Get/SetWindowLongPtrW 能加,且 CI 的 unsafe-scope 是集合相等);
  `app/boot.rs` 只负责开窗,句柄到手后由 `Message::WindowHandle` → `window_alpha::dispatch`
  先补样式再下发。本机实测 `layered=ok outcome=Applied { byte: 205 }`(α=0.8)。
- 三条入口都会重算并下发:启动(句柄到达)、设置页改动、配置文件热切。
- `layered` 日志有三态:`n/a`(α≥1,压根不需要)/ `ok` / `FAILED`,避免把「没用到」写成假红。

面板 α 与 CPU 档(D36)
- 面板 α 独立于整窗 α:只作用到外壳层(根容器 / 标题栏 / 导航轨)与外观卡片;
  CPU 档强制 1.0、玻璃强制关(`pages/appearance/alpha.rs` 的真值表 + 单测)。
- 色卡档继续走令牌 baseline,只有取色档(①②级)才换 `Ui` 的 scheme(`Ui::from_scheme`)。

背景图(D38)
- 读盘解码(png)→ cover/contain/tile 三种填充几何 → `bg_dim` 向底色退 → 36dp 网格马赛克;
  iced 的 `image` widget 需要 `iced/image` feature(Cargo.toml 不在 scope),故为已登记降级。
- 读盘失败静默回落 + 一条可读诊断(设置页错误行 + 日志),配色经 `Provenance` 回落色卡。

设置页「外观」分组
- 整窗透明度滑块(下限 0.08)/ 面板透明度(CPU 档禁用并写明 55%→100% 的降级)/
  背景图开关与路径 / 填充三档 / 暗化 / 配色来源三档;改完即时生效并写回:
  bridge 覆盖的 5 个键走 `SettingsBridge::persist`,`window_opacity` / `palette_source`
  走 `AppCommand::SetSetting`(每个键只有一个写者)。
- `SettingsBridge::load_base()`(presenter)让 app-iced 不需要直接依赖 core 就能读回
  `window_opacity` / `palette_source`。

取证
- `ULL_APPEARANCE_*` / `ULL_START_ROUTE` 只影响本次进程(语义同 ULL_TICK_TRACE),
  截图通道不读用户配置(D30)。
2026-11-02 18:07:31 +08:00
AskaEth f159529936 feat(presenter): 新建 unlucky-presenter,把 host/状态/DTO/设置桥从 app 迁出(M85 第 1 块)
D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate,
Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。

- 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`),
  workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、
  iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。
- `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、
  prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、
  mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等),
  内部实现零改动。
- 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS /
  SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport /
  system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。
- crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge|
  toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*`
  (64 处),backend.rs 的 config_path/build_backend 提为 pub。
- crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。
- prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
2026-11-02 09:29:31 +08:00