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

@@ -29,8 +29,7 @@ import {
Loader2
} from 'lucide-react';
import { useSecurityStore, AuditLogEntry } from '../store/securityStore';
import { getGatewayClient } from '../lib/gateway-client';
import { getClient } from '../store/connectionStore';
// === Types ===
@@ -514,7 +513,7 @@ export function AuditLogsPanel() {
const auditLogs = useSecurityStore((s) => s.auditLogs);
const loadAuditLogs = useSecurityStore((s) => s.loadAuditLogs);
const isLoading = useSecurityStore((s) => s.auditLogsLoading);
const client = getGatewayClient();
const client = getClient();
// State
const [limit, setLimit] = useState(50);

View File

@@ -9,8 +9,7 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Wifi, WifiOff, Loader2, RefreshCw, Heart, HeartPulse } from 'lucide-react';
import { useConnectionStore } from '../store/connectionStore';
import { getGatewayClient } from '../lib/gateway-client';
import { useConnectionStore, getClient } from '../store/connectionStore';
import {
createHealthCheckScheduler,
getHealthStatusLabel,
@@ -90,7 +89,7 @@ export function ConnectionStatus({
// Listen for reconnect events
useEffect(() => {
const client = getGatewayClient();
const client = getClient();
const unsubReconnecting = client.on('reconnecting', (info) => {
setReconnectInfo(info as ReconnectInfo);

View File

@@ -331,7 +331,8 @@ export function IdentityChangeProposalPanel() {
setSnapshots(agentSnapshots);
} catch (err) {
console.error('[IdentityChangeProposal] Failed to approve:', err);
setError('审批失败');
const message = err instanceof Error ? err.message : '审批失败,请重试';
setError(`审批失败: ${message}`);
} finally {
setProcessingId(null);
}
@@ -348,7 +349,8 @@ export function IdentityChangeProposalPanel() {
setProposals(pendingProposals);
} catch (err) {
console.error('[IdentityChangeProposal] Failed to reject:', err);
setError('拒绝失败');
const message = err instanceof Error ? err.message : '拒绝失败,请重试';
setError(`拒绝失败: ${message}`);
} finally {
setProcessingId(null);
}
@@ -365,7 +367,8 @@ export function IdentityChangeProposalPanel() {
setSnapshots(agentSnapshots);
} catch (err) {
console.error('[IdentityChangeProposal] Failed to restore:', err);
setError('恢复失败');
const message = err instanceof Error ? err.message : '恢复失败,请重试';
setError(`恢复失败: ${message}`);
} finally {
setProcessingId(null);
}

View File

@@ -116,6 +116,58 @@ const PRIORITY_CONFIG: Record<string, { color: string; bgColor: string }> = {
},
};
// === Field to File Mapping ===
/**
* Maps reflection field names to identity file types.
* This ensures correct routing of identity change proposals.
*/
function mapFieldToFile(field: string): 'soul' | 'instructions' {
// Direct matches
if (field === 'soul' || field === 'instructions') {
return field;
}
// Known soul fields (core personality traits)
const soulFields = [
'personality',
'traits',
'values',
'identity',
'character',
'essence',
'core_behavior',
];
// Known instructions fields (operational guidelines)
const instructionsFields = [
'guidelines',
'rules',
'behavior_rules',
'response_format',
'communication_guidelines',
'task_handling',
];
const lowerField = field.toLowerCase();
// Check explicit mappings
if (soulFields.some((f) => lowerField.includes(f))) {
return 'soul';
}
if (instructionsFields.some((f) => lowerField.includes(f))) {
return 'instructions';
}
// Fallback heuristics
if (lowerField.includes('soul') || lowerField.includes('personality') || lowerField.includes('trait')) {
return 'soul';
}
// Default to instructions for operational changes
return 'instructions';
}
// === Components ===
function SentimentBadge({ sentiment }: { sentiment: string }) {
@@ -419,6 +471,7 @@ export function ReflectionLog({
const [isReflecting, setIsReflecting] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [config, setConfig] = useState<ReflectionConfig>(() => loadConfig());
const [error, setError] = useState<string | null>(null);
// Persist config changes
useEffect(() => {
@@ -446,8 +499,24 @@ export function ReflectionLog({
const handleReflect = useCallback(async () => {
setIsReflecting(true);
setError(null);
try {
const result = await intelligenceClient.reflection.reflect(agentId, []);
// Fetch recent memories for analysis
const memories = await intelligenceClient.memory.search({
agentId,
limit: 50, // Get enough memories for pattern analysis
});
// Convert to analysis format
const memoriesForAnalysis = memories.map((m) => ({
memory_type: m.type,
content: m.content,
importance: m.importance,
access_count: m.accessCount,
tags: m.tags,
}));
const result = await intelligenceClient.reflection.reflect(agentId, memoriesForAnalysis);
setHistory((prev) => [result, ...prev]);
// Convert reflection identity_proposals to actual identity proposals
@@ -455,13 +524,8 @@ export function ReflectionLog({
if (result.identity_proposals && result.identity_proposals.length > 0) {
for (const proposal of result.identity_proposals) {
try {
// Determine which file to modify based on the field
const file: 'soul' | 'instructions' =
proposal.field === 'soul' || proposal.field === 'instructions'
? (proposal.field as 'soul' | 'instructions')
: proposal.field.toLowerCase().includes('soul')
? 'soul'
: 'instructions';
// Map field to file type with explicit mapping rules
const file = mapFieldToFile(proposal.field);
// Persist the proposal to the identity system
await intelligenceClient.identity.proposeChange(
@@ -479,8 +543,10 @@ export function ReflectionLog({
const proposals = await intelligenceClient.identity.getPendingProposals(agentId);
setPendingProposals(proposals);
}
} catch (error) {
console.error('[ReflectionLog] Reflection failed:', error);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error('[ReflectionLog] Reflection failed:', err);
setError(`反思失败: ${errorMessage}`);
} finally {
setIsReflecting(false);
}
@@ -559,6 +625,31 @@ export function ReflectionLog({
</span>
</div>
{/* Error Banner */}
<AnimatePresence>
{error && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<div className="flex items-center justify-between gap-2 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800">
<div className="flex items-center gap-2 text-red-700 dark:text-red-300 text-sm">
<AlertTriangle className="w-4 h-4" />
<span>{error}</span>
</div>
<button
onClick={() => setError(null)}
className="p-1 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200"
>
<X className="w-4 h-4" />
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Config Panel */}
<AnimatePresence>
{showConfig && (