优化: 项目文件树/依赖图组件 + 灵感对抗校验 + 小程序脚本 + 模块管理

This commit is contained in:
lxy
2026-08-10 00:00:31 +08:00
parent 6bafdcd5a5
commit cf00345c35
19 changed files with 521 additions and 185 deletions
+1
View File
@@ -71,6 +71,7 @@ pub(crate) async fn ensure_conversation_title(
prompt_cache_hit_tokens: None,
prompt_cache_miss_tokens: None,
reasoning_tokens: None,
is_estimated: None,
timestamp: m.timestamp,
})
.collect();
+50 -6
View File
@@ -474,7 +474,7 @@ pub async fn get_module_git_status(
Ok(serde_json::to_value(status).map_err(err_str)?)
}
/// 列出工程本地分支(只读)。返回 { current, branches: [{ name, is_current }] }。
/// 列出工程分支(本地 + 远程跟踪,只读)。返回 { current, branches: [{ name, is_current }] }。
#[tauri::command]
pub async fn list_branches(
state: State<'_, AppState>,
@@ -497,10 +497,15 @@ pub async fn list_branches(
let dir = module.path.clone();
// git 命令在 spawn_blocking 中执行(阻塞 IO 不污染 async runtime),10s 超时
let result = tokio::task::spawn_blocking(move || -> (String, Vec<serde_json::Value>) {
// git branch --format="%(HEAD)%00%(refname:short)"
// git branch -a --format="%(HEAD)%00%(refname:short)%00%(refname)"
// -a 同时列出本地分支(refs/heads/*)与远程跟踪分支(refs/remotes/*,短名形如 origin/main);
// 原仅 `git branch` 只列本地分支,远程分支被排除(仓库只见一个分支的根因)。
let out = run_git_cmd(
std::path::Path::new(&dir),
&["branch", "--format=%(HEAD)%00%(refname:short)"],
&[
"branch", "-a",
"--format=%(HEAD)%00%(refname:short)%00%(refname)",
],
std::time::Duration::from_secs(10),
)
.unwrap_or_default();
@@ -509,10 +514,16 @@ pub async fn list_branches(
for line in out.lines() {
let line = line.trim();
if line.is_empty() { continue; }
// 格式: "*\0branch_name" 或 "\0branch_name"
let (head, name) = line.split_once('\u{0}').unwrap_or(("", line));
// 格式: "*\0branch_name\0refs/...",三字段(HEAD 标记 / 短名 / 完整 ref)。
let parts: Vec<&str> = line.split('\u{0}').collect();
if parts.len() < 2 { continue; }
let (head, short) = (parts[0], parts[1]);
// 跳过 remote HEAD 伪分支(refs/remotes/<remote>/HEAD → 短名即裸 remote 名如 origin,
// 指向远端默认分支,非真实分支,列出会误导)。
let full = parts.get(2).copied().unwrap_or("");
if full.ends_with("/HEAD") { continue; }
let is_current = head.contains('*');
let name = name.trim().to_string();
let name = short.trim().to_string();
if name.is_empty() { continue; }
if is_current { current = name.clone(); }
branches.push(serde_json::json!({ "name": name, "is_current": is_current }));
@@ -655,6 +666,30 @@ const NOISE_DIRS: &[&str] = &[
".vite",
];
/// 判断目录是否含可见子项(过滤隐藏文件/噪音目录后,与 `get_module_file_tree` 展开实际展示一致)。
///
/// 供 `FileTreeEntry.has_children` 用:前端未展开目录即可据此在行内标注空目录,
/// 不必等用户展开才发现是空的。`read_dir` 首个子项命中即短路返回,开销为 O(可见项数)。
fn dir_has_visible_children(dir: &Path) -> bool {
let Ok(read) = std::fs::read_dir(dir) else {
// 目录不可读(权限等)按空处理,避免误导为有内容
return false;
};
read.flatten().any(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
// 与列目录过滤保持一致:隐藏文件 + 噪音目录不算可见子项
if name.starts_with('.') {
return false;
}
if NOISE_DIRS.iter().any(|n| *n == name.as_ref()) {
return false;
}
// 与列目录一致:无法判定类型的条目(断链符号链接等)视为不可见
entry.file_type().is_ok()
})
}
/// 文件树条目(单层;前端点击文件夹再懒加载下一层)。
#[derive(Debug, Serialize)]
struct FileTreeEntry {
@@ -668,6 +703,9 @@ struct FileTreeEntry {
/// 文件夹恒为 None(只标文件)。
#[serde(skip_serializing_if = "Option::is_none")]
git_status: Option<String>,
/// 目录是否含可见子项(过滤隐藏/噪音目录后,与展开实际展示一致)。
/// 文件恒为 false;目录在列目录时即时统计。前端未展开时行内标注空目录用。
has_children: bool,
}
/// 列出工程目录(可钻入子目录)的文件树(单层 + git 状态合并)。
@@ -771,12 +809,18 @@ pub async fn get_module_file_tree(
} else {
git_map_clone.get(&posix_rel).cloned()
};
let has_children = if is_dir {
dir_has_visible_children(&entry.path())
} else {
false
};
items.push(FileTreeEntry {
name,
path: posix_rel,
is_dir,
size,
git_status,
has_children,
});
}
// 目录优先,各自字母序(稳定可预期,前端无需再排)。