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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user