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:
iven
2026-03-21 15:17:39 +08:00
parent 17fb1e69aa
commit f3ec3c8d4c
24 changed files with 1172 additions and 3095 deletions

View File

@@ -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);

View File

@@ -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, {

View File

@@ -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 ${

View File

@@ -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();
}
};

View File

@@ -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>

View File

@@ -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,