Skip to main content

spool/desktop/
errors.rs

1//! `DesktopErrorEnvelope` 与 kind classification。
2
3use serde::Serialize;
4use ts_rs::TS;
5
6#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, TS)]
7#[serde(rename_all = "snake_case")]
8#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
9pub enum DesktopErrorKind {
10    Input,
11    Config,
12    Routing,
13    Scan,
14    Runtime,
15}
16
17#[derive(Debug, Clone, Serialize, PartialEq, Eq, TS)]
18#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
19pub struct DesktopErrorEnvelope {
20    pub kind: DesktopErrorKind,
21    pub message: String,
22    pub hint: String,
23    pub explain: String,
24}
25
26pub type DesktopResult<T> = Result<T, DesktopErrorEnvelope>;
27
28pub(super) fn input_error(message: String) -> DesktopErrorEnvelope {
29    present_error(
30        DesktopErrorKind::Input,
31        &message,
32        "请先修正输入字段或路径,再重新运行。",
33    )
34}
35
36pub(super) fn runtime_error(error: anyhow::Error) -> DesktopErrorEnvelope {
37    let message = error.to_string();
38    let kind = classify_runtime_error(&message);
39    let hint = match kind {
40        DesktopErrorKind::Input => "请先修正输入字段或路径,再重新运行。",
41        DesktopErrorKind::Config => {
42            "请检查 config 路径、TOML 内容以及 vault.root / note_roots 配置。"
43        }
44        DesktopErrorKind::Routing => {
45            "请确认 CWD 位于某个 project.repo_paths 下,且该项目已配置 note_roots。"
46        }
47        DesktopErrorKind::Scan => {
48            "请检查 vault root、note_roots、Markdown 文件体积限制,以及相关目录是否可读。"
49        }
50        DesktopErrorKind::Runtime => "请查看 explain 中的原始错误,并结合输入与配置继续排查。",
51    };
52    present_error(kind, &message, hint)
53}
54
55fn classify_runtime_error(message: &str) -> DesktopErrorKind {
56    let lowered = message.to_ascii_lowercase();
57    if message.contains("no project matched cwd") {
58        DesktopErrorKind::Routing
59    } else if message.contains("configured note_root")
60        || message.contains("vault scan exceeded")
61        || message.contains("markdown file exceeds")
62        || message.contains("failed to read markdown file")
63        || message.contains("failed to stat markdown file")
64        || message.contains("failed to canonicalize vault root")
65    {
66        DesktopErrorKind::Scan
67    } else if lowered.contains("toml")
68        || lowered.contains("config")
69        || lowered.contains("missing field")
70        || lowered.contains("unknown field")
71        || lowered.contains("invalid type")
72    {
73        DesktopErrorKind::Config
74    } else {
75        DesktopErrorKind::Runtime
76    }
77}
78
79fn present_error(kind: DesktopErrorKind, message: &str, hint: &str) -> DesktopErrorEnvelope {
80    let heading = match kind {
81        DesktopErrorKind::Input => "输入校验失败",
82        DesktopErrorKind::Config => "配置错误",
83        DesktopErrorKind::Routing => "项目路由失败",
84        DesktopErrorKind::Scan => "Vault 扫描失败",
85        DesktopErrorKind::Runtime => "运行时错误",
86    };
87    DesktopErrorEnvelope {
88        kind,
89        message: message.to_string(),
90        hint: hint.to_string(),
91        explain: format!("{heading}\n\n建议:{hint}\n\n原始错误:{message}"),
92    }
93}