fix(ui): 审计修复 — 路径规范化/SkillInfo类型/分页offset/初始加载/显示统一
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- workspace.rs: canonicalize() 解析 '..' 和符号链接
- Workspace.tsx: 组件挂载时调用 loadDirStats + 统一 KB 显示
- configStore: SkillInfo 接口补充 category 字段 + 空数组回退注释
- securityStore: localStorage 审计日志添加 offset 分页支持
This commit is contained in:
iven
2026-04-10 23:24:32 +08:00
parent 1d0e60d028
commit 550e525554
4 changed files with 18 additions and 9 deletions

View File

@@ -13,20 +13,23 @@ pub struct DirStats {
#[tauri::command] #[tauri::command]
pub async fn workspace_dir_stats(path: String) -> Result<DirStats, String> { pub async fn workspace_dir_stats(path: String) -> Result<DirStats, String> {
let dir = Path::new(&path); let dir = Path::new(&path);
if !dir.exists() {
// Canonicalize to resolve '..' components and symlinks
let canonical = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
if !canonical.exists() {
return Ok(DirStats { return Ok(DirStats {
file_count: 0, file_count: 0,
total_size: 0, total_size: 0,
}); });
} }
if !dir.is_dir() { if !canonical.is_dir() {
return Err(format!("{} is not a directory", path)); return Err(format!("{} is not a directory", path));
} }
let mut file_count: u64 = 0; let mut file_count: u64 = 0;
let mut total_size: u64 = 0; let mut total_size: u64 = 0;
let entries = std::fs::read_dir(dir).map_err(|e| format!("Failed to read dir: {}", e))?; let entries = std::fs::read_dir(&canonical).map_err(|e| format!("Failed to read dir: {}", e))?;
for entry in entries.flatten() { for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() { if let Ok(metadata) = entry.metadata() {
if metadata.is_file() { if metadata.is_file() {

View File

@@ -26,7 +26,9 @@ export function Workspace() {
}, []); }, []);
useEffect(() => { useEffect(() => {
setProjectDir(quickConfig.workspaceDir || workspaceInfo?.path || '~/.zclaw/zclaw-workspace'); const dir = quickConfig.workspaceDir || workspaceInfo?.path || '~/.zclaw/zclaw-workspace';
setProjectDir(dir);
loadDirStats(dir);
}, [quickConfig.workspaceDir, workspaceInfo?.path]); }, [quickConfig.workspaceDir, workspaceInfo?.path]);
const handleWorkspaceBlur = async () => { const handleWorkspaceBlur = async () => {
@@ -86,8 +88,8 @@ export function Workspace() {
<div>{workspaceInfo?.resolvedPath || projectDir}</div> <div>{workspaceInfo?.resolvedPath || projectDir}</div>
<div> <div>
{dirStats?.fileCount ?? workspaceInfo?.fileCount ?? 0} {dirStats?.fileCount ?? workspaceInfo?.fileCount ?? 0}
{dirStats && `,大小:${(dirStats.totalSize / 1024).toFixed(1)} KB`} {((dirStats?.totalSize ?? workspaceInfo?.totalSize ?? 0) > 0) &&
{!dirStats && workspaceInfo?.totalSize ? `,大小:${workspaceInfo.totalSize} bytes` : ''} `,大小:${(((dirStats?.totalSize ?? workspaceInfo?.totalSize) ?? 0) / 1024).toFixed(1)} KB`}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -81,6 +81,7 @@ export interface SkillInfo {
capabilities?: string[]; capabilities?: string[];
tags?: string[]; tags?: string[];
mode?: string; mode?: string;
category?: string;
triggers?: Array<{ type: string; pattern?: string }>; triggers?: Array<{ type: string; pattern?: string }>;
actions?: Array<{ type: string; params?: Record<string, unknown> }>; actions?: Array<{ type: string; params?: Record<string, unknown> }>;
enabled?: boolean; enabled?: boolean;
@@ -455,6 +456,8 @@ export const useConfigStore = create<ConfigStateSlice & ConfigActionsSlice>((set
if (client) { if (client) {
try { try {
const result = await client.listSkills(); const result = await client.listSkills();
// Empty array from client may indicate connected-but-load-failure;
// fall through to direct Tauri invoke as recovery path
if (result?.skills && result.skills.length > 0) { if (result?.skills && result.skills.length > 0) {
set({ skillsCatalog: result.skills }); set({ skillsCatalog: result.skills });
if (result.extraDirs) { if (result.extraDirs) {

View File

@@ -309,10 +309,11 @@ export const useSecurityStore = create<SecurityStore>((set, get) => ({
})); }));
} }
// Sort by timestamp descending and apply limit // Sort by timestamp descending and apply pagination
allLogs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); allLogs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
const limited = opts?.limit ? allLogs.slice(0, opts.limit) : allLogs; const offset = opts?.offset ?? 0;
set({ auditLogs: limited, auditLogsLoading: false }); const paginated = opts?.limit ? allLogs.slice(offset, offset + opts.limit) : allLogs.slice(offset);
set({ auditLogs: paginated, auditLogsLoading: false });
} catch { } catch {
set({ auditLogsLoading: false }); set({ auditLogsLoading: false });
} }