feat(intelligence): complete migration to Rust backend
- Unify all intelligence modules to use intelligenceClient - Delete legacy TS implementations (agent-memory, reflection-engine, heartbeat-engine, context-compactor, agent-identity, memory-index) - Update all consumers to use snake_case backend types - Remove deprecated llm-integration.test.ts This eliminates code duplication between frontend and backend, resolves localStorage limitations, and enables persistent intelligence features. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useHandStore } from '../../store/handStore';
|
||||
import { useWorkflowStore, type Workflow } from '../../store/workflowStore';
|
||||
import { useWorkflowStore } from '../../store/workflowStore';
|
||||
import {
|
||||
type AutomationItem,
|
||||
type CategoryType,
|
||||
@@ -54,7 +54,9 @@ export function AutomationPanel({
|
||||
// Store state - use domain stores
|
||||
const hands = useHandStore((s) => s.hands);
|
||||
const workflows = useWorkflowStore((s) => s.workflows);
|
||||
const isLoading = useHandStore((s) => s.isLoading) || useWorkflowStore((s) => s.isLoading);
|
||||
const handLoading = useHandStore((s) => s.isLoading);
|
||||
const workflowLoading = useWorkflowStore((s) => s.isLoading);
|
||||
const isLoading = handLoading || workflowLoading;
|
||||
const loadHands = useHandStore((s) => s.loadHands);
|
||||
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
|
||||
const triggerHand = useHandStore((s) => s.triggerHand);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
X,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
Bug,
|
||||
WifiOff,
|
||||
ShieldAlert,
|
||||
@@ -44,14 +43,14 @@ interface ErrorNotificationProps {
|
||||
|
||||
const categoryIcons: Record<ErrorCategory, typeof AlertCircle> = {
|
||||
network: WifiOff,
|
||||
authentication: ShieldAlert,
|
||||
authorization: ShieldAlert,
|
||||
auth: ShieldAlert,
|
||||
permission: ShieldAlert,
|
||||
validation: AlertTriangle,
|
||||
configuration: AlertTriangle,
|
||||
internal: Bug,
|
||||
external: AlertCircle,
|
||||
config: AlertTriangle,
|
||||
server: Bug,
|
||||
client: AlertCircle,
|
||||
timeout: Clock,
|
||||
unknown: AlertCircle,
|
||||
system: Bug,
|
||||
};
|
||||
|
||||
const severityColors: Record<ErrorSeverity, {
|
||||
|
||||
@@ -26,11 +26,23 @@ import {
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
HeartbeatEngine,
|
||||
DEFAULT_HEARTBEAT_CONFIG,
|
||||
intelligenceClient,
|
||||
type HeartbeatConfig as HeartbeatConfigType,
|
||||
type HeartbeatResult,
|
||||
} from '../lib/heartbeat-engine';
|
||||
type HeartbeatAlert,
|
||||
} from '../lib/intelligence-client';
|
||||
|
||||
// === Default Config ===
|
||||
|
||||
const DEFAULT_HEARTBEAT_CONFIG: HeartbeatConfigType = {
|
||||
enabled: false,
|
||||
interval_minutes: 30,
|
||||
quiet_hours_start: null,
|
||||
quiet_hours_end: null,
|
||||
notify_channel: 'ui',
|
||||
proactivity_level: 'standard',
|
||||
max_alerts_per_tick: 5,
|
||||
};
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -309,8 +321,8 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon
|
||||
const handleTestHeartbeat = useCallback(async () => {
|
||||
setIsTesting(true);
|
||||
try {
|
||||
const engine = new HeartbeatEngine('zclaw-main', config);
|
||||
const result = await engine.tick();
|
||||
await intelligenceClient.heartbeat.init('zclaw-main', config);
|
||||
const result = await intelligenceClient.heartbeat.tick('zclaw-main');
|
||||
setLastResult(result);
|
||||
} catch (error) {
|
||||
console.error('[HeartbeatConfig] Test failed:', error);
|
||||
@@ -408,12 +420,12 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon
|
||||
min="5"
|
||||
max="120"
|
||||
step="5"
|
||||
value={config.intervalMinutes}
|
||||
onChange={(e) => updateConfig({ intervalMinutes: parseInt(e.target.value) })}
|
||||
value={config.interval_minutes}
|
||||
onChange={(e) => updateConfig({ interval_minutes: parseInt(e.target.value) })}
|
||||
className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-pink-500"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 w-16 text-right">
|
||||
{config.intervalMinutes} 分钟
|
||||
{config.interval_minutes} 分钟
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -428,8 +440,8 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon
|
||||
</div>
|
||||
<div className="pl-6">
|
||||
<ProactivityLevelSelector
|
||||
value={config.proactivityLevel}
|
||||
onChange={(level) => updateConfig({ proactivityLevel: level })}
|
||||
value={config.proactivity_level}
|
||||
onChange={(level) => updateConfig({ proactivity_level: level })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -437,15 +449,15 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon
|
||||
{/* Quiet Hours */}
|
||||
<div className="space-y-2">
|
||||
<QuietHoursConfig
|
||||
start={config.quietHoursStart}
|
||||
end={config.quietHoursEnd}
|
||||
enabled={!!config.quietHoursStart}
|
||||
onStartChange={(time) => updateConfig({ quietHoursStart: time })}
|
||||
onEndChange={(time) => updateConfig({ quietHoursEnd: time })}
|
||||
start={config.quiet_hours_start ?? undefined}
|
||||
end={config.quiet_hours_end ?? undefined}
|
||||
enabled={!!config.quiet_hours_start}
|
||||
onStartChange={(time) => updateConfig({ quiet_hours_start: time })}
|
||||
onEndChange={(time) => updateConfig({ quiet_hours_end: time })}
|
||||
onToggle={(enabled) =>
|
||||
updateConfig({
|
||||
quietHoursStart: enabled ? '22:00' : undefined,
|
||||
quietHoursEnd: enabled ? '08:00' : undefined,
|
||||
quiet_hours_start: enabled ? '22:00' : null,
|
||||
quiet_hours_end: enabled ? '08:00' : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -484,12 +496,12 @@ export function HeartbeatConfig({ className = '', onConfigChange }: HeartbeatCon
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
检查了 {lastResult.checkedItems} 项
|
||||
检查了 {lastResult.checked_items} 项
|
||||
{lastResult.alerts.length > 0 && ` · ${lastResult.alerts.length} 个提醒`}
|
||||
</div>
|
||||
{lastResult.alerts.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{lastResult.alerts.map((alert, i) => (
|
||||
{lastResult.alerts.map((alert: HeartbeatAlert, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`text-xs p-2 rounded ${
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
import { cardHover, defaultTransition } from '../lib/animations';
|
||||
import { Button, Badge, EmptyState } from './ui';
|
||||
import {
|
||||
getMemoryManager,
|
||||
intelligenceClient,
|
||||
type MemoryEntry,
|
||||
type MemoryType,
|
||||
type MemoryStats,
|
||||
} from '../lib/agent-memory';
|
||||
} from '../lib/intelligence-client';
|
||||
import { useChatStore } from '../store/chatStore';
|
||||
|
||||
const TYPE_LABELS: Record<MemoryType, { label: string; emoji: string; color: string }> = {
|
||||
@@ -34,22 +34,26 @@ export function MemoryPanel() {
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const loadMemories = useCallback(async () => {
|
||||
const mgr = getMemoryManager();
|
||||
const typeFilter = filterType !== 'all' ? { type: filterType as MemoryType } : {};
|
||||
|
||||
if (searchQuery.trim()) {
|
||||
const results = await mgr.search(searchQuery, {
|
||||
const results = await intelligenceClient.memory.search({
|
||||
agentId,
|
||||
query: searchQuery,
|
||||
limit: 50,
|
||||
...typeFilter,
|
||||
});
|
||||
setMemories(results);
|
||||
} else {
|
||||
const all = await mgr.getAll(agentId, { ...typeFilter, limit: 50 });
|
||||
setMemories(all);
|
||||
const results = await intelligenceClient.memory.search({
|
||||
agentId,
|
||||
limit: 50,
|
||||
...typeFilter,
|
||||
});
|
||||
setMemories(results);
|
||||
}
|
||||
|
||||
const s = await mgr.stats(agentId);
|
||||
const s = await intelligenceClient.memory.stats();
|
||||
setStats(s);
|
||||
}, [agentId, searchQuery, filterType]);
|
||||
|
||||
@@ -58,15 +62,22 @@ export function MemoryPanel() {
|
||||
}, [loadMemories]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await getMemoryManager().forget(id);
|
||||
await intelligenceClient.memory.delete(id);
|
||||
loadMemories();
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const md = await getMemoryManager().exportToMarkdown(agentId);
|
||||
const blob = new Blob([md], { type: 'text/markdown' });
|
||||
const memories = await intelligenceClient.memory.export();
|
||||
const filtered = memories.filter(m => m.agentId === agentId);
|
||||
const md = filtered.map(m =>
|
||||
`## [${m.type}] ${m.content}\n` +
|
||||
`- 重要度: ${m.importance}\n` +
|
||||
`- 标签: ${m.tags.join(', ') || '无'}\n` +
|
||||
`- 创建时间: ${m.createdAt}\n`
|
||||
).join('\n---\n\n');
|
||||
const blob = new Blob([md || '# 无记忆数据'], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -79,12 +90,20 @@ export function MemoryPanel() {
|
||||
};
|
||||
|
||||
const handlePrune = async () => {
|
||||
const pruned = await getMemoryManager().prune({
|
||||
// Find old, low-importance memories and delete them
|
||||
const memories = await intelligenceClient.memory.search({
|
||||
agentId,
|
||||
maxAgeDays: 30,
|
||||
minImportance: 3,
|
||||
minImportance: 0,
|
||||
limit: 1000,
|
||||
});
|
||||
if (pruned > 0) {
|
||||
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||
const toDelete = memories.filter(m =>
|
||||
new Date(m.createdAt).getTime() < thirtyDaysAgo && m.importance < 3
|
||||
);
|
||||
for (const m of toDelete) {
|
||||
await intelligenceClient.memory.delete(m.id);
|
||||
}
|
||||
if (toDelete.length > 0) {
|
||||
loadMemories();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,14 +29,13 @@ import {
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
ReflectionEngine,
|
||||
intelligenceClient,
|
||||
type ReflectionResult,
|
||||
type IdentityChangeProposal,
|
||||
type ReflectionConfig,
|
||||
type PatternObservation,
|
||||
type ImprovementSuggestion,
|
||||
type ReflectionConfig,
|
||||
DEFAULT_REFLECTION_CONFIG,
|
||||
} from '../lib/reflection-engine';
|
||||
import { getAgentIdentityManager, type IdentityChangeProposal } from '../lib/agent-identity';
|
||||
} from '../lib/intelligence-client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -231,8 +230,8 @@ function ProposalCard({
|
||||
当前内容
|
||||
</h5>
|
||||
<pre className="text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 p-2 rounded overflow-x-auto whitespace-pre-wrap">
|
||||
{proposal.currentContent.slice(0, 500)}
|
||||
{proposal.currentContent.length > 500 && '...'}
|
||||
{proposal.current_content.slice(0, 500)}
|
||||
{proposal.current_content.length > 500 && '...'}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
@@ -240,8 +239,8 @@ function ProposalCard({
|
||||
建议内容
|
||||
</h5>
|
||||
<pre className="text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 p-2 rounded overflow-x-auto whitespace-pre-wrap">
|
||||
{proposal.suggestedContent.slice(0, 500)}
|
||||
{proposal.suggestedContent.length > 500 && '...'}
|
||||
{proposal.suggested_content.slice(0, 500)}
|
||||
{proposal.suggested_content.length > 500 && '...'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,9 +308,9 @@ function ReflectionEntry({
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{result.improvements.length} 建议
|
||||
</span>
|
||||
{result.identityProposals.length > 0 && (
|
||||
{result.identity_proposals.length > 0 && (
|
||||
<span className="text-yellow-600 dark:text-yellow-400">
|
||||
{result.identityProposals.length} 变更
|
||||
{result.identity_proposals.length} 变更
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -362,8 +361,8 @@ function ReflectionEntry({
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<span>新增记忆: {result.newMemories}</span>
|
||||
<span>身份变更提议: {result.identityProposals.length}</span>
|
||||
<span>新增记忆: {result.new_memories}</span>
|
||||
<span>身份变更提议: {result.identity_proposals.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -381,56 +380,63 @@ export function ReflectionLog({
|
||||
onProposalApprove,
|
||||
onProposalReject,
|
||||
}: ReflectionLogProps) {
|
||||
const [engine] = useState(() => new ReflectionEngine());
|
||||
const [history, setHistory] = useState<ReflectionResult[]>([]);
|
||||
const [pendingProposals, setPendingProposals] = useState<IdentityChangeProposal[]>([]);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [isReflecting, setIsReflecting] = useState(false);
|
||||
const [config, setConfig] = useState<ReflectionConfig>(DEFAULT_REFLECTION_CONFIG);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [config, setConfig] = useState<ReflectionConfig>({
|
||||
trigger_after_conversations: 5,
|
||||
allow_soul_modification: true,
|
||||
require_approval: true,
|
||||
});
|
||||
|
||||
// Load history and pending proposals
|
||||
useEffect(() => {
|
||||
const loadedHistory = engine.getHistory();
|
||||
setHistory([...loadedHistory].reverse()); // Most recent first
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const loadedHistory = await intelligenceClient.reflection.getHistory();
|
||||
setHistory([...loadedHistory].reverse()); // Most recent first
|
||||
|
||||
const identityManager = getAgentIdentityManager();
|
||||
const proposals = identityManager.getPendingProposals(agentId);
|
||||
setPendingProposals(proposals);
|
||||
}, [engine, agentId]);
|
||||
const proposals = await intelligenceClient.identity.getPendingProposals(agentId);
|
||||
setPendingProposals(proposals);
|
||||
} catch (error) {
|
||||
console.error('[ReflectionLog] Failed to load data:', error);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, [agentId]);
|
||||
|
||||
const handleReflect = useCallback(async () => {
|
||||
setIsReflecting(true);
|
||||
try {
|
||||
const result = await engine.reflect(agentId);
|
||||
const result = await intelligenceClient.reflection.reflect(agentId, []);
|
||||
setHistory((prev) => [result, ...prev]);
|
||||
|
||||
// Update pending proposals
|
||||
if (result.identityProposals.length > 0) {
|
||||
setPendingProposals((prev) => [...prev, ...result.identityProposals]);
|
||||
if (result.identity_proposals.length > 0) {
|
||||
setPendingProposals((prev) => [...prev, ...result.identity_proposals]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ReflectionLog] Reflection failed:', error);
|
||||
} finally {
|
||||
setIsReflecting(false);
|
||||
}
|
||||
}, [engine, agentId]);
|
||||
}, [agentId]);
|
||||
|
||||
const handleApproveProposal = useCallback(
|
||||
(proposal: IdentityChangeProposal) => {
|
||||
const identityManager = getAgentIdentityManager();
|
||||
identityManager.approveProposal(proposal.id);
|
||||
setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id));
|
||||
async (proposal: IdentityChangeProposal) => {
|
||||
await intelligenceClient.identity.approveProposal(proposal.id);
|
||||
setPendingProposals((prev: IdentityChangeProposal[]) => prev.filter((p: IdentityChangeProposal) => p.id !== proposal.id));
|
||||
onProposalApprove?.(proposal);
|
||||
},
|
||||
[onProposalApprove]
|
||||
);
|
||||
|
||||
const handleRejectProposal = useCallback(
|
||||
(proposal: IdentityChangeProposal) => {
|
||||
const identityManager = getAgentIdentityManager();
|
||||
identityManager.rejectProposal(proposal.id);
|
||||
setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id));
|
||||
async (proposal: IdentityChangeProposal) => {
|
||||
await intelligenceClient.identity.rejectProposal(proposal.id);
|
||||
setPendingProposals((prev: IdentityChangeProposal[]) => prev.filter((p: IdentityChangeProposal) => p.id !== proposal.id));
|
||||
onProposalReject?.(proposal);
|
||||
},
|
||||
[onProposalReject]
|
||||
@@ -438,9 +444,9 @@ export function ReflectionLog({
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const totalReflections = history.length;
|
||||
const totalPatterns = history.reduce((sum, r) => sum + r.patterns.length, 0);
|
||||
const totalImprovements = history.reduce((sum, r) => sum + r.improvements.length, 0);
|
||||
const totalIdentityChanges = history.reduce((sum, r) => sum + r.identityProposals.length, 0);
|
||||
const totalPatterns = history.reduce((sum: number, r: ReflectionResult) => sum + r.patterns.length, 0);
|
||||
const totalImprovements = history.reduce((sum: number, r: ReflectionResult) => sum + r.improvements.length, 0);
|
||||
const totalIdentityChanges = history.reduce((sum: number, r: ReflectionResult) => sum + r.identity_proposals.length, 0);
|
||||
return { totalReflections, totalPatterns, totalImprovements, totalIdentityChanges };
|
||||
}, [history]);
|
||||
|
||||
@@ -507,9 +513,9 @@ export function ReflectionLog({
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
value={config.triggerAfterConversations}
|
||||
value={config.trigger_after_conversations || 5}
|
||||
onChange={(e) =>
|
||||
setConfig((prev) => ({ ...prev, triggerAfterConversations: parseInt(e.target.value) || 5 }))
|
||||
setConfig((prev) => ({ ...prev, trigger_after_conversations: parseInt(e.target.value) || 5 }))
|
||||
}
|
||||
className="w-16 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
@@ -517,13 +523,13 @@ export function ReflectionLog({
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">允许修改 SOUL.md</span>
|
||||
<button
|
||||
onClick={() => setConfig((prev) => ({ ...prev, allowSoulModification: !prev.allowSoulModification }))}
|
||||
onClick={() => setConfig((prev) => ({ ...prev, allow_soul_modification: !prev.allow_soul_modification }))}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${
|
||||
config.allowSoulModification ? 'bg-purple-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
config.allow_soul_modification ? 'bg-purple-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: config.allowSoulModification ? 18 : 0 }}
|
||||
animate={{ x: config.allow_soul_modification ? 18 : 0 }}
|
||||
className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow"
|
||||
/>
|
||||
</button>
|
||||
@@ -531,13 +537,13 @@ export function ReflectionLog({
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">变更需审批</span>
|
||||
<button
|
||||
onClick={() => setConfig((prev) => ({ ...prev, requireApproval: !prev.requireApproval }))}
|
||||
onClick={() => setConfig((prev) => ({ ...prev, require_approval: !prev.require_approval }))}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${
|
||||
config.requireApproval ? 'bg-purple-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
config.require_approval ? 'bg-purple-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: config.requireApproval ? 18 : 0 }}
|
||||
animate={{ x: config.require_approval ? 18 : 0 }}
|
||||
className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Search,
|
||||
Package,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
ChevronRight,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { useConfigStore, type SkillInfo } from '../store/configStore';
|
||||
import { useConfigStore } from '../store/configStore';
|
||||
import {
|
||||
adaptSkillsCatalog,
|
||||
type SkillDisplay,
|
||||
|
||||
Reference in New Issue
Block a user