改用gray_matter解析md头元数据,增加相应的日志
This commit is contained in:
parent
141ffda1ee
commit
56612389ae
82
Cargo.lock
generated
82
Cargo.lock
generated
@ -19,6 +19,18 @@ dependencies = [
|
|||||||
"cpufeatures 0.2.17",
|
"cpufeatures 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@ -28,6 +40,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -93,6 +111,12 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arraydeque"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.89"
|
||||||
@ -545,7 +569,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.2",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -616,7 +640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -844,6 +868,27 @@ dependencies = [
|
|||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gray_matter"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8666976c40b8633f918783969b6681a3ddb205f29150348617de425d85a3e3bd"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"yaml-rust2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@ -868,6 +913,15 @@ version = "0.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@ -1485,7 +1539,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1596,6 +1650,7 @@ dependencies = [
|
|||||||
"dotenv",
|
"dotenv",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"gray_matter",
|
||||||
"http",
|
"http",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"image",
|
"image",
|
||||||
@ -2123,7 +2178,7 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"fallible-streaming-iterator",
|
"fallible-streaming-iterator",
|
||||||
"hashlink",
|
"hashlink 0.11.1",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlite-wasm-rs",
|
"sqlite-wasm-rs",
|
||||||
@ -2188,7 +2243,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2247,7 +2302,7 @@ dependencies = [
|
|||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2656,7 +2711,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3347,7 +3402,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3657,6 +3712,17 @@ version = "0.6.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaml-rust2"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
|
||||||
|
dependencies = [
|
||||||
|
"arraydeque",
|
||||||
|
"encoding_rs",
|
||||||
|
"hashlink 0.8.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|||||||
@ -41,6 +41,7 @@ rustls = { version = "0.23", features = ["ring"] }
|
|||||||
wechatbot = { path = "vendor/wechatbot" }
|
wechatbot = { path = "vendor/wechatbot" }
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
gray_matter = { version = "0.2", default-features = false, features = ["yaml"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-sys = { version = "0.59", features = [
|
windows-sys = { version = "0.59", features = [
|
||||||
|
|||||||
@ -795,11 +795,14 @@ fn load_experts_from_root(root: &Path, source: ExpertSource) -> Vec<Expert> {
|
|||||||
fn parse_expert_file(path: &Path, source: ExpertSource) -> Result<Expert, String> {
|
fn parse_expert_file(path: &Path, source: ExpertSource) -> Result<Expert, String> {
|
||||||
let content = fs::read_to_string(path).map_err(|e| format!("failed to read file: {}", e))?;
|
let content = fs::read_to_string(path).map_err(|e| format!("failed to read file: {}", e))?;
|
||||||
|
|
||||||
let (frontmatter_raw, body) =
|
let (frontmatter, body) = match crate::frontmatter::parse::<ExpertFrontmatter>(&content) {
|
||||||
split_frontmatter(&content).ok_or_else(|| "missing YAML frontmatter block".to_string())?;
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
let frontmatter: ExpertFrontmatter = serde_yaml::from_str(frontmatter_raw)
|
let bytes = content.len();
|
||||||
.map_err(|e| format!("invalid YAML frontmatter: {}", e))?;
|
let crlf = content.contains('\r');
|
||||||
|
return Err(format!("{} (bytes={}, crlf={})", err, bytes, crlf));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let description = frontmatter.description.trim();
|
let description = frontmatter.description.trim();
|
||||||
if description.is_empty() {
|
if description.is_empty() {
|
||||||
@ -823,26 +826,6 @@ fn parse_expert_file(path: &Path, source: ExpertSource) -> Result<Expert, String
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_frontmatter(content: &str) -> Option<(&str, &str)> {
|
|
||||||
// 兼容 CRLF(Windows)和 LF(Unix)行尾符
|
|
||||||
let rest = content
|
|
||||||
.strip_prefix("---\n")
|
|
||||||
.or_else(|| content.strip_prefix("---\r\n"))?;
|
|
||||||
let marker = "\n---\n";
|
|
||||||
let marker_crlf = "\n---\r\n";
|
|
||||||
if let Some(idx) = rest.find(marker) {
|
|
||||||
let frontmatter = &rest[..idx];
|
|
||||||
let body = &rest[idx + marker.len()..];
|
|
||||||
Some((frontmatter, body))
|
|
||||||
} else if let Some(idx) = rest.find(marker_crlf) {
|
|
||||||
let frontmatter = &rest[..idx];
|
|
||||||
let body = &rest[idx + marker_crlf.len()..];
|
|
||||||
Some((frontmatter, body))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== State file I/O ==========
|
// ========== State file I/O ==========
|
||||||
|
|
||||||
fn load_expert_disable_state(cwd: &Path) -> ExpertDisableState {
|
fn load_expert_disable_state(cwd: &Path) -> ExpertDisableState {
|
||||||
@ -992,11 +975,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_split_frontmatter() {
|
fn test_parse_expert_file_handles_crlf_endings() {
|
||||||
let input = "---\ndescription: demo\n---\nhello";
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let (fm, body) = split_frontmatter(input).unwrap();
|
let expert_dir = dir.path().join("demo");
|
||||||
assert!(fm.contains("description"));
|
fs::create_dir_all(&expert_dir).unwrap();
|
||||||
assert_eq!(body, "hello");
|
let expert_md = expert_dir.join("EXPERT.md");
|
||||||
|
fs::write(&expert_md, "---\r\ndescription: demo expert\r\n---\r\nStep A\r\nStep B").unwrap();
|
||||||
|
|
||||||
|
let expert = parse_expert_file(&expert_md, ExpertSource::Project).unwrap();
|
||||||
|
assert_eq!(expert.name, "demo");
|
||||||
|
assert_eq!(expert.description, "demo expert");
|
||||||
|
assert_eq!(expert.body, "Step A\nStep B");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
86
src/frontmatter.rs
Normal file
86
src/frontmatter.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use gray_matter::engine::YAML;
|
||||||
|
use gray_matter::Matter;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
/// Parse a markdown document with YAML frontmatter into `(frontmatter, body)`.
|
||||||
|
///
|
||||||
|
/// Tolerates CRLF, CR, and LF line endings. A `---` appearing inside the body
|
||||||
|
/// is not treated as a delimiter (only the leading frontmatter block is split).
|
||||||
|
///
|
||||||
|
/// - Returns `Err("missing YAML frontmatter block")` when no leading `---`
|
||||||
|
/// delimiter is present.
|
||||||
|
/// - Returns `Err("invalid YAML frontmatter: {e}")` when the block is present
|
||||||
|
/// but the YAML fails to parse or deserialize into `T`.
|
||||||
|
pub fn parse<T: DeserializeOwned>(content: &str) -> Result<(T, String), String> {
|
||||||
|
let normalized = content.replace("\r\n", "\n").replace('\r', "\n");
|
||||||
|
|
||||||
|
if !normalized.starts_with("---\n") {
|
||||||
|
return Err("missing YAML frontmatter block".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let matter = Matter::<YAML>::new();
|
||||||
|
let result = matter.parse(&normalized);
|
||||||
|
let pod = result
|
||||||
|
.data
|
||||||
|
.ok_or_else(|| "invalid YAML frontmatter".to_string())?;
|
||||||
|
let data: T = pod
|
||||||
|
.deserialize()
|
||||||
|
.map_err(|e| format!("invalid YAML frontmatter: {}", e))?;
|
||||||
|
Ok((data, result.content))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, PartialEq)]
|
||||||
|
struct FrontMatter {
|
||||||
|
description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_lf_endings() {
|
||||||
|
let input = "---\ndescription: demo\n---\nbody text";
|
||||||
|
let (fm, body) = parse::<FrontMatter>(input).unwrap();
|
||||||
|
assert_eq!(fm, FrontMatter { description: "demo".to_string(), name: None });
|
||||||
|
assert_eq!(body, "body text");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_crlf_endings() {
|
||||||
|
let input = "---\r\ndescription: demo\r\n---\r\nbody text";
|
||||||
|
let (fm, body) = parse::<FrontMatter>(input).unwrap();
|
||||||
|
assert_eq!(fm.description, "demo");
|
||||||
|
assert_eq!(body, "body text");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_cr_endings() {
|
||||||
|
let input = "---\rdescription: demo\r---\rbody text";
|
||||||
|
let (fm, body) = parse::<FrontMatter>(input).unwrap();
|
||||||
|
assert_eq!(fm.description, "demo");
|
||||||
|
assert_eq!(body, "body text");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_block_is_rejected() {
|
||||||
|
let err = parse::<FrontMatter>("no front matter here").unwrap_err();
|
||||||
|
assert_eq!(err, "missing YAML frontmatter block");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_yaml_is_rejected() {
|
||||||
|
let err = parse::<FrontMatter>("---\n: : bad\n---\nbody").unwrap_err();
|
||||||
|
assert!(err.starts_with("invalid YAML frontmatter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn body_with_inner_delimiter_is_preserved() {
|
||||||
|
let input = "---\ndescription: demo\n---\nexcerpt\n---\nmore content";
|
||||||
|
let (_fm, body) = parse::<FrontMatter>(input).unwrap();
|
||||||
|
assert_eq!(body, "excerpt\n---\nmore content");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ pub mod command;
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod experts;
|
pub mod experts;
|
||||||
|
pub mod frontmatter;
|
||||||
pub mod gateway;
|
pub mod gateway;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
|
|||||||
@ -867,11 +867,14 @@ struct SkillFrontmatter {
|
|||||||
fn parse_skill_file(path: &Path, source: SkillSource) -> Result<Skill, String> {
|
fn parse_skill_file(path: &Path, source: SkillSource) -> Result<Skill, String> {
|
||||||
let content = fs::read_to_string(path).map_err(|e| format!("failed to read file: {}", e))?;
|
let content = fs::read_to_string(path).map_err(|e| format!("failed to read file: {}", e))?;
|
||||||
|
|
||||||
let (frontmatter_raw, body) =
|
let (frontmatter, body) = match crate::frontmatter::parse::<SkillFrontmatter>(&content) {
|
||||||
split_frontmatter(&content).ok_or_else(|| "missing YAML frontmatter block".to_string())?;
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
let frontmatter: SkillFrontmatter = serde_yaml::from_str(frontmatter_raw)
|
let bytes = content.len();
|
||||||
.map_err(|e| format!("invalid YAML frontmatter: {}", e))?;
|
let crlf = content.contains('\r');
|
||||||
|
return Err(format!("{} (bytes={}, crlf={})", err, bytes, crlf));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let description = frontmatter.description.trim();
|
let description = frontmatter.description.trim();
|
||||||
if description.is_empty() {
|
if description.is_empty() {
|
||||||
@ -895,15 +898,6 @@ fn parse_skill_file(path: &Path, source: SkillSource) -> Result<Skill, String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_frontmatter(content: &str) -> Option<(&str, &str)> {
|
|
||||||
let rest = content.strip_prefix("---\n")?;
|
|
||||||
let marker = "\n---\n";
|
|
||||||
let idx = rest.find(marker)?;
|
|
||||||
let frontmatter = &rest[..idx];
|
|
||||||
let body = &rest[idx + marker.len()..];
|
|
||||||
Some((frontmatter, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 platform 模块提供的 xml_escape 和 path_to_uri 函数
|
// 使用 platform 模块提供的 xml_escape 和 path_to_uri 函数
|
||||||
|
|
||||||
// SkillPromptProvider 实现
|
// SkillPromptProvider 实现
|
||||||
@ -999,11 +993,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_split_frontmatter() {
|
fn test_parse_skill_file_handles_crlf_endings() {
|
||||||
let input = "---\ndescription: demo\n---\nhello";
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let (fm, body) = split_frontmatter(input).unwrap();
|
let skill_dir = dir.path().join("demo");
|
||||||
assert!(fm.contains("description"));
|
fs::create_dir_all(&skill_dir).unwrap();
|
||||||
assert_eq!(body, "hello");
|
let skill_md = skill_dir.join("SKILL.md");
|
||||||
|
fs::write(&skill_md, "---\r\ndescription: demo skill\r\n---\r\nStep A\r\nStep B").unwrap();
|
||||||
|
|
||||||
|
let skill = parse_skill_file(&skill_md, SkillSource::Project).unwrap();
|
||||||
|
assert_eq!(skill.name, "demo");
|
||||||
|
assert_eq!(skill.description, "demo skill");
|
||||||
|
assert_eq!(skill.body, "Step A\nStep B");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -1467,11 +1467,14 @@ fn parse_subagent_file(path: &Path, source: SubagentSource) -> Result<SubagentDe
|
|||||||
let content = fs::read_to_string(path)
|
let content = fs::read_to_string(path)
|
||||||
.map_err(|e| format!("failed to read file: {}", e))?;
|
.map_err(|e| format!("failed to read file: {}", e))?;
|
||||||
|
|
||||||
let (frontmatter_raw, body) = split_frontmatter(&content)
|
let (frontmatter, body) = match crate::frontmatter::parse::<SubagentFrontmatter>(&content) {
|
||||||
.ok_or_else(|| "missing YAML frontmatter block".to_string())?;
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
let frontmatter: SubagentFrontmatter = serde_yaml::from_str(frontmatter_raw)
|
let bytes = content.len();
|
||||||
.map_err(|e| format!("invalid YAML frontmatter: {}", e))?;
|
let crlf = content.contains('\r');
|
||||||
|
return Err(format!("{} (bytes={}, crlf={})", err, bytes, crlf));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if frontmatter.description.trim().is_empty() {
|
if frontmatter.description.trim().is_empty() {
|
||||||
return Err("description is required and cannot be empty".to_string());
|
return Err("description is required and cannot be empty".to_string());
|
||||||
@ -1501,37 +1504,6 @@ fn parse_subagent_file(path: &Path, source: SubagentSource) -> Result<SubagentDe
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 分割 frontmatter 和 body
|
|
||||||
fn split_frontmatter(content: &str) -> Option<(&str, &str)> {
|
|
||||||
// 跳过开头的 ---
|
|
||||||
let content = content
|
|
||||||
.strip_prefix("---")
|
|
||||||
.or_else(|| content.strip_prefix("---"))?;
|
|
||||||
|
|
||||||
// 跳过 --- 后的换行符和可能的空行
|
|
||||||
let content = content.trim_start_matches('\r').trim_start_matches('\n');
|
|
||||||
|
|
||||||
// 找结束标记(容忍不同的换行符格式和前面的空行)
|
|
||||||
// 尝试多种可能的结束标记格式
|
|
||||||
let end_markers = ["\n---\n", "\n---", "\r\n---\r\n", "\r\n---"];
|
|
||||||
let mut idx = None;
|
|
||||||
let mut marker_len = 0;
|
|
||||||
for marker in end_markers {
|
|
||||||
if let Some(pos) = content.find(marker) {
|
|
||||||
idx = Some(pos);
|
|
||||||
marker_len = marker.len();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let idx = idx?;
|
|
||||||
|
|
||||||
let frontmatter = &content[..idx];
|
|
||||||
let body = &content[idx + marker_len..];
|
|
||||||
let body = body.trim_start_matches('\r').trim_start_matches('\n');
|
|
||||||
|
|
||||||
Some((frontmatter, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -1817,11 +1789,19 @@ mod tests {
|
|||||||
// ===== frontmatter 解析(denied_tools)测试 =====
|
// ===== frontmatter 解析(denied_tools)测试 =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn split_frontmatter_extracts_yaml_and_body() {
|
fn parse_subagent_file_handles_crlf_endings() {
|
||||||
let content = "---\nname: x\ndescription: y\n---\nbody text";
|
let temp = tempfile::tempdir().unwrap();
|
||||||
let (fm, body) = split_frontmatter(content).expect("should split");
|
let path = temp.path().join("SUBAGENT.md");
|
||||||
assert!(fm.contains("description: y"));
|
std::fs::write(
|
||||||
assert_eq!(body, "body text");
|
&path,
|
||||||
|
"---\r\nname: demo\r\ndescription: demo subagent\r\n---\r\nStep A\r\nStep B",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let subagent = parse_subagent_file(&path, SubagentSource::Project).unwrap();
|
||||||
|
assert_eq!(subagent.name, "demo");
|
||||||
|
assert_eq!(subagent.description, "demo subagent");
|
||||||
|
assert_eq!(subagent.body.as_deref(), Some("Step A\nStep B"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user