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:
@@ -151,6 +151,8 @@ export function Sidebar({
|
||||
<div className="p-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
aria-label="打开设置"
|
||||
title="设置"
|
||||
className="flex items-center gap-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 bg-gray-600 rounded-full flex items-center justify-center text-white font-bold shadow-sm">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useHandStore, type Hand } from '../store/handStore';
|
||||
import type { Workflow } from '../store/workflowStore';
|
||||
import { useWorkflowStore, type Workflow } from '../store/workflowStore';
|
||||
import {
|
||||
X,
|
||||
Plus,
|
||||
@@ -202,6 +202,7 @@ function StepEditor({ step, hands, index, onUpdate, onRemove, onMoveUp, onMoveDo
|
||||
export function WorkflowEditor({ workflow, isOpen, onClose, onSave, isSaving }: WorkflowEditorProps) {
|
||||
const hands = useHandStore((s) => s.hands);
|
||||
const loadHands = useHandStore((s) => s.loadHands);
|
||||
const getWorkflowDetail = useWorkflowStore((s) => s.getWorkflowDetail);
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [steps, setSteps] = useState<WorkflowStep[]>([]);
|
||||
@@ -219,16 +220,31 @@ export function WorkflowEditor({ workflow, isOpen, onClose, onSave, isSaving }:
|
||||
if (workflow) {
|
||||
setName(workflow.name);
|
||||
setDescription(workflow.description || '');
|
||||
// For edit mode, we'd need to load full workflow details
|
||||
// For now, initialize with empty steps
|
||||
setSteps([]);
|
||||
|
||||
// Load full workflow details including steps
|
||||
getWorkflowDetail(workflow.id)
|
||||
.then((detail: { steps: Array<{ handName: string; name?: string; params?: Record<string, unknown>; condition?: string }> } | undefined) => {
|
||||
if (detail && Array.isArray(detail.steps)) {
|
||||
const editorSteps: WorkflowStep[] = detail.steps.map((step: { handName: string; name?: string; params?: Record<string, unknown>; condition?: string }, index: number) => ({
|
||||
id: `step-${workflow.id}-${index}`,
|
||||
handName: step.handName || '',
|
||||
name: step.name,
|
||||
params: step.params,
|
||||
condition: step.condition,
|
||||
}));
|
||||
setSteps(editorSteps);
|
||||
} else {
|
||||
setSteps([]);
|
||||
}
|
||||
})
|
||||
.catch(() => setSteps([]));
|
||||
} else {
|
||||
setName('');
|
||||
setDescription('');
|
||||
setSteps([]);
|
||||
}
|
||||
setError(null);
|
||||
}, [workflow]);
|
||||
}, [workflow, getWorkflowDetail]);
|
||||
|
||||
// Add new step
|
||||
const handleAddStep = useCallback(() => {
|
||||
|
||||
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,
|
||||
|
||||
@@ -47,6 +47,14 @@ export interface WorkflowStep {
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
steps: WorkflowStep[];
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowCreateOptions {
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -70,6 +78,7 @@ export interface ExtendedWorkflowRun extends WorkflowRun {
|
||||
|
||||
interface WorkflowClient {
|
||||
listWorkflows(): Promise<{ workflows: { id: string; name: string; steps: number; description?: string; createdAt?: string }[] } | null>;
|
||||
getWorkflow(id: string): Promise<WorkflowDetail | null>;
|
||||
createWorkflow(workflow: WorkflowCreateOptions): Promise<{ id: string; name: string } | null>;
|
||||
updateWorkflow(id: string, updates: UpdateWorkflowInput): Promise<{ id: string; name: string } | null>;
|
||||
deleteWorkflow(id: string): Promise<{ status: string }>;
|
||||
@@ -94,6 +103,7 @@ export interface WorkflowActionsSlice {
|
||||
setWorkflowStoreClient: (client: WorkflowClient) => void;
|
||||
loadWorkflows: () => Promise<void>;
|
||||
getWorkflow: (id: string) => Workflow | undefined;
|
||||
getWorkflowDetail: (id: string) => Promise<WorkflowDetail | undefined>;
|
||||
createWorkflow: (workflow: WorkflowCreateOptions) => Promise<Workflow | undefined>;
|
||||
updateWorkflow: (id: string, updates: UpdateWorkflowInput) => Promise<Workflow | undefined>;
|
||||
deleteWorkflow: (id: string) => Promise<void>;
|
||||
@@ -149,6 +159,24 @@ export const useWorkflowStore = create<WorkflowStateSlice & WorkflowActionsSlice
|
||||
return get().workflows.find(w => w.id === id);
|
||||
},
|
||||
|
||||
getWorkflowDetail: async (id: string) => {
|
||||
try {
|
||||
const result = await get().client.getWorkflow(id);
|
||||
if (!result) return undefined;
|
||||
return {
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
description: result.description,
|
||||
steps: Array.isArray(result.steps) ? result.steps : [],
|
||||
createdAt: result.createdAt,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load workflow details';
|
||||
set({ error: message });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
createWorkflow: async (workflow: WorkflowCreateOptions) => {
|
||||
set({ error: null });
|
||||
try {
|
||||
@@ -281,6 +309,14 @@ export const useWorkflowStore = create<WorkflowStateSlice & WorkflowActionsSlice
|
||||
*/
|
||||
function createWorkflowClientFromGateway(client: GatewayClient): WorkflowClient {
|
||||
return {
|
||||
getWorkflow: async (id: string) => {
|
||||
const result = await client.getWorkflow(id);
|
||||
if (!result) return null;
|
||||
return {
|
||||
...result,
|
||||
steps: result.steps as WorkflowStep[],
|
||||
};
|
||||
},
|
||||
listWorkflows: () => client.listWorkflows(),
|
||||
createWorkflow: (workflow) => client.createWorkflow(workflow),
|
||||
updateWorkflow: (id, updates) => client.updateWorkflow(id, updates),
|
||||
|
||||
Reference in New Issue
Block a user