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
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:
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user