feat: complete Phase 1-3 architecture optimization
Phase 1 - Security: - Add AES-GCM encryption for localStorage fallback - Enforce WSS protocol for non-localhost WebSocket connections - Add URL sanitization to prevent XSS in markdown links Phase 2 - Domain Reorganization: - Create Intelligence Domain with Valtio store and caching - Add unified intelligence-client for Rust backend integration - Migrate from legacy agent-memory, heartbeat, reflection modules Phase 3 - Core Optimization: - Add virtual scrolling for ChatArea with react-window - Implement LRU cache with TTL for intelligence operations - Add message virtualization utilities Additional: - Add OpenFang compatibility test suite - Update E2E test fixtures - Add audit logging infrastructure - Update project documentation and plans Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
162
desktop/src/lib/audit-logger.ts
Normal file
162
desktop/src/lib/audit-logger.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* audit-logger.ts - 前端审计日志记录工具
|
||||
*
|
||||
* 为 ZCLAW 前端操作提供统一的审计日志记录功能。
|
||||
* 记录关键操作(Hand 触发、Agent 创建等)到本地存储。
|
||||
*/
|
||||
|
||||
export type AuditAction =
|
||||
| 'hand.trigger'
|
||||
| 'hand.approve'
|
||||
| 'hand.cancel'
|
||||
| 'agent.create'
|
||||
| 'agent.update'
|
||||
| 'agent.delete';
|
||||
|
||||
export type AuditResult = 'success' | 'failure' | 'pending';
|
||||
|
||||
export interface FrontendAuditEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
action: AuditAction;
|
||||
target: string;
|
||||
result: AuditResult;
|
||||
actor?: string;
|
||||
details?: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface AuditLogOptions {
|
||||
action: AuditAction;
|
||||
target: string;
|
||||
result: AuditResult;
|
||||
actor?: string;
|
||||
details?: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'zclaw-audit-logs';
|
||||
const MAX_LOCAL_LOGS = 500;
|
||||
|
||||
function generateId(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `audit_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
function getTimestamp(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function loadLocalLogs(): FrontendAuditEntry[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (!stored) return [];
|
||||
const logs = JSON.parse(stored) as FrontendAuditEntry[];
|
||||
return Array.isArray(logs) ? logs : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveLocalLogs(logs: FrontendAuditEntry[]): void {
|
||||
try {
|
||||
const trimmedLogs = logs.slice(-MAX_LOCAL_LOGS);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmedLogs));
|
||||
} catch (err) {
|
||||
console.error('[AuditLogger] Failed to save logs to localStorage:', err);
|
||||
}
|
||||
}
|
||||
|
||||
class AuditLogger {
|
||||
private logs: FrontendAuditEntry[] = [];
|
||||
private initialized = false;
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
if (this.initialized) return;
|
||||
this.logs = loadLocalLogs();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
async log(options: AuditLogOptions): Promise<FrontendAuditEntry> {
|
||||
const entry: FrontendAuditEntry = {
|
||||
id: generateId(),
|
||||
timestamp: getTimestamp(),
|
||||
action: options.action,
|
||||
target: options.target,
|
||||
result: options.result,
|
||||
actor: options.actor,
|
||||
details: options.details,
|
||||
error: options.error,
|
||||
};
|
||||
|
||||
this.logs.push(entry);
|
||||
saveLocalLogs(this.logs);
|
||||
|
||||
console.log('[AuditLogger]', entry.action, entry.target, entry.result, entry.details || '');
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
async logSuccess(
|
||||
action: AuditAction,
|
||||
target: string,
|
||||
details?: Record<string, unknown>
|
||||
): Promise<FrontendAuditEntry> {
|
||||
return this.log({ action, target, result: 'success', details });
|
||||
}
|
||||
|
||||
async logFailure(
|
||||
action: AuditAction,
|
||||
target: string,
|
||||
error: string,
|
||||
details?: Record<string, unknown>
|
||||
): Promise<FrontendAuditEntry> {
|
||||
return this.log({ action, target, result: 'failure', error, details });
|
||||
}
|
||||
|
||||
getLogs(): FrontendAuditEntry[] {
|
||||
return [...this.logs];
|
||||
}
|
||||
|
||||
getLogsByAction(action: AuditAction): FrontendAuditEntry[] {
|
||||
return this.logs.filter(log => log.action === action);
|
||||
}
|
||||
|
||||
clearLogs(): void {
|
||||
this.logs = [];
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
exportLogs(): string {
|
||||
return JSON.stringify(this.logs, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
export const auditLogger = new AuditLogger();
|
||||
|
||||
export function logAudit(options: AuditLogOptions): Promise<FrontendAuditEntry> {
|
||||
return auditLogger.log(options);
|
||||
}
|
||||
|
||||
export function logAuditSuccess(
|
||||
action: AuditAction,
|
||||
target: string,
|
||||
details?: Record<string, unknown>
|
||||
): Promise<FrontendAuditEntry> {
|
||||
return auditLogger.logSuccess(action, target, details);
|
||||
}
|
||||
|
||||
export function logAuditFailure(
|
||||
action: AuditAction,
|
||||
target: string,
|
||||
error: string,
|
||||
details?: Record<string, unknown>
|
||||
): Promise<FrontendAuditEntry> {
|
||||
return auditLogger.logFailure(action, target, error, details);
|
||||
}
|
||||
@@ -14,8 +14,6 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { isTauriRuntime } from './tauri-gateway';
|
||||
import {
|
||||
arrayToBase64,
|
||||
base64ToArray,
|
||||
deriveKey,
|
||||
encrypt,
|
||||
decrypt,
|
||||
|
||||
Reference in New Issue
Block a user