Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s

Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
AskaEth committed 2026-10-31 21:54:10 +08:00
commit bc41dfcf7d
517 files changed
+92844

No files matched your search

+331
View File
@@ -0,0 +1,331 @@
//! 配置键元数据:类型/范围、默认值、AI 可写白名单、敏感分级。
//!
//! 出处:`DevDocs/PlanDoc/04-data-and-config.md` §3(schema 化 + AI 可写键 = schema 子集)、
//! `DevDocs/PlanDoc/02-ai-agent-design.md` §3.1(AI 安全边界:白名单 + 类型/范围校验 + 分级确认)。
//!
//! 这份元数据是「唯一真相源」:UI 设置页、AI 的 `get_config`/`set_config`、审计日志都读它。
use serde_json::{json, Value};
/// 键的值类型与约束。
#[derive(Debug, Clone, PartialEq)]
pub enum ValueKind {
/// 布尔。
Bool,
/// 整数(含闭区间范围)。
Int {
/// 下界(含)。
min: i64,
/// 上界(含)。
max: i64,
},
/// 浮点(含闭区间范围)。
Float {
/// 下界(含)。
min: f64,
/// 上界(含)。
max: f64,
},
/// 文本(长度上限,按字符数)。
Text {
/// 最大字符数。
max_len: usize,
},
/// 枚举(只允许列出的字面量)。
Enum(&'static [&'static str]),
/// 路径(非空字符串;绝对/相对都由业务层决定)。
Path,
/// 字符串列表。
TextList,
/// 枚举字面量数组:每个元素都必须落在白名单里(`overrides` 这类键用)。
EnumList(&'static [&'static str]),
/// 任意 JSON(数据结构本身不参与校验,用于档案列表这类「数据而非配置」的键)。
Any,
}
impl ValueKind {
/// 校验;返回中文原因便于直接给用户看。
pub fn check(&self, value: &Value) -> Result<(), String> {
match self {
Self::Bool => value
.is_boolean()
.then_some(())
.ok_or_else(|| format!("期望布尔,实际是 {value}")),
Self::Int { min, max } => {
let Some(number) = value.as_i64() else {
return Err(format!("期望整数,实际是 {value}"));
};
if number < *min || number > *max {
return Err(format!("{number} 超出范围 [{min}, {max}]"));
}
Ok(())
}
Self::Float { min, max } => {
let Some(number) = value.as_f64() else {
return Err(format!("期望数字,实际是 {value}"));
};
if number < *min || number > *max {
return Err(format!("{number} 超出范围 [{min}, {max}]"));
}
Ok(())
}
Self::Text { max_len } => {
let Some(text) = value.as_str() else {
return Err(format!("期望文本,实际是 {value}"));
};
let len = text.chars().count();
if len > *max_len {
return Err(format!("文本 {len} 字符,超过上限 {max_len}"));
}
Ok(())
}
Self::Enum(allowed) => {
let Some(text) = value.as_str() else {
return Err(format!("期望枚举字面量,实际是 {value}"));
};
if allowed.contains(&text) {
Ok(())
} else {
Err(format!("{text:?} 不在 {allowed:?} 之中"))
}
}
Self::Path => match value.as_str() {
Some(text) if !text.trim().is_empty() => Ok(()),
_ => Err(format!("期望非空路径字符串,实际是 {value}")),
},
Self::TextList => {
let Some(items) = value.as_array() else {
return Err(format!("期望字符串数组,实际是 {value}"));
};
if items.iter().all(Value::is_string) {
Ok(())
} else {
Err("数组里含非字符串元素".to_owned())
}
}
Self::EnumList(allowed) => {
let Some(items) = value.as_array() else {
return Err(format!("期望枚举字面量数组,实际是 {value}"));
};
for item in items {
let Some(text) = item.as_str() else {
return Err(format!("数组里含非字符串元素: {item}"));
};
if !allowed.contains(&text) {
return Err(format!("{text:?} 不在 {allowed:?} 之中"));
}
}
Ok(())
}
Self::Any => Ok(()),
}
}
/// 转成 JSON Schema 片段(喂给 M16 的 `ToolSpec`)。
#[must_use]
pub(crate) fn to_json_schema(&self) -> Value {
match self {
Self::Bool => json!({ "type": "boolean" }),
Self::Int { min, max } => json!({ "type": "integer", "minimum": min, "maximum": max }),
Self::Float { min, max } => json!({ "type": "number", "minimum": min, "maximum": max }),
Self::Text { max_len } => json!({ "type": "string", "maxLength": max_len }),
Self::Enum(allowed) => json!({ "type": "string", "enum": allowed }),
Self::Path => json!({ "type": "string", "minLength": 1 }),
Self::TextList => json!({ "type": "array", "items": { "type": "string" } }),
Self::EnumList(allowed) => {
json!({ "type": "array", "items": { "type": "string", "enum": allowed } })
}
Self::Any => json!({}),
}
}
}
// AI 边界类型从 [`super::policy`] 拆出去(`spec.rs` 摸到 400 行硬上限),
// 路径保持不变:`setting::spec::AiPolicy` 仍是可用名。
pub use super::policy::{AiPolicy, Sensitivity};
/// 键废弃信息:替代键 + 旧值映射表。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Deprecation {
/// 替代键(写永远走这里)。
pub replaced_by: &'static str,
/// 旧值 → 新值映射(读兼容);表里没有的值原样带过去。
pub aliases: &'static [(&'static str, &'static str)],
}
impl Deprecation {
/// 把旧值翻译成新键能接受的值(非字符串或表里没有的值原样返回)。
#[must_use]
pub fn translate(&self, legacy: &Value) -> Value {
match legacy.as_str() {
Some(text) => self
.aliases
.iter()
.find(|(old, _)| *old == text)
.map_or_else(|| legacy.clone(), |(_, new)| Value::from(*new)),
None => legacy.clone(),
}
}
}
/// 一个配置键的完整元数据。
#[derive(Debug, Clone, PartialEq)]
pub struct SettingSpec {
/// 点分键名(如 `download.concurrency`)。
pub key: &'static str,
/// 类型与约束。
pub kind: ValueKind,
/// 默认值;`Value::Null` 表示「无默认」(如用户必须先选的路径)。
pub default: Value,
/// AI 策略。
pub ai: AiPolicy,
/// 一句话说明(UI 提示 + AI 工具描述)。
pub doc: &'static str,
/// 废弃信息:有值表示这个键只做**读兼容**,写必须走替代键。
pub deprecated: Option<Deprecation>,
}
impl SettingSpec {
/// 完全手工构造。
#[must_use]
pub const fn new(
key: &'static str,
kind: ValueKind,
default: Value,
ai: AiPolicy,
doc: &'static str,
) -> Self {
Self {
key,
kind,
default,
ai,
doc,
deprecated: None,
}
}
/// 标记为「已废弃,读兼容到 `replaced_by`」,并按 `aliases` 翻译旧值。
///
/// 用于键改名/语义变更(如 `ui.theme` → `appearance.theme_mode`,tower 键冲突裁决):
/// 旧文件照读、下次保存只写新键,AI 白名单与工具 JSON Schema 自动排除本键。
#[must_use]
pub const fn deprecated(
mut self,
replaced_by: &'static str,
aliases: &'static [(&'static str, &'static str)],
) -> Self {
self.deprecated = Some(Deprecation {
replaced_by,
aliases,
});
self
}
/// 该键是否已废弃。
#[must_use]
pub const fn is_deprecated(&self) -> bool {
self.deprecated.is_some()
}
/// 布尔键。
#[must_use]
pub fn bool(key: &'static str, default: bool, ai: AiPolicy, doc: &'static str) -> Self {
Self::new(key, ValueKind::Bool, Value::Bool(default), ai, doc)
}
/// 整数键(闭区间)。
#[must_use]
pub fn int(
key: &'static str,
default: i64,
(min, max): (i64, i64),
ai: AiPolicy,
doc: &'static str,
) -> Self {
Self::new(
key,
ValueKind::Int { min, max },
Value::from(default),
ai,
doc,
)
}
/// 浮点键(闭区间)。
#[must_use]
pub fn float(
key: &'static str,
default: f64,
(min, max): (f64, f64),
ai: AiPolicy,
doc: &'static str,
) -> Self {
Self::new(
key,
ValueKind::Float { min, max },
Value::from(default),
ai,
doc,
)
}
/// 文本键。
#[must_use]
pub fn text(
key: &'static str,
default: &str,
max_len: usize,
ai: AiPolicy,
doc: &'static str,
) -> Self {
Self::new(
key,
ValueKind::Text { max_len },
Value::from(default),
ai,
doc,
)
}
/// 枚举键。
#[must_use]
pub fn enumeration(
key: &'static str,
default: &'static str,
allowed: &'static [&'static str],
ai: AiPolicy,
doc: &'static str,
) -> Self {
Self::new(key, ValueKind::Enum(allowed), Value::from(default), ai, doc)
}
/// 路径键(无默认值,未设置时为 `null`)。
#[must_use]
pub fn path(key: &'static str, ai: AiPolicy, doc: &'static str) -> Self {
Self::new(key, ValueKind::Path, Value::Null, ai, doc)
}
/// 枚举字面量数组键(默认空数组;元素逐个校验)。
#[must_use]
pub fn enum_list(
key: &'static str,
allowed: &'static [&'static str],
ai: AiPolicy,
doc: &'static str,
) -> Self {
Self::new(
key,
ValueKind::EnumList(allowed),
Value::Array(Vec::new()),
ai,
doc,
)
}
/// 任意 JSON 键(数据结构不参与校验)。
#[must_use]
pub fn any(key: &'static str, ai: AiPolicy, doc: &'static str) -> Self {
Self::new(key, ValueKind::Any, Value::Null, ai, doc)
}
}