spool-memory 0.2.3

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
//! `DesktopErrorEnvelope` 与 kind classification。

use serde::Serialize;
use ts_rs::TS;

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
pub enum DesktopErrorKind {
    Input,
    Config,
    Routing,
    Scan,
    Runtime,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq, TS)]
#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
pub struct DesktopErrorEnvelope {
    pub kind: DesktopErrorKind,
    pub message: String,
    pub hint: String,
    pub explain: String,
}

pub type DesktopResult<T> = Result<T, DesktopErrorEnvelope>;

pub(super) fn input_error(message: String) -> DesktopErrorEnvelope {
    present_error(
        DesktopErrorKind::Input,
        &message,
        "请先修正输入字段或路径,再重新运行。",
    )
}

pub(super) fn runtime_error(error: anyhow::Error) -> DesktopErrorEnvelope {
    let message = error.to_string();
    let kind = classify_runtime_error(&message);
    let hint = match kind {
        DesktopErrorKind::Input => "请先修正输入字段或路径,再重新运行。",
        DesktopErrorKind::Config => {
            "请检查 config 路径、TOML 内容以及 vault.root / note_roots 配置。"
        }
        DesktopErrorKind::Routing => {
            "请确认 CWD 位于某个 project.repo_paths 下,且该项目已配置 note_roots。"
        }
        DesktopErrorKind::Scan => {
            "请检查 vault root、note_roots、Markdown 文件体积限制,以及相关目录是否可读。"
        }
        DesktopErrorKind::Runtime => "请查看 explain 中的原始错误,并结合输入与配置继续排查。",
    };
    present_error(kind, &message, hint)
}

fn classify_runtime_error(message: &str) -> DesktopErrorKind {
    let lowered = message.to_ascii_lowercase();
    if message.contains("no project matched cwd") {
        DesktopErrorKind::Routing
    } else if message.contains("configured note_root")
        || message.contains("vault scan exceeded")
        || message.contains("markdown file exceeds")
        || message.contains("failed to read markdown file")
        || message.contains("failed to stat markdown file")
        || message.contains("failed to canonicalize vault root")
    {
        DesktopErrorKind::Scan
    } else if lowered.contains("toml")
        || lowered.contains("config")
        || lowered.contains("missing field")
        || lowered.contains("unknown field")
        || lowered.contains("invalid type")
    {
        DesktopErrorKind::Config
    } else {
        DesktopErrorKind::Runtime
    }
}

fn present_error(kind: DesktopErrorKind, message: &str, hint: &str) -> DesktopErrorEnvelope {
    let heading = match kind {
        DesktopErrorKind::Input => "输入校验失败",
        DesktopErrorKind::Config => "配置错误",
        DesktopErrorKind::Routing => "项目路由失败",
        DesktopErrorKind::Scan => "Vault 扫描失败",
        DesktopErrorKind::Runtime => "运行时错误",
    };
    DesktopErrorEnvelope {
        kind,
        message: message.to_string(),
        hint: hint.to_string(),
        explain: format!("{heading}\n\n建议:{hint}\n\n原始错误:{message}"),
    }
}