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:
iven
2026-03-21 22:11:50 +08:00
parent 815c56326b
commit ce562e8bfc
36 changed files with 5241 additions and 201 deletions

View File

@@ -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">

View File

@@ -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(() => {

View 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);
}

View File

@@ -14,8 +14,6 @@
import { invoke } from '@tauri-apps/api/core';
import { isTauriRuntime } from './tauri-gateway';
import {
arrayToBase64,
base64ToArray,
deriveKey,
encrypt,
decrypt,

View File

@@ -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),