Files
Unlucky-Launcher/crates/core/src/diagnose/rules.rs
T
AskaEth bc41dfcf7d
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
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

320 lines
9.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 诊断规则模型:YAML schema + 编译后的规则。
//!
//! 设计定稿:`DevDocs/PlanDoc/06-launch-and-diagnostics.md` §2.2–§2.3;
//! 规则来源:HMCL `CrashReportAnalyzer.Rule` 59 条(`DevDocs/analyse/02-launch-pipeline.md` §7.3)。
//! 字段说明与给 M16/M18 的契约见同目录 `CONTRACT.md`。
//!
//! 相对 HMCL「枚举即签名库」的四处升级(analyse/02 §7.10):
//! 1. 规则是**数据**不是代码:加规则只改 `rules.d/*.yaml`,不重编译;
//! 2. 每条规则带 `confidence`/`priority`/`suppress`,可做主因排序(HMCL 只能平铺);
//! 3. 命名捕获组由正则自声明,不再有 `groupNames` 顺序契约;
//! 4. 每条规则绑定六维修复动作,直接成为 agent 工具的参数来源。
use std::collections::BTreeMap;
use std::fmt;
use regex::Regex;
use serde::Deserialize;
// 六维修复动作按体量拆两个文件(本目录文件上限 400 行),从本模块统一导出公开面。
#[path = "fix_action.rs"]
mod fix_action;
#[path = "fix_params.rs"]
mod fix_params;
pub use fix_action::{class_file_to_java_major, FixAction, FixActionKind, MaterializedFixAction};
pub use fix_params::{
AdjustMemory, ChangeGraphics, ChangeJava, ComponentKind, GraphicsSetting, JavaVendor,
ModAction, Reinstall, RemoveMod, SeekHelp,
};
/// 规则分类:HMCL 59 条规则的 10 个根因分组,与 `rules.d/<category>.yaml` 一一对应。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuleCategory {
/// JVM 实现 / Java 版本不兼容。
Jvm,
/// 堆内存不足、系统内存耗尽。
Memory,
/// 显卡驱动 / OpenGL / 分辨率 / 图形覆盖层。
Graphics,
/// 文件被改、安装不完整、原生库缺失。
Integrity,
/// Fabric 的 ModResolutionException 家族。
Fabric,
/// Forge 的依赖解析 / 重复安装 / mod 元数据。
Forge,
/// mod 加载期或运行期崩溃(含 mixin)。
ModRuntime,
/// 具体 mod 与 OptiFine 等成名冲突。
ModConflict,
/// 崩在某个实体 / 方块的 tick。
World,
/// 其它(手动触发调试崩溃等)。
Other,
}
impl RuleCategory {
/// 稳定字符串(也用于诊断输出的 `category` 字段)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Jvm => "jvm",
Self::Memory => "memory",
Self::Graphics => "graphics",
Self::Integrity => "integrity",
Self::Fabric => "fabric",
Self::Forge => "forge",
Self::ModRuntime => "mod_runtime",
Self::ModConflict => "mod_conflict",
Self::World => "world",
Self::Other => "other",
}
}
/// 全部分类(loader 校验规则文件归属、测试遍历用)。
#[must_use]
pub const fn all() -> [Self; 10] {
[
Self::Jvm,
Self::Memory,
Self::Graphics,
Self::Integrity,
Self::Fabric,
Self::Forge,
Self::ModRuntime,
Self::ModConflict,
Self::World,
Self::Other,
]
}
}
impl fmt::Display for RuleCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// 结论严重度(UI 配色与「是否弹窗」的依据)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
/// 提示性:能跑但不最佳。
Info,
/// 警告:可疑但结论不确定。
#[default]
Warning,
/// 严重:基本可以确定崩溃原因。
Critical,
}
impl Severity {
/// 稳定字符串(诊断输出用)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Critical => "critical",
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// 匹配范围:决定引擎怎么切文本,也是性能开关(analyse/02 §8.3 第 1 条)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchScope {
/// 逐行匹配(默认,绝大多数 Java 异常都是单行签名)。
#[default]
Line,
/// 按空行切块匹配(一段异常 + 上下文)。
Block,
/// 整份文本匹配(仅跨行贪婪模式:mod 解析列表、实体 tick 上下文等)。
File,
}
/// 结论文案(`summary` 支持 `{group}` 占位符,`key` 是稳定的 i18n/AI 引用 id)。
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Cause {
/// 稳定 id,如 `diagnose.cause.jvm.too_old_java`。
pub key: String,
/// 一句话结论(中文,可直接给用户看)。
pub summary: String,
/// 更细的解释 / 处理建议。
#[serde(default)]
pub detail: Option<String>,
}
impl Cause {
/// 代入捕获组后的(结论, 细化说明)。
#[must_use]
pub fn render(&self, groups: &BTreeMap<String, String>) -> (String, Option<String>) {
(
substitute(&self.summary, groups),
self.detail
.as_ref()
.map(|detail| substitute(detail, groups)),
)
}
}
/// 一条规则的 YAML schema。
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Rule {
/// 全局唯一 id(`命名空间.短名`)。
pub id: String,
/// 严重度。
#[serde(default)]
pub severity: Severity,
/// 正则(Rust `regex` 语法,命名组用 `(?<name>…)`)。
pub pattern: String,
/// 匹配范围。
#[serde(default)]
pub scope: MatchScope,
/// 命中可信度 0.0~1.0。
#[serde(default = "default_confidence")]
pub confidence: f32,
/// 主因排序权重 0~100。
#[serde(default = "default_priority")]
pub priority: u8,
/// 命中本规则时把列出的规则降级为次生结论。
#[serde(default)]
pub suppress: Vec<String>,
/// 结论文案。
pub cause: Cause,
/// 六维修复动作。
#[serde(default)]
pub fix_action: Vec<FixAction>,
/// 回归样本路径(相对 `rules.d/`)。
#[serde(default)]
pub sample: Option<String>,
/// 出处(HMCL 枚举名 / issue 链接)。
#[serde(default)]
pub source: Option<String>,
}
fn default_confidence() -> f32 {
0.5
}
fn default_priority() -> u8 {
50
}
/// 编译后的规则(正则只编译一次)。
#[derive(Debug, Clone)]
pub struct CompiledRule {
rule: Rule,
category: RuleCategory,
regex: Regex,
}
impl CompiledRule {
/// 编译一条规则。
pub fn compile(rule: Rule, category: RuleCategory) -> Result<Self, RuleError> {
let regex = Regex::new(&rule.pattern).map_err(|err| RuleError::InvalidPattern {
id: rule.id.clone(),
message: err.to_string(),
})?;
Ok(Self {
rule,
category,
regex,
})
}
/// 原始规则。
#[must_use]
pub const fn rule(&self) -> &Rule {
&self.rule
}
/// 规则 id。
#[must_use]
pub fn id(&self) -> &str {
&self.rule.id
}
/// 所属分类。
#[must_use]
pub const fn category(&self) -> RuleCategory {
self.category
}
/// 匹配范围。
#[must_use]
pub const fn scope(&self) -> MatchScope {
self.rule.scope
}
/// 编译好的正则。
#[must_use]
pub const fn regex(&self) -> &Regex {
&self.regex
}
/// 命名捕获组名(文档/测试用;顺序无关,取代 HMCL 的 `groupNames` 契约)。
#[must_use]
pub fn named_groups(&self) -> Vec<&str> {
self.regex.capture_names().flatten().collect()
}
}
/// 规则编译错误。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RuleError {
/// 正则非法(规则 id + 正则库报错原文)。
InvalidPattern {
/// 出错的规则 id。
id: String,
/// 正则库报错。
message: String,
},
}
impl fmt::Display for RuleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidPattern { id, message } => {
write!(f, "规则 {id} 的正则非法:{message}")
}
}
}
}
impl std::error::Error for RuleError {}
/// 把 `{group}` 占位符替换成捕获组值;未知占位符原样保留(便于发现规则写错)。
#[must_use]
pub fn substitute(template: &str, groups: &BTreeMap<String, String>) -> String {
if !template.contains('{') {
return template.to_owned();
}
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(start) = rest.find('{') {
let Some(close) = rest[start..].find('}') else {
break;
};
let end = start + close;
let name = &rest[start + 1..end];
out.push_str(&rest[..start]);
match groups.get(name) {
Some(value) => out.push_str(value),
None => out.push_str(&rest[start..=end]),
}
rest = &rest[end + 1..];
}
out.push_str(rest);
out
}