用户原话「默认左侧固定,允许折叠」:M297 盘查确认 iced 从来没接过折叠按钮, presenter 的 toggle_nav/nav_collapsed 是 Slint 遗留、唯一调用方在已退役的 crates/app。 ## 令牌(先定数值再动手) - nav_collapsed_width = 56 + 2 × Space::X1 = **64**:胶囊(M283 定稿 56×32,本轮不动) 两侧各留 4px 才不被裁切;旧值 48 的死因写进注释(48 − 2×4 = 40 < 56)。 - 断言:折叠宽 ≥ 胶囊宽 + 2×X1、≥ touch_target 48、在 4px 栅格上。 ## 折叠按钮与折叠态 - 按钮放**轨底、头像位之上**:轨顶一个像素都不动(M283/M299 的展开态对照图仍可比), 轨底本来就是「固定贴底」的槽,两态下位置不变。几何与导航项同一套(56×32 药丸 + 52 命中高)。 - 折叠态:隐藏文字标签与账户名,每项/头像/按钮都补 M3 气泡 tooltip(折叠态不许是一列无名符号); 纵节奏不变(项高仍 52),所以切换时图标不上下跳。五项图标两两不同由用例钉住。 ## 动效(复用底盘、零新常数) - 帧泵加**第二条并列宽度轨**(nav ⇄ rail),时长/曲线复用同一张 rail_spec(); 借用右栗那条会让左轨折叠改掉右栗的目标(暗耦合),tower 已否掉那个方案。 - 起跑在 update 侧;启动按落盘档对表(幂等,仅在没有轨道在跑时);折叠**不置 view_dirty** —— 宽度在 view 里逐帧采样,帧由轨道自己续,跑完 needs_frames() 回 false。 ## 落盘(任务要求「重启读回仍折叠」) - core schema 登记 ui.nav.collapsed(纯增量 bool,不碰 schemaVersion);presenter 桥加 load/persist 一对(走唯一写入口 write_with,现读→改→落盘);启动 construct 读回。 - 截图通道不读用户配置(D30),折叠态出图走 ULL_NAV_COLLAPSED=1。 ## 判据 - app-iced: 1053 passed(新增 3 条接线用例 + 7 条 shell/几何/树结构用例 + 6 条帧泵用例); presenter: 408 passed(新增落盘往返 1 条);fmt / clippy --workspace --all-targets 均 0。 - 变异实验:删 observe_nav ⇒ 宽度停在 88 红;删启动对表 ⇒ 首次展开硬跳红。 - 像素(DevDocs/artifacts/m300/):折叠态内容左缘 112→88(24px 真回收)、药丸两态都 56 宽 且折叠档右缘 59 < 64(不裁切)、图标行是展开态子集(纵节奏不变)。 ## 已知偏离(已登记,tower 批了紧随 mission) crates/app-iced/src/pages/layout.rs 的 rail_width() 仍是 const fn(恒 88),所以折叠态内容块 宽度不变、整块左移 24px ⇒ 右侧多一条 24px 空白带(像素图里保留、未裁)。
386 lines
19 KiB
Rust
386 lines
19 KiB
Rust
//! `config/launcher.json` 的键表(全局设置)。键名与默认值对齐 `04-data-and-config.md` §2-§3;
|
||
//! AI 可写 + 敏感分级按 `02-ai-agent-design.md` §3.1 落:**密钥类一律 locked**。
|
||
|
||
use super::catalog::SettingSchema;
|
||
use super::schema::{SchemaId, SchemaVersion};
|
||
use super::spec::{AiPolicy, SettingSpec};
|
||
|
||
/// `core.data_mode` 允许值。
|
||
pub const DATA_MODES: &[&str] = &["portable", "standard", "custom"];
|
||
/// `download.region` 允许值。
|
||
pub const REGIONS: &[&str] = &["auto", "cn", "global"];
|
||
/// `download.mirror_preference` 允许值。
|
||
pub const MIRROR_PREFERENCES: &[&str] = &["auto", "official", "mirror"];
|
||
/// `download.proxy_mode` 允许值。
|
||
pub const PROXY_MODES: &[&str] = &["none", "system", "manual"];
|
||
/// `ai.provider` 允许值。
|
||
pub const AI_PROVIDERS: &[&str] = &["openai_compatible", "anthropic", "ollama", "disabled"];
|
||
/// `ui.language` 允许值。
|
||
pub const LANGUAGES: &[&str] = &["zh_cn", "en_us"];
|
||
/// `appearance.theme_mode` 允许值(D17 定案:auto/light/dark)。
|
||
pub const THEME_MODES: &[&str] = &["auto", "light", "dark"];
|
||
/// `ui.theme` 的旧允许值(该键已废弃,仅读兼容,不再落盘)。
|
||
pub const LEGACY_THEMES: &[&str] = &["system", "metro_light", "metro_dark"];
|
||
/// 旧 `ui.theme` 值 → 新 `appearance.theme_mode` 值(tower 键冲突裁决的读兼容映射)。
|
||
pub const THEME_ALIASES: &[(&str, &str)] = &[
|
||
("system", "auto"),
|
||
("metro_light", "light"),
|
||
("metro_dark", "dark"),
|
||
];
|
||
/// `log.level` 允许值。
|
||
pub const LOG_LEVELS: &[&str] = &["error", "warn", "info", "debug", "trace"];
|
||
/// `instances.mode` 允许值(D20 定案:默认隔离,用户可手动切共享)。
|
||
pub const INSTANCE_MODES: &[&str] = &["isolated", "shared"];
|
||
/// `java.installRoot` 未设置时的哨兵(空串 = 按数据目录模式派生落点)。
|
||
pub const JAVA_INSTALL_ROOT_UNSET: &str = "";
|
||
/// `java.vendorPriority` 的允许值(D23:四家发行版,顺序即优先级)。与 `game::java::vendor::VENDOR_IDS`
|
||
/// 必须同一份集合(setting 层不依赖 game 层),一致性由 `tests/setting/java_install_keys.rs` 守。
|
||
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||
|
||
/// 拆到各自文件的键域:中文字体 CDN(M168)/ 自更新(M161)/ 代理与 gh-proxy(M197)/
|
||
/// 下载调优四键(M292)。主表各留一行登记。
|
||
pub mod download;
|
||
pub mod font;
|
||
pub mod proxy;
|
||
pub mod update;
|
||
|
||
/// launcher.json 当前 schema 版本。
|
||
///
|
||
/// `1.1.0` 登记 D20 实例隔离键(`instances.mode` 等)。
|
||
/// `1.2.0` 登记 D23 的 JDK 落点与发行版优先级键(`java.installRoot` / `java.vendorPriority`):
|
||
/// 同样只推 minor、不强制迁移——[`super::document`] 的「默认值不落盘」让老文件缺键即取默认,
|
||
/// 迁移链(按 major 走)不需要新步骤;`1.1.0 → 1.2.0` 的差异只体现在文件里出现新键之后。
|
||
/// `1.3.0` 登记 D17 的「外观(高级)」P1 键(`appearance.bg_image*` / `panel_alpha` / `glass` /
|
||
/// `accent_color`):同一套纯增量语义——出厂全默认(高级特性全关)即等于老文件缺键。
|
||
/// `1.4.0` 登记 P4 的外观下层三键(`window_opacity` / `titlebar_mode` / `palette_source`,M91):
|
||
/// 仍然只推 minor、不加迁移步骤——三者的出厂值正是「老文件没有这些键」时的等价物。
|
||
/// `1.5.0` 登记 M116 的窗口圆角策略(`appearance.window_corner`,出厂 `system`):纯增量。
|
||
/// `1.6.0` 登记 M161 的「自更新」两键(见 [`update`]):纯增量但带一步数据迁移
|
||
/// (此前住在 `cache/update/prefs.json`,迁移见 [`super::update_prefs`])。
|
||
/// `1.7.0` 登记 M168 的中文字体下载地址覆盖键(`ui.font_cdn`,见 [`font`]):纯增量、无迁移。
|
||
/// `1.8.0` 登记 M197 的 gh-proxy 前缀覆盖键(`download.gh_proxy`):同样纯增量——出厂值是空串,
|
||
/// 语义正是「老文件没有这个键」时的行为(跟随内置镜像表的默认前缀)。
|
||
/// `1.9.0` 登记 M292 的三个下载超时/阈值键(`download.throughput_floor_kib` /
|
||
/// `stall_timeout_secs` / `response_timeout_secs`,见 [`download`]):纯增量,且**默认值与 M289 的
|
||
/// 硬编码逐值相等**——老文件缺这三键时的行为就是 M292 之前的行为。
|
||
///
|
||
/// **旧读新(方向相反,措辞必须精确)**:1.4.0 的客户端读 1.5.0 的文件时,
|
||
/// [`super::schema::SchemaCompat::check_url`] 走 `found.minor > expected.minor` →
|
||
/// `ReadOnlyPreserveSchema` → [`super::schema::SchemaCompat::access`] → `FileAccess::ReadOnly`。
|
||
/// 也就是说**整份配置对它只读**(值照读、`store.set` 一律 `SettingError::ReadOnly`、`save` 跳过),
|
||
/// 不是「只是少一档」。这是 minor 提升的既定代价——也正是新键必须推 minor 而不是偷偷塞进旧版本的
|
||
/// 原因:回退到旧版本会出现「设置改不动」,而原因只在 `$schema` 那一行里,不看版本号无从排查。
|
||
#[must_use]
|
||
pub fn launcher_version() -> SchemaVersion {
|
||
SchemaVersion::new(1, 9, 0)
|
||
}
|
||
|
||
/// `config/launcher.json` 的 schema。
|
||
#[must_use]
|
||
pub fn launcher_schema() -> SettingSchema {
|
||
SettingSchema::new(SchemaId::LAUNCHER, launcher_version())
|
||
.with(SettingSpec::enumeration(
|
||
"core.data_mode",
|
||
"portable",
|
||
DATA_MODES,
|
||
AiPolicy::sensitive(),
|
||
"数据目录模式:portable(exe 旁)/ standard(系统标准目录)/ custom(自定义)",
|
||
))
|
||
.with(SettingSpec::path(
|
||
"core.data_custom_root",
|
||
AiPolicy::sensitive(),
|
||
"custom 模式的根目录(必须是绝对路径)",
|
||
))
|
||
.with(SettingSpec::path(
|
||
"core.launcher_home",
|
||
AiPolicy::sensitive(),
|
||
"显式覆盖启动器数据目录(.ULL 的等价物)",
|
||
))
|
||
.with(SettingSpec::path(
|
||
"core.game_home",
|
||
AiPolicy::sensitive(),
|
||
"游戏目录(.minecraft),与数据目录分离指定",
|
||
))
|
||
// D20(research/04 §5.1):只留一个全局默认档,实例级 override 在
|
||
// `instance_keys::instance_schema`(`isolation`),派生逻辑在 `game::isolation`。
|
||
.with(SettingSpec::enumeration(
|
||
"instances.mode",
|
||
"isolated",
|
||
INSTANCE_MODES,
|
||
AiPolicy::sensitive(),
|
||
"实例默认隔离模式:isolated(运行目录派生 versions/<版本 id>,D20 默认)/ shared(所有实例共用 .minecraft 根)",
|
||
))
|
||
// M292:下载调优四键(并发 / 单源尝试次数 / 地区 / 镜像偏好)搬到 [`download`]——主表这一行
|
||
// 代替原先四条 `.with(..)`,登记顺序与搬到那边之前逐项相同。
|
||
.with_all(download::specs())
|
||
// M197:代理与 gh-proxy 三键(`download.proxy_mode` / `proxy_url` / `gh_proxy`)搬到
|
||
// [`proxy`]——主表这一段的注释也一起搬过去,见那个模块的表格。
|
||
.with_all(proxy::specs())
|
||
.with(SettingSpec::path(
|
||
"java.selected_path",
|
||
AiPolicy::sensitive(),
|
||
"用户选定的 Java 可执行文件路径",
|
||
))
|
||
.with(SettingSpec::bool(
|
||
"java.auto_install",
|
||
true,
|
||
AiPolicy::audited(),
|
||
// M197:这句会进 AI 工具的 JSON Schema `description`(`catalog.rs` 直接塞 `spec.doc`),
|
||
// 所以必须写**实现真做的事**:缺 Java 时在 Java 页标出推荐版本并可一键安装,
|
||
// 不会自己开始下载(是否下载由用户点「安装」决定)。
|
||
"缺 Java 时在 Java 页标出推荐版本并可一键安装;不会自动下载",
|
||
))
|
||
// D23:托管 JDK 的落点三模式。语义在 `game::java::location`:
|
||
// 未设置(空串)→ 按 `core.data_mode` 派生(Portable 数据根 / Standard 系统目录);
|
||
// 设置了 → 覆盖前两者,按 `<vendor>/<version>` 分层。
|
||
.with(SettingSpec::text(
|
||
"java.installRoot",
|
||
JAVA_INSTALL_ROOT_UNSET,
|
||
512,
|
||
AiPolicy::sensitive(),
|
||
"托管 JDK 的自定义安装根(留空 = 按数据目录模式:portable 进数据根、standard 进 %APPDATA%\\Java);设置后覆盖两者,JDK 落在 <root>/<发行方>/<版本>/",
|
||
))
|
||
// D23:发行版回退链的用户优先级。空数组 = 按地区默认序
|
||
// (国内 Temurin→Oracle→Microsoft→Zulu;国外 Temurin→Microsoft→Oracle→Zulu)。
|
||
.with(SettingSpec::enum_list(
|
||
"java.vendorPriority",
|
||
JDK_VENDORS,
|
||
AiPolicy::audited(),
|
||
"JDK 发行版优先级(temurin/oracle/microsoft/zulu);留空按地区默认序,只写几家则其余按默认序补齐",
|
||
))
|
||
.with(SettingSpec::int(
|
||
"java.default_memory_mb",
|
||
2048,
|
||
(512, 65536),
|
||
AiPolicy::silent(),
|
||
"默认分配给游戏的最大堆内存(MB)",
|
||
))
|
||
.with(SettingSpec::text(
|
||
"java.extra_args",
|
||
"",
|
||
1024,
|
||
AiPolicy::sensitive(),
|
||
"附加 JVM 参数(原样拼到命令行)",
|
||
))
|
||
.with(SettingSpec::bool(
|
||
"ai.enabled",
|
||
true,
|
||
AiPolicy::silent(),
|
||
"启用内置 AI agent(关闭后规则引擎诊断仍可用)",
|
||
))
|
||
.with(SettingSpec::enumeration(
|
||
"ai.provider",
|
||
"openai_compatible",
|
||
AI_PROVIDERS,
|
||
AiPolicy::sensitive(),
|
||
"LLM 提供方:openai_compatible(含本地 llama.cpp/DeepSeek/Qwen)/ anthropic / ollama / disabled",
|
||
))
|
||
.with(SettingSpec::text(
|
||
"ai.base_url",
|
||
"",
|
||
512,
|
||
AiPolicy::sensitive(),
|
||
"LLM 端点 base_url(OpenAI 兼容形态)",
|
||
))
|
||
.with(SettingSpec::text(
|
||
"ai.model",
|
||
"",
|
||
128,
|
||
AiPolicy::audited(),
|
||
"模型名(如 gpt-4o-mini / deepseek-chat)",
|
||
))
|
||
.with(SettingSpec::bool(
|
||
"ai.yolo",
|
||
false,
|
||
AiPolicy::sensitive(),
|
||
"自动批准全部工具调用(审计照记,白名单不变)",
|
||
))
|
||
.with(SettingSpec::new(
|
||
"ai.api_key",
|
||
super::spec::ValueKind::Text { max_len: 4096 },
|
||
serde_json::Value::Null,
|
||
AiPolicy::locked(),
|
||
"LLM API 密钥——AI 永不读写的凭据(D9)",
|
||
))
|
||
// M39(P3 MCP server,02 §0「传输定论」):对外 agent 工具服务。
|
||
// 三个开关默认「关 / 只读」——外部客户端拿到的是与内置客户端**同一注册表**的工具,
|
||
// 但写权限必须用户显式打开(02 §1「同一注册表、同一安全策略、同一审计日志」)。
|
||
.with(SettingSpec::bool(
|
||
"agent.mcp.enabled",
|
||
false,
|
||
AiPolicy::sensitive(),
|
||
"对外 MCP server 总开关(127.0.0.1 上的 Streamable HTTP;stdio 形态按需显式启动)",
|
||
))
|
||
.with(SettingSpec::int(
|
||
"agent.mcp.port",
|
||
47823,
|
||
(1024, 65535),
|
||
AiPolicy::sensitive(),
|
||
"MCP server 的 Streamable HTTP 端口(只绑 127.0.0.1)",
|
||
))
|
||
.with(SettingSpec::bool(
|
||
"agent.mcp.allow_write",
|
||
false,
|
||
AiPolicy::sensitive(),
|
||
"允许外部 MCP 客户端调用写工具(默认只读;敏感键仍受分级确认与 YOLO 规则约束)",
|
||
))
|
||
.with(SettingSpec::text(
|
||
"agent.mcp.token",
|
||
"",
|
||
256,
|
||
AiPolicy::locked(),
|
||
"MCP server 本地访问令牌(Authorization: Bearer;空 = 不校验,仅绑 127.0.0.1)",
|
||
))
|
||
.with(SettingSpec::enumeration(
|
||
"ui.language",
|
||
"zh_cn",
|
||
LANGUAGES,
|
||
AiPolicy::audited(),
|
||
"界面语言",
|
||
))
|
||
// M300(NAVCOLLAPSE):左侧导航轨折叠成图标条。纯视觉、一键可复原(切回 `false` 即展开),
|
||
// 没有安全面 ⇒ 分级与 `ui.language` 同族取 audited(AI 改过要留痕,但不要确认框)。
|
||
// 键名与 `unlucky_presenter::prefs::keys::NAV_COLLAPSED` 逐字相同(两侧各有一份常量,
|
||
// 相不相同由 `presenter/src/prefs/tests.rs` 与 `settings_bridge` 的往返用例一起钉住)。
|
||
.with(SettingSpec::bool(
|
||
"ui.nav.collapsed",
|
||
false,
|
||
AiPolicy::audited(),
|
||
"左侧导航轨折叠成图标条(false = 展开,带文字标签)",
|
||
))
|
||
.with(SettingSpec::enumeration(
|
||
"appearance.theme_mode",
|
||
"auto",
|
||
THEME_MODES,
|
||
AiPolicy::silent(),
|
||
"主题模式:auto(跟随系统)/ light(Metro 浅色)/ dark(Metro 深色)",
|
||
))
|
||
// D17「外观(高级)」P1 四件(09 §101-104 / §111):**默认全部关闭**(出厂 Metro 纯色)。
|
||
// 分级一律 audited(Medium):全部是纯视觉、可一键复原的键,没有安全面(不碰凭据/网络/
|
||
// 数据根),谈不上 High 级确认;但 AI 改过什么要留审计痕迹,故不用 silent。
|
||
.with(SettingSpec::bool(
|
||
"appearance.bg_image_enabled",
|
||
false,
|
||
AiPolicy::audited(),
|
||
"自定义窗口背景图开关(默认关;开启时设置页给出对比度提示)",
|
||
))
|
||
.with(SettingSpec::text(
|
||
"appearance.bg_image",
|
||
"",
|
||
512,
|
||
AiPolicy::audited(),
|
||
"背景图路径(PNG;空 = 未设置。Slint 默认解码器只带 PNG)",
|
||
))
|
||
.with(SettingSpec::enumeration(
|
||
"appearance.bg_fill",
|
||
"cover",
|
||
super::appearance::BG_FILLS,
|
||
AiPolicy::audited(),
|
||
"背景图填充模式:cover(铺满裁切)/ contain(完整留底色)/ tile(平铺)",
|
||
))
|
||
.with(SettingSpec::float(
|
||
"appearance.bg_dim",
|
||
0.35,
|
||
(
|
||
super::appearance::BG_DIM_MIN,
|
||
super::appearance::BG_DIM_MAX,
|
||
),
|
||
AiPolicy::audited(),
|
||
"背景图叠加暗化层强度(越大前景越清晰;09 §101 的对比度保底手法)",
|
||
))
|
||
.with(SettingSpec::float(
|
||
"appearance.panel_alpha",
|
||
1.0,
|
||
(super::appearance::PANEL_ALPHA_MIN, 1.0),
|
||
AiPolicy::audited(),
|
||
"全局面板半透明度(卡片/导航/AI 栏统一透明度;1.0 = 出厂不透明)",
|
||
))
|
||
.with(SettingSpec::bool(
|
||
"appearance.glass",
|
||
false,
|
||
AiPolicy::audited(),
|
||
"玻璃拟态卡片(背景图预生成模糊层作卡片底;Slint 无 backdrop-filter 的替代)",
|
||
))
|
||
.with(SettingSpec::text(
|
||
"appearance.accent_color",
|
||
"",
|
||
9,
|
||
AiPolicy::audited(),
|
||
"自定义强调色 #RRGGBB(空 = 出厂品牌板;非空时只换 primary/secondary/tertiary 三族,中性表面与 error 保持基线;非法值不写盘,设置页给错行)",
|
||
))
|
||
// M91(P4 / D36·D37·D38):「外观下层」三键——只登记取值与范围,判定与落地都在 UI 侧。
|
||
// 分级同 D17 那批(audited/Medium):纯视觉、可一键复原,但要留审计痕迹。
|
||
.with(SettingSpec::float(
|
||
"appearance.window_opacity",
|
||
1.0,
|
||
(super::appearance::WINDOW_ALPHA_MIN, 1.0),
|
||
AiPolicy::audited(),
|
||
"整窗透明度 0.08~1.0(1.0 = 完全不透明;下限 0.08 是为了不把窗口写到穿透,D36)",
|
||
))
|
||
.with(SettingSpec::enumeration(
|
||
"appearance.titlebar_mode",
|
||
"auto",
|
||
super::appearance::TITLEBAR_MODES,
|
||
AiPolicy::audited(),
|
||
"标题栏三态:auto(按合成器判定)/ always(总是自绘)/ never(合成器已装饰),默认 auto(D37)",
|
||
))
|
||
.with(SettingSpec::enumeration(
|
||
"appearance.palette_source",
|
||
"preset",
|
||
super::appearance::PALETTE_SOURCES,
|
||
AiPolicy::audited(),
|
||
"配色来源档(D38 三级):background-image(背景图取色)/ wallpaper(桌面壁纸取色)/ preset(内置色卡,默认)",
|
||
))
|
||
// M116:窗口外框圆角策略。只有两档(system / square)——系统不暴露圆角半径,
|
||
// 「固定 8/12/16」只能是自绘圆角,而自绘圆角在无边框 + 整窗 α 上要裁内容层,
|
||
// 会连标题栏三键与缩放热区一起裁掉(用户定案 2026-11-03)。
|
||
.with(SettingSpec::enumeration(
|
||
"appearance.window_corner",
|
||
"system",
|
||
super::appearance::WINDOW_CORNERS,
|
||
AiPolicy::audited(),
|
||
"窗口圆角:system(跟随系统:Win11/Server 2025 走 DWM,Win10 保持直角)/ square(总是直角),默认 system",
|
||
))
|
||
// 已废弃:D17 定案 appearance.theme_mode 为准(旧值读兼容映射,写永远走新键)。
|
||
.with(
|
||
SettingSpec::new(
|
||
"ui.theme",
|
||
super::spec::ValueKind::Enum(LEGACY_THEMES),
|
||
serde_json::Value::Null,
|
||
AiPolicy::retired(),
|
||
"【已废弃】改用 appearance.theme_mode;旧值读兼容:system→auto / metro_light→light / metro_dark→dark",
|
||
)
|
||
.deprecated("appearance.theme_mode", THEME_ALIASES),
|
||
)
|
||
.with(SettingSpec::float(
|
||
"ui.scale",
|
||
1.0,
|
||
(0.75, 2.0),
|
||
AiPolicy::silent(),
|
||
"界面缩放系数",
|
||
))
|
||
.with(SettingSpec::int(
|
||
"ui.window.width",
|
||
1280,
|
||
(320, 16384),
|
||
AiPolicy::silent(),
|
||
"主窗口宽度(逻辑像素)",
|
||
))
|
||
.with(SettingSpec::int(
|
||
"ui.window.height",
|
||
800,
|
||
(200, 16384),
|
||
AiPolicy::silent(),
|
||
"主窗口高度(逻辑像素)",
|
||
))
|
||
.with(SettingSpec::enumeration(
|
||
"log.level",
|
||
"info",
|
||
LOG_LEVELS,
|
||
AiPolicy::audited(),
|
||
"启动器日志级别",
|
||
))
|
||
.with_all(update::specs())
|
||
.with(font::cdn_spec())
|
||
}
|