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:
183
desktop/src/lib/useProposalNotifications.ts
Normal file
183
desktop/src/lib/useProposalNotifications.ts
Normal 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;
|
||||
Reference in New Issue
Block a user