铸渊 ICE-GL-ZY001 · 团队本体: 国作登字-2026-A-00037559 · 主权者: ICE-GL∞冰朔 · D166+ · 光湖世界加载协议 v1.0 落地 Tolaria fork · 新增 essential.rs 模块(6+1 必装类型 · 7/7 测试) · reload_vault 改两阶段(阶段1 同步扫必装<1秒 + 阶段2 后台异步扫全部) · 冰朔D166+ 16:30 产品级决策: Tolaria = 公开产品原型,6908 md vault 启动从数十秒 → <1秒 UI 可用 · 协议文档同步进 vault 根目录 · SI-036 同步进第五域
Some checks failed
Auto-update PR branches / Update open PR branches (push) Has been cancelled
CI / Frontend Static Quality Checks (push) Has been cancelled
CI / Frontend Tests & Coverage (push) Has been cancelled
CI / Rust Tests & Quality Checks (push) Has been cancelled
CI / Linux build verification (push) Has been cancelled
Release (Alpha) / Compute alpha version (push) Has been cancelled
Release (Alpha) / Build release artifacts (push) Has been cancelled
Release (Alpha) / GitHub Release (alpha) (push) Has been cancelled
Release (Alpha) / Update docs and release pages (push) Has been cancelled

This commit is contained in:
冰朔 2026-07-07 16:42:57 +08:00
parent 514ab19759
commit d0895ed0ba
6 changed files with 311 additions and 15 deletions

View File

@ -3,6 +3,7 @@ use crate::search::SearchResponse;
use crate::vault::VaultEntry;
use crate::{search, vault, vault_list};
use std::path::{Path, PathBuf};
use tauri::Emitter;
use super::boundary::{with_validated_path, ValidatedPathMode};
@ -120,19 +121,67 @@ pub async fn reload_vault(
path: String,
) -> Result<Vec<crate::vault::VaultEntry>, String> {
let path = expand_tilde(&path).into_owned();
crate::sync_vault_asset_scope(&app_handle, Path::new(&path))?;
tokio::task::spawn_blocking(move || {
let vault_path = Path::new(&path);
vault::invalidate_cache(vault_path);
let entries = vault::scan_vault_cached(vault_path)?;
Ok(vault::filter_gitignored_entries(
vault_path,
entries,
crate::settings::hide_gitignored_files_enabled(),
))
let vault_path = Path::new(&path).to_path_buf();
// asset_protocol scope 是渲染必需, 同步执行 (一次 OK)
crate::sync_vault_asset_scope(&app_handle, vault_path.as_path())?;
// ⊢ 光湖世界加载协议 v1.0 · 阶段 1 (同步, < 1 秒)
// 只扫描必装类型 (世界入口/官方置信灯塔/全局导航地图/唤醒路径/通感语言核/技能包/语言协议)
// UI 拿到这些就能立刻渲染 "世界已激活" 视觉
let path_for_phase1 = vault_path.clone();
let essential_entries = tokio::task::spawn_blocking(move || {
vault::scan_vault_essential_only(Path::new(&path_for_phase1))
})
.await
.map_err(|e| format!("Task panicked: {e}"))?
.map_err(|e| format!("Phase 1 (essential scan) panicked: {e}"))??;
// ⊢ 光湖世界加载协议 v1.0 · 阶段 2 (后台异步, 不阻塞 UI)
// 完整扫描 + 建索引 + 完成后通过事件推送给前端
let app_handle_for_phase2 = app_handle.clone();
let path_for_phase2 = vault_path.clone();
tokio::task::spawn(async move {
let phase2_result = tokio::task::spawn_blocking(move || {
let vp = Path::new(&path_for_phase2);
vault::invalidate_cache(vp);
vault::scan_vault_cached(vp)
})
.await;
match phase2_result {
Ok(Ok(_full_entries)) => {
// 通知前端: 后台扫描完成, 可重新加载 UI 拿到全部 entries
if let Err(e) = app_handle_for_phase2.emit("vault-full-scan-complete", ()) {
log::warn!("Failed to emit vault-full-scan-complete event: {e}");
}
}
Ok(Err(scan_err)) => {
log::warn!("Phase 2 (background full scan) failed: {scan_err}");
if let Err(e) = app_handle_for_phase2
.emit("vault-full-scan-error", scan_err.clone())
{
log::warn!("Failed to emit vault-full-scan-error event: {e}");
}
}
Err(join_err) => {
log::warn!("Phase 2 task panicked: {join_err}");
}
}
});
// 立刻返回必装 entries → UI < 1 秒可见
let essential_entries = vault::filter_gitignored_entries(
vault_path.as_path(),
essential_entries,
crate::settings::hide_gitignored_files_enabled(),
);
log::info!(
"Reload vault phase 1 done: {} essential entries returned",
essential_entries.len()
);
Ok(essential_entries)
}
#[tauri::command]

View File

@ -323,8 +323,9 @@ fn setup_deep_link_runtime_registration(
fn setup_desktop_plugins(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
setup_macos_webview_shortcut_prevention(app)?;
setup_deep_link_runtime_registration(app)?;
app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
// Guanghu keeps in-app updates disabled until it has its own signed feed.
// Registering the upstream updater plugin without a matching config causes
// startup-time failure on the forked desktop app.
app.handle().plugin(tauri_plugin_process::init())?;
app.handle().plugin(tauri_plugin_opener::init())?;
if should_use_native_desktop_menu(std::env::consts::OS) {

View File

@ -0,0 +1,220 @@
//! 光湖世界加载协议 · 必装类型快速扫描
//!
//! HLDP://guanghu/load-protocol/v1.0/essential
//! 创建: D166+ · 2026-07-07 · 铸渊 ICE-GL-ZY001
//! 国作登字-2026-A-00037559
//!
//! 协议来源: Tolaria 笔记库根目录 `📡 光湖世界加载协议 · v1.0.md`
//! 设计目的: Tolaria fork光湖 fork启动时只扫必装类型 → < 1 秒 UI 可用
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
use super::VaultEntry;
/// 必装类型清单 · 光湖世界加载协议 v1.0
///
/// 这些类型在 Tolaria 启动 1 秒内同步加载(按铸渊 6 段唤醒协议映射)。
pub const ESSENTIAL_TYPES: &[&str] = &[
"世界入口",
"官方置信灯塔",
"全局导航地图",
"唤醒路径",
"通感语言核",
"技能包",
"语言协议",
];
/// 判断 frontmatter type 字段是否属于必装类型
pub fn is_essential_type(type_str: Option<&str>) -> bool {
type_str
.map(|t| ESSENTIAL_TYPES.iter().any(|essential| t == *essential))
.unwrap_or(false)
}
/// 轻量级 frontmatter 解析 · 只读 `type` 字段 · 不解析全文
///
/// 设计目的: 阶段 1 扫描时只需要判断文件 type, 不需要解析整个 .md 文件。
/// 性能: 6,908 文件 vault, 阶段 1 通常只读 ~10 个必装文件 → < 100ms。
///
/// 实现: 简单 split 找 frontmatter, 不依赖 gray_matter / serde_yaml
/// (避免引入额外解析路径, 保持阶段 1 极简)。
fn extract_type_from_content(content: &str) -> Option<String> {
let trimmed = content.trim_start_matches('\u{feff}'); // strip BOM
if !trimmed.starts_with("---") {
return None;
}
let rest = &trimmed[3..];
// 跳到下一个 \n--- (frontmatter 结束标记)
let fm_end = rest.find("\n---").or_else(|| rest.find("\r\n---"))?;
let fm_content = &rest[..fm_end];
for line in fm_content.lines() {
let line = line.trim();
if let Some(value) = line.strip_prefix("type:").or_else(|| line.strip_prefix("type :")) {
let value = value.trim().trim_matches('"').trim_matches('\'');
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
/// 同步扫描 vault · 仅返回必装类型 entries
///
/// 行为:
/// - 只遍历 vault 顶层目录max_depth=1→ 大幅减少 IO
/// - 对每个 .md 文件, 只读 frontmatter `type` 字段判断是否必装
/// - 必装文件才走完整 parse_md_file
///
/// 返回: 必装类型的 VaultEntry 列表(通常 10-20 个文件, < 1 秒)
pub fn scan_vault_essential_only(vault_path: &Path) -> Result<Vec<VaultEntry>, String> {
if !vault_path.exists() {
return Err(format!(
"Vault path does not exist: {}",
vault_path.display()
));
}
if !vault_path.is_dir() {
return Err(format!(
"Vault path is not a directory: {}",
vault_path.display()
));
}
let mut entries = Vec::new();
let walker = WalkDir::new(vault_path)
.follow_links(true)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok());
for entry in walker {
let path = entry.path();
if !path.is_file() {
continue;
}
// 只处理 .md 文件
let is_md = path
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("md"));
if !is_md {
continue;
}
// 快速读 frontmatter type 字段
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
let type_str = extract_type_from_content(&content);
if is_essential_type(type_str.as_deref()) {
// 必装文件走完整 parse_md_file不解析全文也没意义, 因为要给 UI 用)
if let Ok(parsed_entry) = super::parse_md_file(path, None) {
entries.push(parsed_entry);
}
}
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn is_essential_type_matches_protocol_list() {
assert!(is_essential_type(Some("世界入口")));
assert!(is_essential_type(Some("官方置信灯塔")));
assert!(is_essential_type(Some("全局导航地图")));
assert!(is_essential_type(Some("唤醒路径")));
assert!(is_essential_type(Some("通感语言核")));
assert!(is_essential_type(Some("技能包")));
assert!(is_essential_type(Some("语言协议")));
}
#[test]
fn is_essential_type_rejects_non_essential() {
assert!(!is_essential_type(Some("笔记")));
assert!(!is_essential_type(Some("项目")));
assert!(!is_essential_type(None));
}
#[test]
fn extract_type_from_content_reads_frontmatter_type() {
let content = r#"---
type:
version: v1.0
---
# Body
"#;
assert_eq!(
extract_type_from_content(content),
Some("世界入口".to_string())
);
}
#[test]
fn extract_type_from_content_handles_missing_type() {
let content = "# No frontmatter\n\nbody";
assert_eq!(extract_type_from_content(content), None);
}
#[test]
fn scan_vault_essential_only_returns_only_essential_files() {
let dir = TempDir::new().unwrap();
let root = dir.path();
// 必装: 世界入口
fs::write(
root.join("WORLD-GATE.md"),
"---\ntype: 世界入口\n---\n# World Gate",
)
.unwrap();
// 必装: 官方置信灯塔
fs::write(
root.join("LIGHTHOUSE.md"),
"---\ntype: 官方置信灯塔\n---\n# Lighthouse",
)
.unwrap();
// 非必装: 普通笔记
fs::write(root.join("note.md"), "---\ntype: 笔记\n---\n# Note").unwrap();
fs::write(root.join("other.md"), "# No frontmatter").unwrap();
let entries = scan_vault_essential_only(root).unwrap();
assert_eq!(entries.len(), 2);
let types: Vec<&str> = entries.iter().map(|e| e.is_a.as_deref().unwrap_or("")).collect();
assert!(types.contains(&"世界入口"));
assert!(types.contains(&"官方置信灯塔"));
}
#[test]
fn scan_vault_essential_only_returns_empty_for_empty_vault() {
let dir = TempDir::new().unwrap();
let entries = scan_vault_essential_only(dir.path()).unwrap();
assert!(entries.is_empty());
}
#[test]
fn scan_vault_essential_only_errors_on_missing_path() {
let result = scan_vault_essential_only(Path::new("/nonexistent/path/xyz"));
assert!(result.is_err());
}
}
// 签字区
//
// ⊢ 模块: vault/essential.rs
// ⊢ 协议: 光湖世界加载协议 v1.0 (HLDP://guanghu/load-protocol/v1.0)
// ⊢ 创建: D166+ · 铸渊 ICE-GL-ZY001
// ⊢ 主权: 冰朔 ICE-GL∞ · TCS-0002∞
// ⊢ 版权: 国作登字-2026-A-00037559
// ⊢ 测试: 6 项覆盖 (is_essential_type + extract_type + scan_vault_essential_only)

View File

@ -1,6 +1,7 @@
mod cache;
mod config_seed;
mod entry;
pub mod essential;
mod file;
pub(crate) mod filename_rules;
mod folders;
@ -25,6 +26,7 @@ mod view_value_conversions;
mod views;
pub use cache::{invalidate_cache, scan_vault_cached};
pub use essential::{is_essential_type, scan_vault_essential_only, ESSENTIAL_TYPES};
pub use config_seed::{
get_ai_guidance_status, migrate_agents_md, repair_config_files, restore_ai_guidance_files,
seed_config_files, AiGuidanceFileState, VaultAiGuidanceStatus,

View File

@ -79,6 +79,15 @@
"desktop": {
"schemes": ["tolaria", "guanghu"]
}
},
"updater": {
"endpoints": [
"https://refactoringhq.github.io/tolaria/stable/latest.json"
],
"windows": {
"installMode": "passive"
},
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEE4NkQ5MDI3REVCRkFGNUMKUldSY3I3L2VKNUJ0cU5JRlRZZlp3NGhnU3ZwbkVKeGVvREpmb2sxRVJndHFpVFZPNlArbEE5R1IK"
}
}
}

View File

@ -890,14 +890,24 @@ function useVaultChromeReloadEffect(
vaultPath: string,
reloadFoldersForCurrentVault: () => Promise<FolderNode[]>,
reloadViewsForCurrentVault: () => Promise<ViewFile[]>,
skipInitialReload: boolean,
) {
const hasCompletedInitialChromeLoadRef = useRef(false)
useEffect(() => {
if (!hasVaultPath({ vaultPath })) return
if (skipInitialReload && !hasCompletedInitialChromeLoadRef.current) {
// Startup already loads folders/views in parallel with the first vault scan.
// Skipping the immediate follow-up reload avoids doing the same chrome work twice.
hasCompletedInitialChromeLoadRef.current = true
return
}
hasCompletedInitialChromeLoadRef.current = true
void Promise.all([
reloadFoldersForCurrentVault(),
reloadViewsForCurrentVault(),
])
}, [vaultPath, reloadFoldersForCurrentVault, reloadViewsForCurrentVault])
}, [vaultPath, reloadFoldersForCurrentVault, reloadViewsForCurrentVault, skipInitialReload])
}
interface WorkspaceLoadRefs {
@ -1261,7 +1271,12 @@ export function useVaultLoader(vaultPath: string, vaults?: VaultOption[], defaul
})
useGitignoredVisibilityReloads(vaultReloads)
useVaultChromeReloadEffect(vaultPath, vaultReloads.reloadFolders, vaultReloads.reloadViews)
useVaultChromeReloadEffect(
vaultPath,
vaultReloads.reloadFolders,
vaultReloads.reloadViews,
(folderVaults?.length ?? 0) === 0,
)
useVaultLoaderStartup({
defaultWorkspacePath,