release(v0.2.0): streaming, MCP protocol, Browser Hand, security enhancements

## Major Features

### Streaming Response System
- Implement LlmDriver trait with `stream()` method returning async Stream
- Add SSE parsing for Anthropic and OpenAI API streaming
- Integrate Tauri event system for frontend streaming (`stream:chunk` events)
- Add StreamChunk types: Delta, ToolStart, ToolEnd, Complete, Error

### MCP Protocol Implementation
- Add MCP JSON-RPC 2.0 types (mcp_types.rs)
- Implement stdio-based MCP transport (mcp_transport.rs)
- Support tool discovery, execution, and resource operations

### Browser Hand Implementation
- Complete browser automation with Playwright-style actions
- Support Navigate, Click, Type, Scrape, Screenshot, Wait actions
- Add educational Hands: Whiteboard, Slideshow, Speech, Quiz

### Security Enhancements
- Implement command whitelist/blacklist for shell_exec tool
- Add SSRF protection with private IP blocking
- Create security.toml configuration file

## Test Improvements
- Fix test import paths (security-utils, setup)
- Fix vi.mock hoisting issues with vi.hoisted()
- Update test expectations for validateUrl and sanitizeFilename
- Add getUnsupportedLocalGatewayStatus mock

## Documentation Updates
- Update architecture documentation
- Improve configuration reference
- Add quick-start guide updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-24 03:24:24 +08:00
parent e49ba4460b
commit 3ff08faa56
78 changed files with 29575 additions and 1682 deletions

View File

@@ -0,0 +1,183 @@
/**
* Proposal Notifications Hook
*
* Periodically polls for pending identity change proposals and shows
* notifications when new proposals are available.
*
* Usage:
* ```tsx
* // In App.tsx or a top-level component
* useProposalNotifications();
* ```
*/
import { useEffect, useRef, useCallback } from 'react';
import { useChatStore } from '../store/chatStore';
import { intelligenceClient, type IdentityChangeProposal } from './intelligence-client';
// Configuration
const POLL_INTERVAL_MS = 60_000; // 1 minute
const NOTIFICATION_COOLDOWN_MS = 300_000; // 5 minutes - don't spam notifications
// Storage key for tracking notified proposals
const NOTIFIED_PROPOSALS_KEY = 'zclaw-notified-proposals';
/**
* Get set of already notified proposal IDs
*/
function getNotifiedProposals(): Set<string> {
try {
const stored = localStorage.getItem(NOTIFIED_PROPOSALS_KEY);
if (stored) {
return new Set(JSON.parse(stored) as string[]);
}
} catch {
// Ignore errors
}
return new Set();
}
/**
* Save notified proposal IDs
*/
function saveNotifiedProposals(ids: Set<string>): void {
try {
// Keep only last 100 IDs to prevent storage bloat
const arr = Array.from(ids).slice(-100);
localStorage.setItem(NOTIFIED_PROPOSALS_KEY, JSON.stringify(arr));
} catch {
// Ignore errors
}
}
/**
* Hook for showing proposal notifications
*
* This hook:
* 1. Polls for pending proposals every minute
* 2. Shows a toast notification when new proposals are found
* 3. Tracks which proposals have already been notified to avoid spam
*/
export function useProposalNotifications(): {
pendingCount: number;
refresh: () => Promise<void>;
} {
const { currentAgent } = useChatStore();
const agentId = currentAgent?.id;
const pendingCountRef = useRef(0);
const lastNotificationTimeRef = useRef(0);
const notifiedProposalsRef = useRef(getNotifiedProposals());
const isPollingRef = useRef(false);
const checkForNewProposals = useCallback(async () => {
if (!agentId || isPollingRef.current) return;
isPollingRef.current = true;
try {
const proposals = await intelligenceClient.identity.getPendingProposals(agentId);
pendingCountRef.current = proposals.length;
// Find proposals we haven't notified about
const newProposals = proposals.filter(
(p: IdentityChangeProposal) => !notifiedProposalsRef.current.has(p.id)
);
if (newProposals.length > 0) {
const now = Date.now();
// Check cooldown to avoid spam
if (now - lastNotificationTimeRef.current >= NOTIFICATION_COOLDOWN_MS) {
// Dispatch custom event for the app to handle
// This allows the app to show toast, play sound, etc.
const event = new CustomEvent('zclaw:proposal-available', {
detail: {
count: newProposals.length,
proposals: newProposals,
},
});
window.dispatchEvent(event);
lastNotificationTimeRef.current = now;
}
// Mark these proposals as notified
for (const p of newProposals) {
notifiedProposalsRef.current.add(p.id);
}
saveNotifiedProposals(notifiedProposalsRef.current);
}
} catch (err) {
console.warn('[ProposalNotifications] Failed to check proposals:', err);
} finally {
isPollingRef.current = false;
}
}, [agentId]);
// Set up polling
useEffect(() => {
if (!agentId) return;
// Initial check
checkForNewProposals();
// Set up interval
const intervalId = setInterval(checkForNewProposals, POLL_INTERVAL_MS);
return () => {
clearInterval(intervalId);
};
}, [agentId, checkForNewProposals]);
// Listen for visibility change to refresh when app becomes visible
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
checkForNewProposals();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [checkForNewProposals]);
return {
pendingCount: pendingCountRef.current,
refresh: checkForNewProposals,
};
}
/**
* Component that sets up proposal notification handling
*
* Place this near the root of the app to enable proposal notifications
*/
export function ProposalNotificationHandler(): null {
// This effect sets up the global event listener for proposal notifications
useEffect(() => {
const handleProposalAvailable = (event: Event) => {
const customEvent = event as CustomEvent<{ count: number }>;
const { count } = customEvent.detail;
// You can integrate with a toast system here
console.log(`[ProposalNotifications] ${count} new proposal(s) available`);
// If using the Toast system from Toast.tsx, you would do:
// toast(`${count} 个新的人格变更提案待审批`, 'info');
};
window.addEventListener('zclaw:proposal-available', handleProposalAvailable);
return () => {
window.removeEventListener('zclaw:proposal-available', handleProposalAvailable);
};
}, []);
return null;
}
export default useProposalNotifications;