fix(desktop): component cleanup + dead code removal + DeerFlow ai-elements
- ChatArea: DeerFlow ai-elements annotations for accessibility - Conversation: remove unused Context, simplify message rendering - Delete dead modules: audit-logger.ts, gateway-reconnect.ts - Replace console.log with structured logger across components - Add idb dependency for IndexedDB persistence - Fix kernel-skills type safety improvements
This commit is contained in:
@@ -1,173 +0,0 @@
|
||||
/**
|
||||
* audit-logger.ts - 前端审计日志记录工具
|
||||
*
|
||||
* 为 ZCLAW 前端操作提供统一的审计日志记录功能。
|
||||
* 记录关键操作(Hand 触发、Agent 创建等)到本地存储。
|
||||
*
|
||||
* @reserved This module is reserved for future audit logging integration.
|
||||
* It is not currently imported by any component. When audit logging is needed,
|
||||
* import { logAudit, logAuditSuccess, logAuditFailure } from this module.
|
||||
*/
|
||||
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const log = createLogger('AuditLogger');
|
||||
|
||||
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();
|
||||
}
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(6));
|
||||
const suffix = Array.from(bytes).map(b => b.toString(36).padStart(2, '0')).join('');
|
||||
return `audit_${Date.now()}_${suffix}`;
|
||||
}
|
||||
|
||||
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 (e) {
|
||||
log.debug('Failed to parse audit logs from localStorage', { error: e });
|
||||
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);
|
||||
|
||||
log.debug(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);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* gateway-reconnect.ts - Gateway Reconnect Methods
|
||||
*
|
||||
* Extracted from gateway-client.ts for modularity.
|
||||
* Installs reconnect methods onto GatewayClient.prototype via mixin pattern.
|
||||
*/
|
||||
|
||||
import type { GatewayClient } from './gateway-client';
|
||||
|
||||
// === Reconnect Constants ===
|
||||
|
||||
/** Maximum number of reconnect attempts before giving up */
|
||||
export const MAX_RECONNECT_ATTEMPTS = 10;
|
||||
|
||||
// === Mixin Installer ===
|
||||
|
||||
/**
|
||||
* Install reconnect methods onto GatewayClient.prototype.
|
||||
*
|
||||
* These methods access instance properties:
|
||||
* - this.reconnectAttempts: number
|
||||
* - this.reconnectInterval: number
|
||||
* - this.reconnectTimer: number | null
|
||||
* - this.log(level, message): void
|
||||
* - this.connect(): Promise<void>
|
||||
* - this.setState(state): void
|
||||
* - this.emitEvent(event, payload): void
|
||||
*/
|
||||
export function installReconnectMethods(ClientClass: { prototype: GatewayClient }): void {
|
||||
const proto = ClientClass.prototype as any;
|
||||
|
||||
/**
|
||||
* Schedule a reconnect attempt with exponential backoff.
|
||||
*/
|
||||
proto.scheduleReconnect = function (this: GatewayClient): void {
|
||||
const self = this as any;
|
||||
if (self.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
self.log('error', `Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Please reconnect manually.`);
|
||||
self.setState('disconnected');
|
||||
self.emitEvent('reconnect_failed', {
|
||||
attempts: self.reconnectAttempts,
|
||||
maxAttempts: MAX_RECONNECT_ATTEMPTS,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self.reconnectAttempts++;
|
||||
self.setState('reconnecting');
|
||||
const delay = Math.min(self.reconnectInterval * Math.pow(1.5, self.reconnectAttempts - 1), 30000);
|
||||
|
||||
self.log('info', `Scheduling reconnect attempt ${self.reconnectAttempts} in ${delay}ms`);
|
||||
|
||||
// Emit reconnecting event for UI
|
||||
self.emitEvent('reconnecting', {
|
||||
attempt: self.reconnectAttempts,
|
||||
delay,
|
||||
maxAttempts: MAX_RECONNECT_ATTEMPTS,
|
||||
});
|
||||
|
||||
self.reconnectTimer = window.setTimeout(async () => {
|
||||
try {
|
||||
await self.connect();
|
||||
} catch (e) {
|
||||
/* close handler will trigger another reconnect */
|
||||
self.log('warn', `Reconnect attempt ${self.reconnectAttempts} failed: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel a pending reconnect attempt.
|
||||
*/
|
||||
proto.cancelReconnect = function (this: GatewayClient): void {
|
||||
const self = this as any;
|
||||
if (self.reconnectTimer !== null) {
|
||||
clearTimeout(self.reconnectTimer);
|
||||
self.reconnectTimer = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { KernelClient } from './kernel-client';
|
||||
import { useConversationStore } from '../store/chat/conversationStore';
|
||||
|
||||
/** Skill shape returned by list/refresh/create/update operations. */
|
||||
type SkillItem = {
|
||||
@@ -107,12 +108,16 @@ export function installSkillMethods(ClientClass: { prototype: KernelClient }): v
|
||||
error?: string;
|
||||
durationMs?: number;
|
||||
}> {
|
||||
const convStore = useConversationStore.getState();
|
||||
const agent = convStore.currentAgent;
|
||||
const sessionKey = convStore.sessionKey;
|
||||
|
||||
return invoke('skill_execute', {
|
||||
id,
|
||||
context: {
|
||||
agentId: '',
|
||||
sessionId: '',
|
||||
workingDir: '',
|
||||
agentId: agent?.id || 'zclaw-main',
|
||||
sessionId: sessionKey || crypto.randomUUID(),
|
||||
workingDir: null,
|
||||
},
|
||||
input: input || {},
|
||||
});
|
||||
|
||||
@@ -98,6 +98,7 @@ import type {
|
||||
PromptCheckResult,
|
||||
PromptTemplateInfo,
|
||||
PromptVersionInfo,
|
||||
PaginatedResponse,
|
||||
AgentTemplateAvailable,
|
||||
AgentTemplateFull,
|
||||
} from './saas-types';
|
||||
|
||||
@@ -277,28 +277,6 @@ function clearLocalStorageBackup(key: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep synchronous versions for backward compatibility (deprecated)
|
||||
function writeLocalStorageBackup(key: string, value: string): void {
|
||||
try {
|
||||
if (value) {
|
||||
localStorage.setItem(key, value);
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug('writeLocalStorageBackup failed', { error: e });
|
||||
}
|
||||
}
|
||||
|
||||
function readLocalStorageBackup(key: string): string | null {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (e) {
|
||||
logger.debug('readLocalStorageBackup failed', { error: e });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// === Device Keys Secure Storage ===
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useChatStore } from '../store/chatStore';
|
||||
import { useConversationStore } from '../store/chat/conversationStore';
|
||||
import { intelligenceClient, type IdentityChangeProposal } from './intelligence-client';
|
||||
import { createLogger } from './logger';
|
||||
|
||||
@@ -65,7 +65,7 @@ export function useProposalNotifications(): {
|
||||
pendingCount: number;
|
||||
refresh: () => Promise<void>;
|
||||
} {
|
||||
const { currentAgent } = useChatStore();
|
||||
const currentAgent = useConversationStore((s) => s.currentAgent);
|
||||
const agentId = currentAgent?.id;
|
||||
|
||||
const pendingCountRef = useRef(0);
|
||||
|
||||
Reference in New Issue
Block a user