Skip to main content

yggdrasil/api/
writing_guide.rs

1//! 公开写作指南。源码与下载共用 public 中的文件,不读取令牌或用户数据。
2#![allow(clippy::unused_unit, deprecated)]
3
4use dioxus::prelude::*;
5
6pub const SKILL_SOURCE: &str = include_str!("../../public/skills/yggdrasil-writing/SKILL.md");
7pub const SKILL_URL: &str = "/skills/yggdrasil-writing/SKILL.md";
8
9#[derive(Clone, serde::Serialize, serde::Deserialize)]
10pub struct WritingGuideData {
11    pub html: String,
12    pub toc_html: String,
13    pub configs: [String; 3],
14    pub install_prompt: String,
15}
16
17/// Frontmatter 留在下载及复制的原文中;阅读版从 Markdown 正文开始。
18#[cfg(feature = "server")]
19fn render_guide() -> crate::api::markdown::RenderedContent {
20    let body = SKILL_SOURCE
21        .strip_prefix("---\n")
22        .and_then(|text| text.split_once("\n---\n"))
23        .map(|(_, body)| body.trim_start())
24        .unwrap_or(SKILL_SOURCE);
25    // 页面已有 h1,文档标题由面板的 h2 承担。
26    let body = body.strip_prefix("# Yggdrasil 文章写作\n").unwrap_or(body);
27    crate::api::markdown::render_markdown_enhanced(body)
28}
29
30#[server]
31pub async fn get_writing_guide() -> Result<WritingGuideData, ServerFnError> {
32    static RENDERED: std::sync::LazyLock<crate::api::markdown::RenderedContent> =
33        std::sync::LazyLock::new(render_guide);
34    let base_url = crate::api::settings::runtime_security_settings()
35        .await
36        .app_base_url;
37    let base_url = if base_url.trim().is_empty() {
38        crate::mcp::config::base_url_from_env()
39    } else {
40        base_url
41    };
42    let configs = crate::mcp::config::generate_client_configs(&base_url, "YOUR_YGGDRASIL_TOKEN");
43    Ok(WritingGuideData {
44        html: RENDERED.html.clone(),
45        toc_html: RENDERED.toc_html.clone(),
46        configs: [configs.codex_toml, configs.omp_json, configs.opencode_json],
47        install_prompt: format!(
48            "请从 {}{SKILL_URL} 下载完整的 SKILL.md,将 yggdrasil-writing 安装为用户全局 skill,保存到 ~/.agents/skills/yggdrasil-writing/SKILL.md(不要安装到项目目录),并确认当前 Agent 能发现该技能。",
49            base_url.trim_end_matches('/')
50        ),
51    })
52}
53
54#[cfg(all(test, feature = "server"))]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn guide_renders_documentation_without_activating_example_blocks() {
60        let rendered = render_guide();
61        assert!(!rendered.html.contains("description:"));
62        assert!(!rendered.html.contains("<h1"));
63        assert!(rendered.html.contains("<h2"));
64        assert!(rendered.html.contains("<table"));
65        assert!(rendered.html.contains("language-markdown"));
66        assert!(!rendered.html.contains("data-runnable=\"true\""));
67        assert!(!rendered.html.contains("class=\"language-mermaid\""));
68        assert!(rendered.toc_html.contains("可运行代码块"));
69        assert!(SKILL_SOURCE.starts_with("---\nname: yggdrasil-writing\n"));
70    }
71}