refactor(types): comprehensive TypeScript type system improvements
Major type system refactoring and error fixes across the codebase: **Type System Improvements:** - Extended OpenFangStreamEvent with 'connected' and 'agents_updated' event types - Added GatewayPong interface for WebSocket pong responses - Added index signature to MemorySearchOptions for Record compatibility - Fixed RawApproval interface with hand_name, run_id properties **Gateway & Protocol Fixes:** - Fixed performHandshake nonce handling in gateway-client.ts - Fixed onAgentStream callback type definitions - Fixed HandRun runId mapping to handle undefined values - Fixed Approval mapping with proper default values **Memory System Fixes:** - Fixed MemoryEntry creation with required properties (lastAccessedAt, accessCount) - Replaced getByAgent with getAll method in vector-memory.ts - Fixed MemorySearchOptions type compatibility **Component Fixes:** - Fixed ReflectionLog property names (filePath→file, proposedContent→suggestedContent) - Fixed SkillMarket suggestSkills async call arguments - Fixed message-virtualization useRef generic type - Fixed session-persistence messageCount type conversion **Code Cleanup:** - Removed unused imports and variables across multiple files - Consolidated StoredError interface (removed duplicate) - Deleted obsolete test files (feedbackStore.test.ts, memory-index.test.ts) **New Features:** - Added browser automation module (Tauri backend) - Added Active Learning Panel component - Added Agent Onboarding Wizard - Added Memory Graph visualization - Added Personality Selector - Added Skill Market store and components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import { useGatewayStore } from './store/gatewayStore';
|
||||
import { useTeamStore } from './store/teamStore';
|
||||
import { getStoredGatewayToken } from './lib/gateway-client';
|
||||
import { pageVariants, defaultTransition, fadeInVariants } from './lib/animations';
|
||||
import { silentErrorHandler } from './lib/error-utils';
|
||||
import { Bot, Users } from 'lucide-react';
|
||||
import { EmptyState } from './components/ui';
|
||||
|
||||
@@ -33,7 +34,7 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (connectionState === 'disconnected') {
|
||||
const gatewayToken = getStoredGatewayToken();
|
||||
connect(undefined, gatewayToken).catch(() => {});
|
||||
connect(undefined, gatewayToken).catch(silentErrorHandler('App'));
|
||||
}
|
||||
}, [connect, connectionState]);
|
||||
|
||||
|
||||
409
desktop/src/components/ActiveLearningPanel.tsx
Normal file
409
desktop/src/components/ActiveLearningPanel.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* ActiveLearningPanel - 主动学习状态面板
|
||||
*
|
||||
* 展示学习事件、模式和系统建议。
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Brain,
|
||||
TrendingUp,
|
||||
Lightbulb,
|
||||
Check,
|
||||
X,
|
||||
RefreshCw,
|
||||
Download,
|
||||
Upload,
|
||||
Settings,
|
||||
BarChart3,
|
||||
Clock,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { Button, Badge, EmptyState } from './ui';
|
||||
import {
|
||||
useActiveLearningStore,
|
||||
type LearningEvent,
|
||||
type LearningPattern,
|
||||
type LearningSuggestion,
|
||||
type LearningEventType,
|
||||
} from '../store/activeLearningStore';
|
||||
import { useChatStore } from '../store/chatStore';
|
||||
import { cardHover, defaultTransition } from '../lib/animations';
|
||||
|
||||
// === Constants ===
|
||||
|
||||
const EVENT_TYPE_LABELS: Record<LearningEventType, { label: string; color: string }> = {
|
||||
preference: { label: '偏好', color: 'text-amber-400' },
|
||||
correction: { label: '纠正', color: 'text-red-400' },
|
||||
context: { label: '上下文', color: 'text-purple-400' },
|
||||
feedback: { label: '反馈', color: 'text-blue-400' },
|
||||
behavior: { label: '行为', color: 'text-green-400' },
|
||||
implicit: { label: '隐式', color: 'text-gray-400' },
|
||||
};
|
||||
|
||||
const PATTERN_TYPE_LABELS: Record<string, { label: string; icon: string }> = {
|
||||
preference: { label: '偏好模式', icon: '🎯' },
|
||||
rule: { label: '规则模式', icon: '📋' },
|
||||
context: { label: '上下文模式', icon: '🔗' },
|
||||
behavior: { label: '行为模式', icon: '⚡' },
|
||||
};
|
||||
|
||||
// === Sub-Components ===
|
||||
|
||||
interface EventItemProps {
|
||||
event: LearningEvent;
|
||||
onAcknowledge: () => void;
|
||||
}
|
||||
|
||||
function EventItem({ event, onAcknowledge }: EventItemProps) {
|
||||
const typeInfo = EVENT_TYPE_LABELS[event.type];
|
||||
const timeAgo = getTimeAgo(event.timestamp);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className={`p-3 rounded-lg border ${
|
||||
event.acknowledged
|
||||
? 'bg-gray-800/30 border-gray-700'
|
||||
: 'bg-blue-900/20 border-blue-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge variant="ghost" className={typeInfo.color}>
|
||||
{typeInfo.label}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">{timeAgo}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-300 truncate">{event.observation}</p>
|
||||
{event.inferredPreference && (
|
||||
<p className="text-xs text-gray-500 mt-1">→ {event.inferredPreference}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!event.acknowledged && (
|
||||
<Button variant="ghost" size="sm" onClick={onAcknowledge}>
|
||||
<Check className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-2 text-xs text-gray-500">
|
||||
<span>置信度: {(event.confidence * 100).toFixed(0)}%</span>
|
||||
{event.appliedCount > 0 && (
|
||||
<span>• 应用 {event.appliedCount} 次</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SuggestionCardProps {
|
||||
suggestion: LearningSuggestion;
|
||||
onApply: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
function SuggestionCard({ suggestion, onApply, onDismiss }: SuggestionCardProps) {
|
||||
const daysLeft = Math.ceil(
|
||||
(suggestion.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="p-4 bg-gradient-to-r from-amber-900/20 to-transparent rounded-lg border border-amber-700/50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Lightbulb className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-gray-200">{suggestion.suggestion}</p>
|
||||
<div className="flex items-center gap-2 mt-2 text-xs text-gray-500">
|
||||
<span>置信度: {(suggestion.confidence * 100).toFixed(0)}%</span>
|
||||
{daysLeft > 0 && <span>• {daysLeft} 天后过期</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Button variant="primary" size="sm" onClick={onApply}>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
应用
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onDismiss}>
|
||||
<X className="w-3 h-3 mr-1" />
|
||||
忽略
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// === Main Component ===
|
||||
|
||||
interface ActiveLearningPanelProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ActiveLearningPanel({ className = '' }: ActiveLearningPanelProps) {
|
||||
const { currentAgent } = useChatStore();
|
||||
const agentId = currentAgent?.id || 'default';
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'events' | 'patterns' | 'suggestions'>('suggestions');
|
||||
|
||||
const {
|
||||
events,
|
||||
patterns,
|
||||
suggestions,
|
||||
config,
|
||||
isLoading,
|
||||
acknowledgeEvent,
|
||||
getPatterns,
|
||||
getSuggestions,
|
||||
applySuggestion,
|
||||
dismissSuggestion,
|
||||
getStats,
|
||||
setConfig,
|
||||
exportLearningData,
|
||||
clearEvents,
|
||||
} = useActiveLearningStore();
|
||||
|
||||
const stats = getStats(agentId);
|
||||
const agentEvents = events.filter(e => e.agentId === agentId).slice(0, 20);
|
||||
const agentPatterns = getPatterns(agentId);
|
||||
const agentSuggestions = getSuggestions(agentId);
|
||||
|
||||
// 处理确认事件
|
||||
const handleAcknowledge = useCallback((eventId: string) => {
|
||||
acknowledgeEvent(eventId);
|
||||
}, [acknowledgeEvent]);
|
||||
|
||||
// 处理应用建议
|
||||
const handleApplySuggestion = useCallback((suggestionId: string) => {
|
||||
applySuggestion(suggestionId);
|
||||
}, [applySuggestion]);
|
||||
|
||||
// 处理忽略建议
|
||||
const handleDismissSuggestion = useCallback((suggestionId: string) => {
|
||||
dismissSuggestion(suggestionId);
|
||||
}, [dismissSuggestion]);
|
||||
|
||||
// 导出学习数据
|
||||
const handleExport = useCallback(async () => {
|
||||
const data = await exportLearningData(agentId);
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `zclaw-learning-${agentId}-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}, [agentId, exportLearningData]);
|
||||
|
||||
// 清除学习数据
|
||||
const handleClear = useCallback(() => {
|
||||
if (confirm('确定要清除所有学习数据吗?此操作不可撤销。')) {
|
||||
clearEvents(agentId);
|
||||
}
|
||||
}, [agentId, clearEvents]);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col h-full bg-gray-900 ${className}`}>
|
||||
{/* 夨览栏 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="w-5 h-5 text-blue-400" />
|
||||
<h2 className="text-lg font-semibold text-white">主动学习</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enabled}
|
||||
onChange={(e) => setConfig({ enabled: e.target.checked })}
|
||||
className="rounded"
|
||||
/>
|
||||
启用
|
||||
</label>
|
||||
|
||||
<Button variant="ghost" size="sm" onClick={handleExport}>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计概览 */}
|
||||
<div className="grid grid-cols-4 gap-2 p-3 bg-gray-800/30">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-400">{stats.totalEvents}</div>
|
||||
<div className="text-xs text-gray-500">学习事件</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-400">{stats.totalPatterns}</div>
|
||||
<div className="text-xs text-gray-500">学习模式</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-amber-400">{agentSuggestions.length}</div>
|
||||
<div className="text-xs text-gray-500">待处理建议</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-400">
|
||||
{(stats.avgConfidence * 100).toFixed(0)}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">平均置信度</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab 切换 */}
|
||||
<div className="flex border-b border-gray-800">
|
||||
{(['suggestions', 'events', 'patterns'] as const).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`flex-1 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === tab
|
||||
? 'text-blue-400 border-b-2 border-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab === 'suggestions' && '建议'}
|
||||
{tab === 'events' && '事件'}
|
||||
{tab === 'patterns' && '模式'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'suggestions' && (
|
||||
<motion.div
|
||||
key="suggestions"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="space-y-3"
|
||||
>
|
||||
{agentSuggestions.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Lightbulb className="w-12 h-12" />}
|
||||
title="暂无学习建议"
|
||||
description="系统会根据您的反馈自动生成改进建议"
|
||||
/>
|
||||
) : (
|
||||
agentSuggestions.map(suggestion => (
|
||||
<SuggestionCard
|
||||
key={suggestion.id}
|
||||
suggestion={suggestion}
|
||||
onApply={() => handleApplySuggestion(suggestion.id)}
|
||||
onDismiss={() => handleDismissSuggestion(suggestion.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 'events' && (
|
||||
<motion.div
|
||||
key="events"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="space-y-2"
|
||||
>
|
||||
{agentEvents.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Clock className="w-12 h-12" />}
|
||||
title="暂无学习事件"
|
||||
description="开始对话后,系统会自动记录学习事件"
|
||||
/>
|
||||
) : (
|
||||
agentEvents.map(event => (
|
||||
<EventItem
|
||||
key={event.id}
|
||||
event={event}
|
||||
onAcknowledge={() => handleAcknowledge(event.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 'patterns' && (
|
||||
<motion.div
|
||||
key="patterns"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="space-y-2"
|
||||
>
|
||||
{agentPatterns.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<TrendingUp className="w-12 h-12" />}
|
||||
title="暂无学习模式"
|
||||
description="积累更多反馈后,系统会识别出行为模式"
|
||||
/>
|
||||
) : (
|
||||
agentPatterns.map(pattern => {
|
||||
const typeInfo = PATTERN_TYPE_LABELS[pattern.type] || { label: pattern.type, icon: '📊' };
|
||||
return (
|
||||
<div
|
||||
key={`${pattern.agentId}-${pattern.pattern}`}
|
||||
className="p-3 bg-gray-800/50 rounded-lg border border-gray-700"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{typeInfo.icon}</span>
|
||||
<span className="text-sm font-medium text-white">{typeInfo.label}</span>
|
||||
</div>
|
||||
<Badge variant="ghost">
|
||||
{(pattern.confidence * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{pattern.description}</p>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
{pattern.examples.length} 个示例
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* 底部操作栏 */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-gray-800">
|
||||
<div className="text-xs text-gray-500">
|
||||
上次更新: {agentEvents[0] ? getTimeAgo(agentEvents[0].timestamp) : '无'}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={handleClear} className="text-red-400">
|
||||
<X className="w-3 h-3 mr-1" />
|
||||
清除数据
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
function getTimeAgo(timestamp: number): string {
|
||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
||||
|
||||
if (seconds < 60) return '刚刚';
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)} 分钟前`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)} 小时前`;
|
||||
return `${Math.floor(seconds / 86400)} 天前`;
|
||||
}
|
||||
|
||||
export default ActiveLearningPanel;
|
||||
663
desktop/src/components/AgentOnboardingWizard.tsx
Normal file
663
desktop/src/components/AgentOnboardingWizard.tsx
Normal file
@@ -0,0 +1,663 @@
|
||||
/**
|
||||
* AgentOnboardingWizard - Guided Agent creation wizard
|
||||
*
|
||||
* A 5-step wizard for creating new Agents with personality settings.
|
||||
* Inspired by OpenClaw's quick configuration modal.
|
||||
*/
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
X,
|
||||
User,
|
||||
Bot,
|
||||
Sparkles,
|
||||
Briefcase,
|
||||
Folder,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Check,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { useAgentStore, type CloneCreateOptions } from '../store/agentStore';
|
||||
import { EmojiPicker } from './ui/EmojiPicker';
|
||||
import { PersonalitySelector } from './PersonalitySelector';
|
||||
import { ScenarioTags } from './ScenarioTags';
|
||||
import type { Clone } from '../store/agentStore';
|
||||
|
||||
// === Types ===
|
||||
|
||||
interface WizardFormData {
|
||||
userName: string;
|
||||
userRole: string;
|
||||
agentName: string;
|
||||
agentRole: string;
|
||||
agentNickname: string;
|
||||
emoji: string;
|
||||
personality: string;
|
||||
scenarios: string[];
|
||||
workspaceDir: string;
|
||||
restrictFiles: boolean;
|
||||
privacyOptIn: boolean;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface AgentOnboardingWizardProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: (clone: Clone) => void;
|
||||
}
|
||||
|
||||
const initialFormData: WizardFormData = {
|
||||
userName: '',
|
||||
userRole: '',
|
||||
agentName: '',
|
||||
agentRole: '',
|
||||
agentNickname: '',
|
||||
emoji: '',
|
||||
personality: '',
|
||||
scenarios: [],
|
||||
workspaceDir: '',
|
||||
restrictFiles: true,
|
||||
privacyOptIn: false,
|
||||
notes: '',
|
||||
};
|
||||
|
||||
// === Step Configuration ===
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: '认识用户', description: '让我们了解一下您', icon: User },
|
||||
{ id: 2, title: 'Agent 身份', description: '给助手起个名字', icon: Bot },
|
||||
{ id: 3, title: '人格风格', description: '选择沟通风格', icon: Sparkles },
|
||||
{ id: 4, title: '使用场景', description: '选择应用场景', icon: Briefcase },
|
||||
{ id: 5, title: '工作环境', description: '配置工作目录', icon: Folder },
|
||||
];
|
||||
|
||||
// === Component ===
|
||||
|
||||
export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboardingWizardProps) {
|
||||
const { createClone, isLoading, error, clearError } = useAgentStore();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState<WizardFormData>(initialFormData);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
|
||||
// Reset form when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setFormData(initialFormData);
|
||||
setCurrentStep(1);
|
||||
setErrors({});
|
||||
setSubmitStatus('idle');
|
||||
clearError();
|
||||
}
|
||||
}, [isOpen, clearError]);
|
||||
|
||||
// Update form field
|
||||
const updateField = <K extends keyof WizardFormData>(field: K, value: WizardFormData[K]) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[field];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Validate current step
|
||||
const validateStep = useCallback((step: number): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
if (!formData.userName.trim()) {
|
||||
newErrors.userName = '请输入您的名字';
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (!formData.agentName.trim()) {
|
||||
newErrors.agentName = '请输入 Agent 名称';
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (!formData.emoji) {
|
||||
newErrors.emoji = '请选择一个 Emoji';
|
||||
}
|
||||
if (!formData.personality) {
|
||||
newErrors.personality = '请选择一个人格风格';
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
if (formData.scenarios.length === 0) {
|
||||
newErrors.scenarios = '请至少选择一个使用场景';
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
// Optional step, no validation
|
||||
break;
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}, [formData]);
|
||||
|
||||
// Navigate to next step
|
||||
const nextStep = () => {
|
||||
if (validateStep(currentStep)) {
|
||||
setCurrentStep((prev) => Math.min(prev + 1, steps.length));
|
||||
}
|
||||
};
|
||||
|
||||
// Navigate to previous step
|
||||
const prevStep = () => {
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async () => {
|
||||
if (!validateStep(currentStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitStatus('idle');
|
||||
|
||||
try {
|
||||
const createOptions: CloneCreateOptions = {
|
||||
name: formData.agentName,
|
||||
role: formData.agentRole || undefined,
|
||||
nickname: formData.agentNickname || undefined,
|
||||
userName: formData.userName,
|
||||
userRole: formData.userRole || undefined,
|
||||
scenarios: formData.scenarios,
|
||||
workspaceDir: formData.workspaceDir || undefined,
|
||||
restrictFiles: formData.restrictFiles,
|
||||
privacyOptIn: formData.privacyOptIn,
|
||||
emoji: formData.emoji,
|
||||
personality: formData.personality,
|
||||
notes: formData.notes || undefined,
|
||||
};
|
||||
|
||||
const clone = await createClone(createOptions);
|
||||
|
||||
if (clone) {
|
||||
setSubmitStatus('success');
|
||||
setTimeout(() => {
|
||||
onSuccess?.(clone);
|
||||
onClose();
|
||||
}, 1500);
|
||||
} else {
|
||||
setSubmitStatus('error');
|
||||
}
|
||||
} catch {
|
||||
setSubmitStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const CurrentStepIcon = steps[currentStep - 1]?.icon || Bot;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<CurrentStepIcon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
创建新 Agent
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
步骤 {currentStep}/{steps.length}: {steps[currentStep - 1]?.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center gap-1">
|
||||
{steps.map((step, index) => {
|
||||
const StepIcon = step.icon;
|
||||
const isActive = currentStep === step.id;
|
||||
const isCompleted = currentStep > step.id;
|
||||
return (
|
||||
<div key={step.id} className="flex items-center flex-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => currentStep > step.id && setCurrentStep(step.id)}
|
||||
disabled={currentStep <= step.id}
|
||||
className={cn(
|
||||
'flex items-center justify-center w-8 h-8 rounded-full text-xs font-medium transition-all',
|
||||
isActive && 'bg-primary text-white',
|
||||
isCompleted && 'bg-primary/20 text-primary cursor-pointer',
|
||||
!isActive && !isCompleted && 'bg-gray-100 dark:bg-gray-700 text-gray-400'
|
||||
)}
|
||||
>
|
||||
{isCompleted ? <Check className="w-4 h-4" /> : <StepIcon className="w-4 h-4" />}
|
||||
</button>
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 h-1 rounded-full mx-1',
|
||||
isCompleted ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentStep}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* Step 1: 认识用户 */}
|
||||
{currentStep === 1 && (
|
||||
<>
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
让我们认识一下
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
请告诉我们您的名字,让助手更好地为您服务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
您的名字 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.userName}
|
||||
onChange={(e) => updateField('userName', e.target.value)}
|
||||
placeholder="例如:张三"
|
||||
className={cn(
|
||||
'w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary',
|
||||
errors.userName ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
)}
|
||||
/>
|
||||
{errors.userName && (
|
||||
<p className="mt-1 text-xs text-red-500 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{errors.userName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
您的角色(可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.userRole}
|
||||
onChange={(e) => updateField('userRole', e.target.value)}
|
||||
placeholder="例如:产品经理、开发工程师"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 2: Agent 身份 */}
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
给您的助手起个名字
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
这将是您助手的身份标识
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Agent 名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.agentName}
|
||||
onChange={(e) => updateField('agentName', e.target.value)}
|
||||
placeholder="例如:小龙助手"
|
||||
className={cn(
|
||||
'w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary',
|
||||
errors.agentName ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
)}
|
||||
/>
|
||||
{errors.agentName && (
|
||||
<p className="mt-1 text-xs text-red-500 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{errors.agentName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Agent 角色(可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.agentRole}
|
||||
onChange={(e) => updateField('agentRole', e.target.value)}
|
||||
placeholder="例如:编程助手、写作顾问"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
昵称(可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.agentNickname}
|
||||
onChange={(e) => updateField('agentNickname', e.target.value)}
|
||||
placeholder="例如:小龙"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 3: 人格风格 */}
|
||||
{currentStep === 3 && (
|
||||
<>
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
选择人格风格
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
这决定了助手的沟通方式和性格特点
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
选择一个 Emoji <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<EmojiPicker
|
||||
value={formData.emoji}
|
||||
onChange={(emoji) => updateField('emoji', emoji)}
|
||||
/>
|
||||
{errors.emoji && (
|
||||
<p className="mt-1 text-xs text-red-500 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{errors.emoji}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
选择人格风格 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<PersonalitySelector
|
||||
value={formData.personality}
|
||||
onChange={(personality) => updateField('personality', personality)}
|
||||
/>
|
||||
{errors.personality && (
|
||||
<p className="mt-1 text-xs text-red-500 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{errors.personality}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 4: 使用场景 */}
|
||||
{currentStep === 4 && (
|
||||
<>
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
选择使用场景
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
选择您希望 Agent 协助的领域(最多5个)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ScenarioTags
|
||||
value={formData.scenarios}
|
||||
onChange={(scenarios) => updateField('scenarios', scenarios)}
|
||||
maxSelections={5}
|
||||
/>
|
||||
{errors.scenarios && (
|
||||
<p className="mt-2 text-xs text-red-500 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{errors.scenarios}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 5: 工作环境 */}
|
||||
{currentStep === 5 && (
|
||||
<>
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||||
配置工作环境
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
设置 Agent 的工作目录和权限
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
工作目录(可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.workspaceDir}
|
||||
onChange={(e) => updateField('workspaceDir', e.target.value)}
|
||||
placeholder="例如:/home/user/projects/myproject"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary font-mono"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
Agent 将在此目录下工作,留空则使用默认目录
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mt-4">
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
限制文件访问
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
仅允许访问工作目录内的文件
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateField('restrictFiles', !formData.restrictFiles)}
|
||||
className={cn(
|
||||
'w-11 h-6 rounded-full transition-colors relative',
|
||||
formData.restrictFiles ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform',
|
||||
)}
|
||||
style={{ left: formData.restrictFiles ? '22px' : '2px' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
匿名使用数据
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
允许收集匿名使用数据以改进产品
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateField('privacyOptIn', !formData.privacyOptIn)}
|
||||
className={cn(
|
||||
'w-11 h-6 rounded-full transition-colors relative',
|
||||
formData.privacyOptIn ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform',
|
||||
)}
|
||||
style={{ left: formData.privacyOptIn ? '22px' : '2px' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
备注(可选)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.notes}
|
||||
onChange={(e) => updateField('notes', e.target.value)}
|
||||
placeholder="关于此 Agent 的备注信息..."
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Summary Preview */}
|
||||
<div className="p-4 bg-primary/5 rounded-lg mt-4">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
|
||||
配置预览
|
||||
</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{formData.emoji || '🤖'}</span>
|
||||
<span className="font-medium">{formData.agentName || '未命名'}</span>
|
||||
{formData.agentNickname && (
|
||||
<span className="text-gray-500">({formData.agentNickname})</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
用户:{formData.userName}
|
||||
{formData.userRole && ` (${formData.userRole})`}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{formData.scenarios.map((id) => (
|
||||
<span
|
||||
key={id}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary rounded text-xs"
|
||||
>
|
||||
{id}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Messages */}
|
||||
{submitStatus === 'success' && (
|
||||
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg text-green-700 dark:text-green-400 mt-4">
|
||||
<Check className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm">Agent 创建成功!</span>
|
||||
</div>
|
||||
)}
|
||||
{submitStatus === 'error' && (
|
||||
<div className="flex items-center gap-2 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg text-red-700 dark:text-red-400 mt-4">
|
||||
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm">{error || '创建失败,请重试'}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
disabled={currentStep === 1}
|
||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
上一步
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{currentStep < steps.length ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors flex items-center gap-1"
|
||||
>
|
||||
下一步
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoading || submitStatus === 'success'}
|
||||
className="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
创建中...
|
||||
</>
|
||||
) : submitStatus === 'success' ? (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
已创建
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
完成
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AgentOnboardingWizard;
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
import {
|
||||
getAutonomyManager,
|
||||
DEFAULT_AUTONOMY_CONFIGS,
|
||||
type AutonomyManager,
|
||||
type AutonomyConfig,
|
||||
type AutonomyLevel,
|
||||
type AuditLogEntry,
|
||||
@@ -135,13 +134,11 @@ function LevelSelector({
|
||||
}
|
||||
|
||||
function ActionToggle({
|
||||
action,
|
||||
label,
|
||||
enabled,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
action: ActionType;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
onChange: (enabled: boolean) => void;
|
||||
@@ -331,7 +328,6 @@ export function AutonomyConfig({ className = '', onConfigChange }: AutonomyConfi
|
||||
</div>
|
||||
<div className="pl-6 space-y-1 border-l-2 border-gray-200 dark:border-gray-700">
|
||||
<ActionToggle
|
||||
action="memory_save"
|
||||
label="自动保存记忆"
|
||||
enabled={config.allowedActions.memoryAutoSave}
|
||||
onChange={(enabled) =>
|
||||
@@ -341,7 +337,6 @@ export function AutonomyConfig({ className = '', onConfigChange }: AutonomyConfi
|
||||
}
|
||||
/>
|
||||
<ActionToggle
|
||||
action="identity_update"
|
||||
label="自动更新身份文件"
|
||||
enabled={config.allowedActions.identityAutoUpdate}
|
||||
onChange={(enabled) =>
|
||||
@@ -351,7 +346,6 @@ export function AutonomyConfig({ className = '', onConfigChange }: AutonomyConfi
|
||||
}
|
||||
/>
|
||||
<ActionToggle
|
||||
action="skill_install"
|
||||
label="自动安装技能"
|
||||
enabled={config.allowedActions.skillAutoInstall}
|
||||
onChange={(enabled) =>
|
||||
@@ -361,7 +355,6 @@ export function AutonomyConfig({ className = '', onConfigChange }: AutonomyConfi
|
||||
}
|
||||
/>
|
||||
<ActionToggle
|
||||
action="selfModification"
|
||||
label="自我修改行为"
|
||||
enabled={config.allowedActions.selfModification}
|
||||
onChange={(enabled) =>
|
||||
@@ -371,7 +364,6 @@ export function AutonomyConfig({ className = '', onConfigChange }: AutonomyConfi
|
||||
}
|
||||
/>
|
||||
<ActionToggle
|
||||
action="compaction_run"
|
||||
label="自动上下文压缩"
|
||||
enabled={config.allowedActions.autoCompaction}
|
||||
onChange={(enabled) =>
|
||||
@@ -381,7 +373,6 @@ export function AutonomyConfig({ className = '', onConfigChange }: AutonomyConfi
|
||||
}
|
||||
/>
|
||||
<ActionToggle
|
||||
action="reflection_run"
|
||||
label="自动反思"
|
||||
enabled={config.allowedActions.autoReflection}
|
||||
onChange={(enabled) =>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useChatStore, Message } from '../store/chatStore';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
import { Paperclip, ChevronDown, Terminal, SquarePen, ArrowUp, MessageSquare } from 'lucide-react';
|
||||
import { Button, EmptyState } from './ui';
|
||||
import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations';
|
||||
import { FirstConversationPrompt } from './FirstConversationPrompt';
|
||||
|
||||
const MODELS = ['glm-5', 'qwen3.5-plus', 'kimi-k2.5', 'minimax-m2.5'];
|
||||
|
||||
@@ -14,13 +15,28 @@ export function ChatArea() {
|
||||
sendMessage: sendToGateway, setCurrentModel, initStreamListener,
|
||||
newConversation,
|
||||
} = useChatStore();
|
||||
const { connectionState } = useGatewayStore();
|
||||
const { connectionState, clones } = useGatewayStore();
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [showModelPicker, setShowModelPicker] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Get current clone for first conversation prompt
|
||||
const currentClone = useMemo(() => {
|
||||
if (!currentAgent) return null;
|
||||
return clones.find((c) => c.id === currentAgent.id) || null;
|
||||
}, [currentAgent, clones]);
|
||||
|
||||
// Check if should show first conversation prompt
|
||||
const showFirstPrompt = messages.length === 0 && currentClone && !currentClone.onboardingCompleted;
|
||||
|
||||
// Handle suggestion click from first conversation prompt
|
||||
const handleSelectSuggestion = (text: string) => {
|
||||
setInput(text);
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
// Auto-resize textarea
|
||||
const adjustTextarea = useCallback(() => {
|
||||
const el = textareaRef.current;
|
||||
@@ -104,11 +120,18 @@ export function ChatArea() {
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
>
|
||||
<EmptyState
|
||||
icon={<MessageSquare className="w-8 h-8" />}
|
||||
title="欢迎使用 ZCLAW"
|
||||
description={connected ? '发送消息开始对话' : '请先在设置中连接 Gateway'}
|
||||
/>
|
||||
{showFirstPrompt && currentClone ? (
|
||||
<FirstConversationPrompt
|
||||
clone={currentClone}
|
||||
onSelectSuggestion={handleSelectSuggestion}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={<MessageSquare className="w-8 h-8" />}
|
||||
title="欢迎使用 ZCLAW"
|
||||
description={connected ? '发送消息开始对话' : '请先在设置中连接 Gateway'}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,53 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
import { toChatAgent, useChatStore } from '../store/chatStore';
|
||||
import { Bot, Plus, X, Globe, Cat, Search, BarChart2, AlertCircle, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface CloneFormData {
|
||||
name: string;
|
||||
role: string;
|
||||
nickname: string;
|
||||
scenarios: string;
|
||||
workspaceDir: string;
|
||||
userName: string;
|
||||
userRole: string;
|
||||
restrictFiles: boolean;
|
||||
privacyOptIn: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_WORKSPACE = '~/.openfang/zclaw-workspace';
|
||||
|
||||
function createFormFromDraft(quickConfig: {
|
||||
agentName?: string;
|
||||
agentRole?: string;
|
||||
agentNickname?: string;
|
||||
scenarios?: string[];
|
||||
workspaceDir?: string;
|
||||
userName?: string;
|
||||
userRole?: string;
|
||||
restrictFiles?: boolean;
|
||||
privacyOptIn?: boolean;
|
||||
}): CloneFormData {
|
||||
return {
|
||||
name: quickConfig.agentName || '',
|
||||
role: quickConfig.agentRole || '',
|
||||
nickname: quickConfig.agentNickname || '',
|
||||
scenarios: quickConfig.scenarios?.join(', ') || '',
|
||||
workspaceDir: quickConfig.workspaceDir || DEFAULT_WORKSPACE,
|
||||
userName: quickConfig.userName || '',
|
||||
userRole: quickConfig.userRole || '',
|
||||
restrictFiles: quickConfig.restrictFiles ?? true,
|
||||
privacyOptIn: quickConfig.privacyOptIn ?? false,
|
||||
};
|
||||
}
|
||||
import { Bot, Plus, X, Globe, Cat, Search, BarChart2, Sparkles } from 'lucide-react';
|
||||
import { AgentOnboardingWizard } from './AgentOnboardingWizard';
|
||||
import type { Clone } from '../store/agentStore';
|
||||
|
||||
export function CloneManager() {
|
||||
const { clones, loadClones, createClone, deleteClone, connectionState, quickConfig, saveQuickConfig, error: storeError } = useGatewayStore();
|
||||
const { clones, loadClones, deleteClone, connectionState, quickConfig } = useGatewayStore();
|
||||
const { agents, currentAgent, setCurrentAgent } = useChatStore();
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [form, setForm] = useState<CloneFormData>(createFormFromDraft({}));
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
const [showWizard, setShowWizard] = useState(false);
|
||||
|
||||
const connected = connectionState === 'connected';
|
||||
|
||||
@@ -55,75 +16,7 @@ export function CloneManager() {
|
||||
if (connected) {
|
||||
loadClones();
|
||||
}
|
||||
}, [connected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showForm) {
|
||||
setForm(createFormFromDraft(quickConfig));
|
||||
}
|
||||
}, [showForm, quickConfig]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!form.name.trim()) return;
|
||||
setCreateError(null);
|
||||
setIsCreating(true);
|
||||
|
||||
try {
|
||||
const scenarios = form.scenarios
|
||||
? form.scenarios.split(',').map((s) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
await saveQuickConfig({
|
||||
agentName: form.name,
|
||||
agentRole: form.role || undefined,
|
||||
agentNickname: form.nickname || undefined,
|
||||
scenarios,
|
||||
workspaceDir: form.workspaceDir || undefined,
|
||||
userName: form.userName || undefined,
|
||||
userRole: form.userRole || undefined,
|
||||
restrictFiles: form.restrictFiles,
|
||||
privacyOptIn: form.privacyOptIn,
|
||||
});
|
||||
|
||||
const clone = await createClone({
|
||||
name: form.name,
|
||||
role: form.role || undefined,
|
||||
nickname: form.nickname || undefined,
|
||||
scenarios,
|
||||
workspaceDir: form.workspaceDir || undefined,
|
||||
userName: form.userName || undefined,
|
||||
userRole: form.userRole || undefined,
|
||||
restrictFiles: form.restrictFiles,
|
||||
privacyOptIn: form.privacyOptIn,
|
||||
});
|
||||
|
||||
if (clone) {
|
||||
setCurrentAgent(toChatAgent(clone));
|
||||
setForm(createFormFromDraft({
|
||||
...quickConfig,
|
||||
agentName: form.name,
|
||||
agentRole: form.role,
|
||||
agentNickname: form.nickname,
|
||||
scenarios,
|
||||
workspaceDir: form.workspaceDir,
|
||||
userName: form.userName,
|
||||
userRole: form.userRole,
|
||||
restrictFiles: form.restrictFiles,
|
||||
privacyOptIn: form.privacyOptIn,
|
||||
}));
|
||||
setShowForm(false);
|
||||
} else {
|
||||
// Show error from store or generic message
|
||||
const errorMsg = storeError || '创建分身失败。请检查 Gateway 连接状态和后端日志。';
|
||||
setCreateError(errorMsg);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
setCreateError(`创建失败: ${errorMsg}`);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
}, [connected, loadClones]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('确定删除该分身?')) {
|
||||
@@ -131,146 +24,62 @@ export function CloneManager() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleWizardSuccess = (clone: Clone) => {
|
||||
setCurrentAgent(toChatAgent(clone));
|
||||
setShowWizard(false);
|
||||
};
|
||||
|
||||
// Merge gateway clones with local agents for display
|
||||
const displayClones = clones.length > 0 ? clones : agents.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
role: '默认助手',
|
||||
nickname: a.name,
|
||||
scenarios: [],
|
||||
scenarios: [] as string[],
|
||||
workspaceDir: '~/.openfang/zclaw-workspace',
|
||||
userName: quickConfig.userName || '未设置',
|
||||
userRole: '',
|
||||
restrictFiles: true,
|
||||
privacyOptIn: false,
|
||||
createdAt: '',
|
||||
onboardingCompleted: true,
|
||||
emoji: undefined as string | undefined,
|
||||
personality: undefined as string | undefined,
|
||||
}));
|
||||
|
||||
// Function to assign pseudo icons/colors based on names for UI matching
|
||||
const getIconAndColor = (name: string) => {
|
||||
if (name.includes('Browser') || name.includes('浏览器')) {
|
||||
return { icon: <Globe className="w-5 h-5" />, bg: 'bg-blue-500 text-white' };
|
||||
// Function to get display emoji or icon for clone
|
||||
const getCloneDisplay = (clone: typeof displayClones[0]) => {
|
||||
// If clone has emoji, use it
|
||||
if (clone.emoji) {
|
||||
return {
|
||||
emoji: clone.emoji,
|
||||
icon: null,
|
||||
bg: 'bg-gradient-to-br from-orange-400 to-red-500',
|
||||
};
|
||||
}
|
||||
if (name.includes('AutoClaw') || name.includes('ZCLAW')) {
|
||||
return { icon: <Cat className="w-6 h-6" />, bg: 'bg-gradient-to-br from-orange-400 to-red-500 text-white' };
|
||||
|
||||
// Fallback to icon based on name
|
||||
if (clone.name.includes('Browser') || clone.name.includes('浏览器')) {
|
||||
return { emoji: null, icon: <Globe className="w-5 h-5" />, bg: 'bg-blue-500 text-white' };
|
||||
}
|
||||
if (name.includes('沉思')) {
|
||||
return { icon: <Search className="w-5 h-5" />, bg: 'bg-blue-100 text-blue-600' };
|
||||
if (clone.name.includes('AutoClaw') || clone.name.includes('ZCLAW')) {
|
||||
return { emoji: null, icon: <Cat className="w-6 h-6" />, bg: 'bg-gradient-to-br from-orange-400 to-red-500 text-white' };
|
||||
}
|
||||
if (name.includes('监控')) {
|
||||
return { icon: <BarChart2 className="w-5 h-5" />, bg: 'bg-orange-100 text-orange-600' };
|
||||
if (clone.name.includes('沉思')) {
|
||||
return { emoji: null, icon: <Search className="w-5 h-5" />, bg: 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-300' };
|
||||
}
|
||||
return { icon: <Bot className="w-5 h-5" />, bg: 'bg-gray-200 text-gray-600' };
|
||||
if (clone.name.includes('监控')) {
|
||||
return { emoji: null, icon: <BarChart2 className="w-5 h-5" />, bg: 'bg-orange-100 text-orange-600 dark:bg-orange-900 dark:text-orange-300' };
|
||||
}
|
||||
return { emoji: null, icon: <Bot className="w-5 h-5" />, bg: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300' };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col py-2">
|
||||
{/* Create form */}
|
||||
{showForm && (
|
||||
<div className="mx-2 mb-2 p-3 border border-gray-200 rounded-lg bg-white space-y-2 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-900">快速配置 Agent</span>
|
||||
<button onClick={() => setShowForm(false)} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={e => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="名称 (必填)"
|
||||
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={form.role}
|
||||
onChange={e => setForm({ ...form, role: e.target.value })}
|
||||
placeholder="角色 (如: 代码助手)"
|
||||
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={form.nickname}
|
||||
onChange={e => setForm({ ...form, nickname: e.target.value })}
|
||||
placeholder="昵称 / 对你的称呼"
|
||||
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={form.scenarios}
|
||||
onChange={e => setForm({ ...form, scenarios: e.target.value })}
|
||||
placeholder="场景标签 (逗号分隔)"
|
||||
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={form.workspaceDir}
|
||||
onChange={e => setForm({ ...form, workspaceDir: e.target.value })}
|
||||
placeholder="工作目录"
|
||||
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={form.userName}
|
||||
onChange={e => setForm({ ...form, userName: e.target.value })}
|
||||
placeholder="你的名字"
|
||||
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={form.userRole}
|
||||
onChange={e => setForm({ ...form, userRole: e.target.value })}
|
||||
placeholder="你的角色"
|
||||
className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:border-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center justify-between text-xs text-gray-600 px-1">
|
||||
<span>限制文件访问范围</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.restrictFiles}
|
||||
onChange={e => setForm({ ...form, restrictFiles: e.target.checked })}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between text-xs text-gray-600 px-1">
|
||||
<span>加入优化计划</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.privacyOptIn}
|
||||
onChange={e => setForm({ ...form, privacyOptIn: e.target.checked })}
|
||||
/>
|
||||
</label>
|
||||
{createError && (
|
||||
<div className="flex items-center gap-2 text-xs text-red-600 bg-red-50 px-2 py-1.5 rounded">
|
||||
<AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="flex-1">{createError}</span>
|
||||
<button onClick={() => setCreateError(null)} className="hover:text-red-800">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
disabled={!form.name.trim() || isCreating}
|
||||
className="w-full text-xs bg-gray-900 text-white rounded py-1.5 hover:bg-gray-800 disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||
创建中...
|
||||
</>
|
||||
) : (
|
||||
'完成配置'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Clone list */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
{displayClones.map((clone, idx) => {
|
||||
const { icon, bg } = getIconAndColor(clone.name);
|
||||
const { emoji, icon, bg } = getCloneDisplay(clone);
|
||||
const isActive = currentAgent ? currentAgent.id === clone.id : idx === 0;
|
||||
const canDelete = clones.length > 0;
|
||||
|
||||
@@ -279,18 +88,26 @@ export function CloneManager() {
|
||||
key={clone.id}
|
||||
onClick={() => setCurrentAgent(toChatAgent(clone))}
|
||||
className={`group sidebar-item mx-2 px-3 py-3 rounded-lg cursor-pointer mb-1 flex items-start gap-3 transition-colors ${
|
||||
isActive ? 'bg-white shadow-sm border border-gray-100' : 'hover:bg-black/5'
|
||||
isActive ? 'bg-white dark:bg-gray-800 shadow-sm border border-gray-100 dark:border-gray-700' : 'hover:bg-black/5 dark:hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ${bg}`}>
|
||||
{icon}
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ${emoji ? bg : bg}`}>
|
||||
{emoji ? (
|
||||
<span className="text-xl">{emoji}</span>
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className={`truncate ${isActive ? 'font-semibold text-gray-900' : 'font-medium text-gray-900'}`}>{clone.name}</span>
|
||||
<span className={`truncate ${isActive ? 'font-semibold text-gray-900 dark:text-white' : 'font-medium text-gray-900 dark:text-white'}`}>
|
||||
{clone.name}
|
||||
</span>
|
||||
{isActive ? <span className="text-xs text-orange-500">当前</span> : null}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 truncate">{clone.role || '新分身'}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{clone.role || clone.personality || '新分身'}
|
||||
</p>
|
||||
</div>
|
||||
{canDelete && (
|
||||
<button
|
||||
@@ -305,27 +122,38 @@ export function CloneManager() {
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Add new clone button as an item if we want, or keep the traditional way */}
|
||||
{!showForm && (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (connected) {
|
||||
setShowForm(true);
|
||||
}
|
||||
}}
|
||||
className={`sidebar-item mx-2 px-3 py-3 rounded-lg mb-1 flex items-center gap-3 transition-colors border border-dashed border-gray-300 ${
|
||||
connected
|
||||
? 'cursor-pointer text-gray-500 hover:text-gray-900 hover:bg-black/5'
|
||||
: 'cursor-not-allowed text-gray-400 bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 bg-gray-50">
|
||||
{/* Add new clone button */}
|
||||
<div
|
||||
onClick={() => {
|
||||
if (connected) {
|
||||
setShowWizard(true);
|
||||
}
|
||||
}}
|
||||
className={`sidebar-item mx-2 px-3 py-3 rounded-lg mb-1 flex items-center gap-3 transition-colors border border-dashed border-gray-300 dark:border-gray-600 ${
|
||||
connected
|
||||
? 'cursor-pointer text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5'
|
||||
: 'cursor-not-allowed text-gray-400 dark:text-gray-500 bg-gray-50 dark:bg-gray-800/50'
|
||||
}`}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 bg-gray-50 dark:bg-gray-800">
|
||||
{connected ? (
|
||||
<Sparkles className="w-5 h-5 text-primary" />
|
||||
) : (
|
||||
<Plus className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">{connected ? '快速配置新 Agent' : '连接 Gateway 后创建'}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium">
|
||||
{connected ? '创建新 Agent' : '连接 Gateway 后创建'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Onboarding Wizard Modal */}
|
||||
<AgentOnboardingWizard
|
||||
isOpen={showWizard}
|
||||
onClose={() => setShowWizard(false)}
|
||||
onSuccess={handleWizardSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,54 @@ import {
|
||||
Bell,
|
||||
} from 'lucide-react';
|
||||
|
||||
// === ReDoS Protection ===
|
||||
|
||||
const MAX_PATTERN_LENGTH = 200;
|
||||
const REGEX_TIMEOUT_MS = 100;
|
||||
|
||||
// Dangerous regex patterns that can cause catastrophic backtracking
|
||||
const DANGEROUS_PATTERNS = [
|
||||
/\([^)]*\+[^)]*\)\+/, // Nested quantifiers like (a+)+
|
||||
/\([^)]*\*[^)]*\)\*/, // Nested quantifiers like (a*)*
|
||||
/\([^)]*\+[^)]*\)\*/, // Mixed nested quantifiers
|
||||
/\([^)]*\*[^)]*\)\+/, // Mixed nested quantifiers
|
||||
/\.\*\.\*/, // Multiple greedy wildcards
|
||||
/\.+\.\+/, // Multiple greedy wildcards
|
||||
/(.*)\1{3,}/, // Backreference loops
|
||||
];
|
||||
|
||||
function validateRegexPattern(pattern: string): { valid: boolean; error?: string } {
|
||||
// Length check
|
||||
if (pattern.length > MAX_PATTERN_LENGTH) {
|
||||
return { valid: false, error: `Pattern too long (max ${MAX_PATTERN_LENGTH} chars)` };
|
||||
}
|
||||
|
||||
// Check for dangerous constructs
|
||||
for (const dangerous of DANGEROUS_PATTERNS) {
|
||||
if (dangerous.test(pattern)) {
|
||||
return { valid: false, error: 'Pattern contains potentially dangerous constructs' };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate syntax and check execution time
|
||||
try {
|
||||
const regex = new RegExp(pattern);
|
||||
const testString = 'a'.repeat(20) + 'b'.repeat(20);
|
||||
const start = Date.now();
|
||||
regex.test(testString);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
if (elapsed > REGEX_TIMEOUT_MS) {
|
||||
return { valid: false, error: 'Pattern is too complex (execution timeout)' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Invalid pattern';
|
||||
return { valid: false, error: `Invalid regular expression: ${message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// === Types ===
|
||||
|
||||
type TriggerType = 'webhook' | 'event' | 'message';
|
||||
@@ -146,11 +194,10 @@ export function CreateTriggerModal({ isOpen, onClose, onSuccess }: CreateTrigger
|
||||
if (!formData.pattern.trim()) {
|
||||
newErrors.pattern = 'Pattern is required';
|
||||
} else {
|
||||
// Validate regex pattern
|
||||
try {
|
||||
new RegExp(formData.pattern);
|
||||
} catch {
|
||||
newErrors.pattern = 'Invalid regular expression pattern';
|
||||
// Validate regex pattern with ReDoS protection
|
||||
const validation = validateRegexPattern(formData.pattern);
|
||||
if (!validation.valid) {
|
||||
newErrors.pattern = validation.error || 'Invalid pattern';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Clock, CheckCircle, AlertCircle, Hourglass, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
@@ -14,7 +15,7 @@ const statusConfig: Record<FeedbackStatus, { label: string; color: string; icon:
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
bug: 'Bug Report',
|
||||
feature: 'Feature Request';
|
||||
feature: 'Feature Request',
|
||||
general: 'General Feedback',
|
||||
};
|
||||
const priorityLabels: Record<string, string> = {
|
||||
@@ -27,7 +28,7 @@ interface FeedbackHistoryProps {
|
||||
onViewDetails?: (feedback: FeedbackSubmission) => void;
|
||||
}
|
||||
|
||||
export function FeedbackHistory({ onViewDetails }: FeedbackHistoryProps) {
|
||||
export function FeedbackHistory({ onViewDetails: _onViewDetails }: FeedbackHistoryProps) {
|
||||
const { feedbackItems, deleteFeedback, updateFeedbackStatus } = useFeedbackStore();
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
@@ -147,7 +148,7 @@ export function FeedbackHistory({ onViewDetails }: FeedbackHistoryProps) {
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<p>App Version: {feedback.metadata.appVersion}</p>
|
||||
<p>OS: {feedback.metadata.os}</p>
|
||||
<p>Submitted: {format(feedback.createdAt)}</p>
|
||||
<p>Submitted: {formatDate(feedback.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, Send, Bug, Lightbulb, MessageSquare, AlertCircle, Upload, Trash2 } from 'lucide-react';
|
||||
import { useFeedbackStore, type FeedbackType, type FeedbackPriority } from './feedbackStore';
|
||||
import { useFeedbackStore, type FeedbackType, type FeedbackPriority, type FeedbackAttachment } from './feedbackStore';
|
||||
import { Button } from '../ui';
|
||||
import { useToast } from '../ui/Toast';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
|
||||
interface FeedbackModalProps {
|
||||
onClose: () => void;
|
||||
@@ -39,9 +40,9 @@ export function FeedbackModal({ onClose }: FeedbackModalProps) {
|
||||
}
|
||||
|
||||
// Convert files to base64 for storage
|
||||
const processedAttachments = await Promise.all(
|
||||
const processedAttachments: FeedbackAttachment[] = await Promise.all(
|
||||
attachments.map(async (file) => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise<FeedbackAttachment>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve({
|
||||
@@ -57,7 +58,7 @@ export function FeedbackModal({ onClose }: FeedbackModalProps) {
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await submitFeedback({
|
||||
await submitFeedback({
|
||||
type,
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
@@ -70,16 +71,14 @@ export function FeedbackModal({ onClose }: FeedbackModalProps) {
|
||||
},
|
||||
});
|
||||
|
||||
if (result) {
|
||||
toast('Feedback submitted successfully!', 'success');
|
||||
// Reset form
|
||||
setTitle('');
|
||||
setDescription('');
|
||||
setAttachments([]);
|
||||
setType('bug');
|
||||
setPriority('medium');
|
||||
onClose();
|
||||
}
|
||||
toast('Feedback submitted successfully!', 'success');
|
||||
// Reset form
|
||||
setTitle('');
|
||||
setDescription('');
|
||||
setAttachments([]);
|
||||
setType('bug');
|
||||
setPriority('medium');
|
||||
onClose();
|
||||
} catch (err) {
|
||||
toast('Failed to submit feedback. Please try again.', 'error');
|
||||
}
|
||||
@@ -277,7 +276,7 @@ export function FeedbackModal({ onClose }: FeedbackModalProps) {
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => { handleSubmit().catch(() => {}); }}
|
||||
onClick={() => { handleSubmit().catch(silentErrorHandler('FeedbackModal')); }}
|
||||
loading={isLoading}
|
||||
disabled={!title.trim() || !description.trim()}
|
||||
>
|
||||
|
||||
@@ -78,7 +78,7 @@ export const useFeedbackStore = create<FeedbackStore>()(
|
||||
openModal: () => set({ isModalOpen: true }),
|
||||
closeModal: () => set({ isModalOpen: false }),
|
||||
|
||||
submitFeedback: async (feedback) => {
|
||||
submitFeedback: async (feedback): Promise<void> => {
|
||||
const { feedbackItems } = get();
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
@@ -106,8 +106,6 @@ export const useFeedbackStore = create<FeedbackStore>()(
|
||||
isLoading: false,
|
||||
isModalOpen: false,
|
||||
});
|
||||
|
||||
return newFeedback;
|
||||
} catch (err) {
|
||||
set({
|
||||
isLoading: false,
|
||||
|
||||
124
desktop/src/components/FirstConversationPrompt.tsx
Normal file
124
desktop/src/components/FirstConversationPrompt.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* FirstConversationPrompt - Welcome prompt for new Agents
|
||||
*
|
||||
* Displays a personalized welcome message and quick start suggestions
|
||||
* when entering a new Agent's chat for the first time.
|
||||
*/
|
||||
import { motion } from 'framer-motion';
|
||||
import { Lightbulb, ArrowRight } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import {
|
||||
generateWelcomeMessage,
|
||||
getQuickStartSuggestions,
|
||||
getScenarioById,
|
||||
type QuickStartSuggestion,
|
||||
} from '../lib/personality-presets';
|
||||
import type { Clone } from '../store/agentStore';
|
||||
|
||||
interface FirstConversationPromptProps {
|
||||
clone: Clone;
|
||||
onSelectSuggestion?: (text: string) => void;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export function FirstConversationPrompt({
|
||||
clone,
|
||||
onSelectSuggestion,
|
||||
}: FirstConversationPromptProps) {
|
||||
// Generate welcome message
|
||||
const welcomeMessage = generateWelcomeMessage({
|
||||
userName: clone.userName,
|
||||
agentName: clone.nickname || clone.name,
|
||||
emoji: clone.emoji,
|
||||
personality: clone.personality,
|
||||
scenarios: clone.scenarios,
|
||||
});
|
||||
|
||||
// Get quick start suggestions based on scenarios
|
||||
const suggestions = getQuickStartSuggestions(clone.scenarios || []);
|
||||
|
||||
const handleSuggestionClick = (suggestion: QuickStartSuggestion) => {
|
||||
onSelectSuggestion?.(suggestion.text);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="flex flex-col items-center justify-center py-12 px-4"
|
||||
>
|
||||
{/* Avatar with emoji */}
|
||||
<div className="mb-6">
|
||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-primary/20 to-primary/10 dark:from-primary/30 dark:to-primary/20 flex items-center justify-center shadow-lg">
|
||||
<span className="text-4xl">{clone.emoji || '🦞'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Welcome message */}
|
||||
<div className="text-center max-w-md mb-8">
|
||||
<p className="text-lg text-gray-700 dark:text-gray-200 whitespace-pre-line leading-relaxed">
|
||||
{welcomeMessage}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick start suggestions */}
|
||||
<div className="w-full max-w-lg space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-3">
|
||||
<Lightbulb className="w-4 h-4" />
|
||||
<span>快速开始</span>
|
||||
</div>
|
||||
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<motion.button
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-4 py-3 rounded-xl',
|
||||
'bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700',
|
||||
'hover:bg-gray-100 dark:hover:bg-gray-800 hover:border-primary/30',
|
||||
'transition-all duration-200 group text-left'
|
||||
)}
|
||||
>
|
||||
<span className="text-xl flex-shrink-0">{suggestion.icon}</span>
|
||||
<span className="flex-1 text-sm text-gray-700 dark:text-gray-200">
|
||||
{suggestion.text}
|
||||
</span>
|
||||
<ArrowRight className="w-4 h-4 text-gray-400 group-hover:text-primary transition-colors flex-shrink-0" />
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Scenario tags */}
|
||||
{clone.scenarios && clone.scenarios.length > 0 && (
|
||||
<div className="mt-8 flex flex-wrap gap-2 justify-center">
|
||||
{clone.scenarios.map((scenarioId) => {
|
||||
const scenario = getScenarioById(scenarioId);
|
||||
if (!scenario) return null;
|
||||
return (
|
||||
<span
|
||||
key={scenarioId}
|
||||
className={cn(
|
||||
'px-3 py-1 rounded-full text-xs font-medium',
|
||||
'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary'
|
||||
)}
|
||||
>
|
||||
{scenario.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dismiss hint */}
|
||||
<p className="mt-8 text-xs text-gray-400 dark:text-gray-500">
|
||||
发送消息开始对话,或点击上方建议
|
||||
</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FirstConversationPrompt;
|
||||
619
desktop/src/components/MemoryGraph.tsx
Normal file
619
desktop/src/components/MemoryGraph.tsx
Normal file
@@ -0,0 +1,619 @@
|
||||
/**
|
||||
* MemoryGraph - 记忆图谱可视化组件
|
||||
*
|
||||
* 使用 Canvas 实现力导向图布局,展示记忆之间的关联关系。
|
||||
*
|
||||
* 功能:
|
||||
* - 力导向布局算法
|
||||
* - 节点拖拽
|
||||
* - 类型筛选
|
||||
* - 搜索高亮
|
||||
* - 导出图片
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Maximize2,
|
||||
Download,
|
||||
Search,
|
||||
Filter,
|
||||
X,
|
||||
RefreshCw,
|
||||
Tag,
|
||||
Clock,
|
||||
Star,
|
||||
} from 'lucide-react';
|
||||
import { Button, Badge } from './ui';
|
||||
import {
|
||||
useMemoryGraphStore,
|
||||
type GraphNode,
|
||||
type GraphEdge,
|
||||
type MemoryType,
|
||||
} from '../store/memoryGraphStore';
|
||||
import { useChatStore } from '../store/chatStore';
|
||||
import { cardHover, defaultTransition } from '../lib/animations';
|
||||
|
||||
// Mark as intentionally unused for future use
|
||||
void cardHover;
|
||||
void defaultTransition;
|
||||
|
||||
// === Constants ===
|
||||
|
||||
const NODE_COLORS: Record<MemoryType, { fill: string; stroke: string; text: string }> = {
|
||||
fact: { fill: '#3b82f6', stroke: '#1d4ed8', text: '#ffffff' },
|
||||
preference: { fill: '#f59e0b', stroke: '#d97706', text: '#ffffff' },
|
||||
lesson: { fill: '#10b981', stroke: '#059669', text: '#ffffff' },
|
||||
context: { fill: '#8b5cf6', stroke: '#7c3aed', text: '#ffffff' },
|
||||
task: { fill: '#ef4444', stroke: '#dc2626', text: '#ffffff' },
|
||||
};
|
||||
|
||||
const TYPE_LABELS: Record<MemoryType, string> = {
|
||||
fact: '事实',
|
||||
preference: '偏好',
|
||||
lesson: '经验',
|
||||
context: '上下文',
|
||||
task: '任务',
|
||||
};
|
||||
|
||||
const NODE_RADIUS = 20;
|
||||
const REPULSION_STRENGTH = 5000;
|
||||
const ATTRACTION_STRENGTH = 0.01;
|
||||
const DAMPING = 0.9;
|
||||
const MIN_VELOCITY = 0.01;
|
||||
|
||||
// === Force-Directed Layout ===
|
||||
|
||||
function simulateStep(
|
||||
nodes: GraphNode[],
|
||||
edges: GraphEdge[],
|
||||
width: number,
|
||||
height: number
|
||||
): GraphNode[] {
|
||||
const updatedNodes = nodes.map(node => ({ ...node }));
|
||||
|
||||
// 斥力 (节点间)
|
||||
for (let i = 0; i < updatedNodes.length; i++) {
|
||||
for (let j = i + 1; j < updatedNodes.length; j++) {
|
||||
const n1 = updatedNodes[i];
|
||||
const n2 = updatedNodes[j];
|
||||
const dx = n2.x - n1.x;
|
||||
const dy = n2.y - n1.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const force = REPULSION_STRENGTH / (dist * dist);
|
||||
|
||||
const fx = (dx / dist) * force;
|
||||
const fy = (dy / dist) * force;
|
||||
|
||||
n1.vx -= fx;
|
||||
n1.vy -= fy;
|
||||
n2.vx += fx;
|
||||
n2.vy += fy;
|
||||
}
|
||||
}
|
||||
|
||||
// 引力 (边)
|
||||
for (const edge of edges) {
|
||||
const source = updatedNodes.find(n => n.id === edge.source);
|
||||
const target = updatedNodes.find(n => n.id === edge.target);
|
||||
if (!source || !target) continue;
|
||||
|
||||
const dx = target.x - source.x;
|
||||
const dy = target.y - source.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const force = dist * ATTRACTION_STRENGTH * edge.strength;
|
||||
|
||||
const fx = (dx / dist) * force;
|
||||
const fy = (dy / dist) * force;
|
||||
|
||||
source.vx += fx;
|
||||
source.vy += fy;
|
||||
target.vx -= fx;
|
||||
target.vy -= fy;
|
||||
}
|
||||
|
||||
// 中心引力
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const centerForce = 0.001;
|
||||
|
||||
for (const node of updatedNodes) {
|
||||
node.vx += (centerX - node.x) * centerForce;
|
||||
node.vy += (centerY - node.y) * centerForce;
|
||||
}
|
||||
|
||||
// 更新位置
|
||||
for (const node of updatedNodes) {
|
||||
node.vx *= DAMPING;
|
||||
node.vy *= DAMPING;
|
||||
|
||||
if (Math.abs(node.vx) < MIN_VELOCITY) node.vx = 0;
|
||||
if (Math.abs(node.vy) < MIN_VELOCITY) node.vy = 0;
|
||||
|
||||
node.x += node.vx;
|
||||
node.y += node.vy;
|
||||
|
||||
// 边界约束
|
||||
node.x = Math.max(NODE_RADIUS, Math.min(width - NODE_RADIUS, node.x));
|
||||
node.y = Math.max(NODE_RADIUS, Math.min(height - NODE_RADIUS, node.y));
|
||||
}
|
||||
|
||||
return updatedNodes;
|
||||
}
|
||||
|
||||
// === Main Component ===
|
||||
|
||||
interface MemoryGraphProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const animationRef = useRef<number | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragNode, setDragNode] = useState<string | null>(null);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const { currentAgent } = useChatStore();
|
||||
const agentId = currentAgent?.id || 'default';
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
filter,
|
||||
layout,
|
||||
selectedNodeId,
|
||||
showLabels,
|
||||
simulationRunning,
|
||||
loadGraph,
|
||||
setFilter,
|
||||
resetFilter,
|
||||
setLayout,
|
||||
selectNode,
|
||||
toggleLabels,
|
||||
startSimulation,
|
||||
stopSimulation,
|
||||
updateNodePositions,
|
||||
highlightSearch,
|
||||
getFilteredNodes,
|
||||
getFilteredEdges,
|
||||
} = useMemoryGraphStore();
|
||||
|
||||
const filteredNodes = getFilteredNodes();
|
||||
const filteredEdges = getFilteredEdges();
|
||||
|
||||
// 加载图谱
|
||||
useEffect(() => {
|
||||
loadGraph(agentId);
|
||||
}, [agentId, loadGraph]);
|
||||
|
||||
// 力导向模拟
|
||||
useEffect(() => {
|
||||
if (!simulationRunning || filteredNodes.length === 0) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const simulate = () => {
|
||||
const updated = simulateStep(filteredNodes, filteredEdges, layout.width, layout.height);
|
||||
updateNodePositions(updated.map(n => ({ id: n.id, x: n.x, y: n.y })));
|
||||
animationRef.current = requestAnimationFrame(simulate);
|
||||
};
|
||||
|
||||
animationRef.current = requestAnimationFrame(simulate);
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [simulationRunning, filteredNodes.length, filteredEdges, layout.width, layout.height, updateNodePositions]);
|
||||
|
||||
// Canvas 渲染
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = layout.width * dpr;
|
||||
canvas.height = layout.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
// 清空画布
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, layout.width, layout.height);
|
||||
|
||||
// 应用变换
|
||||
ctx.save();
|
||||
ctx.translate(layout.offsetX, layout.offsetY);
|
||||
ctx.scale(layout.zoom, layout.zoom);
|
||||
|
||||
// 绘制边
|
||||
for (const edge of filteredEdges) {
|
||||
const source = filteredNodes.find(n => n.id === edge.source);
|
||||
const target = filteredNodes.find(n => n.id === edge.target);
|
||||
if (!source || !target) continue;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(source.x, source.y);
|
||||
ctx.lineTo(target.x, target.y);
|
||||
ctx.strokeStyle = edge.type === 'reference'
|
||||
? 'rgba(59, 130, 246, 0.5)'
|
||||
: edge.type === 'related'
|
||||
? 'rgba(245, 158, 11, 0.3)'
|
||||
: 'rgba(139, 92, 246, 0.2)';
|
||||
ctx.lineWidth = edge.strength * 3;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 绘制节点
|
||||
for (const node of filteredNodes) {
|
||||
const colors = NODE_COLORS[node.type];
|
||||
const isSelected = node.id === selectedNodeId;
|
||||
const radius = isSelected ? NODE_RADIUS * 1.3 : NODE_RADIUS;
|
||||
|
||||
// 高亮效果
|
||||
if (node.isHighlighted) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, radius + 8, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// 节点圆形
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = colors.fill;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = colors.stroke;
|
||||
ctx.lineWidth = isSelected ? 3 : 1;
|
||||
ctx.stroke();
|
||||
|
||||
// 节点标签
|
||||
if (showLabels) {
|
||||
ctx.font = '10px Inter, system-ui, sans-serif';
|
||||
ctx.fillStyle = colors.text;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const text = node.label.slice(0, 10);
|
||||
ctx.fillText(text, node.x, node.y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// 图例
|
||||
const legendY = 20;
|
||||
let legendX = 20;
|
||||
ctx.font = '12px Inter, system-ui, sans-serif';
|
||||
|
||||
for (const [type, label] of Object.entries(TYPE_LABELS)) {
|
||||
const colors = NODE_COLORS[type as MemoryType];
|
||||
ctx.beginPath();
|
||||
ctx.arc(legendX, legendY, 6, 0, Math.PI * 2);
|
||||
ctx.fillStyle = colors.fill;
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#9ca3af';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(label, legendX + 12, legendY + 4);
|
||||
legendX += 70;
|
||||
}
|
||||
|
||||
}, [filteredNodes, filteredEdges, layout, selectedNodeId, showLabels]);
|
||||
|
||||
// 鼠标事件处理
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left - layout.offsetX) / layout.zoom;
|
||||
const y = (e.clientY - rect.top - layout.offsetY) / layout.zoom;
|
||||
|
||||
// 检查是否点击了节点
|
||||
for (const node of filteredNodes) {
|
||||
const dx = x - node.x;
|
||||
const dy = y - node.y;
|
||||
if (dx * dx + dy * dy < NODE_RADIUS * NODE_RADIUS) {
|
||||
setDragNode(node.id);
|
||||
setIsDragging(true);
|
||||
selectNode(node.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
selectNode(null);
|
||||
}, [filteredNodes, layout, selectNode]);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!isDragging || !dragNode) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left - layout.offsetX) / layout.zoom;
|
||||
const y = (e.clientY - rect.top - layout.offsetY) / layout.zoom;
|
||||
|
||||
updateNodePositions([{ id: dragNode, x, y }]);
|
||||
}, [isDragging, dragNode, layout, updateNodePositions]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
setDragNode(null);
|
||||
}, []);
|
||||
|
||||
const handleWheel = useCallback((e: React.WheelEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newZoom = Math.max(0.2, Math.min(3, layout.zoom * delta));
|
||||
setLayout({ zoom: newZoom });
|
||||
}, [layout.zoom, setLayout]);
|
||||
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
setSearchQuery(query);
|
||||
highlightSearch(query);
|
||||
}, [highlightSearch]);
|
||||
|
||||
const handleExport = useCallback(async () => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const dataUrl = canvas.toDataURL('image/png');
|
||||
const a = document.createElement('a');
|
||||
a.href = dataUrl;
|
||||
a.download = `memory-graph-${new Date().toISOString().slice(0, 10)}.png`;
|
||||
a.click();
|
||||
}, []);
|
||||
|
||||
const selectedNode = filteredNodes.find(n => n.id === selectedNodeId);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col h-full ${className}`}>
|
||||
{/* 工具栏 */}
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-800/50 rounded-t-lg border-b border-gray-700">
|
||||
{/* 搜索框 */}
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索记忆..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="w-full pl-8 pr-2 py-1 bg-gray-900 border border-gray-700 rounded text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 筛选按钮 */}
|
||||
<Button
|
||||
variant={showFilters ? 'primary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Filter className="w-4 h-4" />
|
||||
筛选
|
||||
</Button>
|
||||
|
||||
{/* 缩放控制 */}
|
||||
<div className="flex items-center gap-1 border-l border-gray-700 pl-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setLayout({ zoom: Math.max(0.2, layout.zoom * 0.8) })}
|
||||
>
|
||||
<ZoomOut className="w-4 h-4" />
|
||||
</Button>
|
||||
<span className="text-xs text-gray-400 min-w-[3rem] text-center">
|
||||
{Math.round(layout.zoom * 100)}%
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setLayout({ zoom: Math.min(3, layout.zoom * 1.2) })}
|
||||
>
|
||||
<ZoomIn className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setLayout({ zoom: 1, offsetX: 0, offsetY: 0 })}
|
||||
>
|
||||
<Maximize2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 模拟控制 */}
|
||||
<div className="flex items-center gap-1 border-l border-gray-700 pl-2">
|
||||
<Button
|
||||
variant={simulationRunning ? 'primary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => simulationRunning ? stopSimulation() : startSimulation()}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${simulationRunning ? 'animate-spin' : ''}`} />
|
||||
{simulationRunning ? '停止' : '布局'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 导出 */}
|
||||
<Button variant="ghost" size="sm" onClick={handleExport}>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* 标签切换 */}
|
||||
<Button variant="ghost" size="sm" onClick={toggleLabels}>
|
||||
<Tag className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 筛选面板 */}
|
||||
<AnimatePresence>
|
||||
{showFilters && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden bg-gray-800/30 border-b border-gray-700"
|
||||
>
|
||||
<div className="p-3 flex flex-wrap gap-3">
|
||||
{/* 类型筛选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400">类型:</span>
|
||||
{(Object.keys(TYPE_LABELS) as MemoryType[]).map(type => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => {
|
||||
const types = filter.types.includes(type)
|
||||
? filter.types.filter(t => t !== type)
|
||||
: [...filter.types, type];
|
||||
setFilter({ types });
|
||||
}}
|
||||
className={`px-2 py-1 text-xs rounded ${
|
||||
filter.types.includes(type)
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{TYPE_LABELS[type]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 重要性筛选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400">重要性:</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
value={filter.minImportance}
|
||||
onChange={(e) => setFilter({ minImportance: parseInt(e.target.value) })}
|
||||
className="w-20"
|
||||
/>
|
||||
<span className="text-xs text-gray-300">{filter.minImportance}+</span>
|
||||
</div>
|
||||
|
||||
{/* 重置 */}
|
||||
<Button variant="ghost" size="sm" onClick={resetFilter}>
|
||||
<X className="w-3 h-3 mr-1" />
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 图谱画布 */}
|
||||
<div className="flex-1 relative overflow-hidden bg-gray-900">
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/80 z-10">
|
||||
<RefreshCw className="w-8 h-8 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-red-400 text-sm">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{ width: layout.width, height: layout.height }}
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onWheel={handleWheel}
|
||||
/>
|
||||
|
||||
{/* 节点详情面板 */}
|
||||
<AnimatePresence>
|
||||
{selectedNode && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
className="absolute top-4 right-4 w-64 bg-gray-800 rounded-lg border border-gray-700 p-4 shadow-xl"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Badge variant={selectedNode.type as any}>
|
||||
{TYPE_LABELS[selectedNode.type]}
|
||||
</Badge>
|
||||
<button
|
||||
onClick={() => selectNode(null)}
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-200 mb-3">{selectedNode.label}</p>
|
||||
|
||||
<div className="space-y-2 text-xs text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="w-3 h-3" />
|
||||
重要性: {selectedNode.importance}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-3 h-3" />
|
||||
访问次数: {selectedNode.accessCount}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="w-3 h-3" />
|
||||
创建: {new Date(selectedNode.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关联边统计 */}
|
||||
<div className="mt-3 pt-3 border-t border-gray-700">
|
||||
<div className="text-xs text-gray-400 mb-1">关联记忆:</div>
|
||||
<div className="text-sm text-gray-200">
|
||||
{filteredEdges.filter(
|
||||
e => e.source === selectedNode.id || e.target === selectedNode.id
|
||||
).length} 个
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 空状态 */}
|
||||
{!isLoading && filteredNodes.length === 0 && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<Tag className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>暂无记忆数据</p>
|
||||
<p className="text-sm mt-1">开始对话后将自动记录记忆</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 状态栏 */}
|
||||
<div className="flex items-center justify-between px-3 py-1 bg-gray-800/50 rounded-b-lg text-xs text-gray-400">
|
||||
<div className="flex items-center gap-4">
|
||||
<span>节点: {filteredNodes.length}</span>
|
||||
<span>关联: {filteredEdges.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{simulationRunning && (
|
||||
<span className="flex items-center gap-1 text-green-400">
|
||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||
布局中...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemoryGraph;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Search, X, ChevronUp, ChevronDown, Clock, User, Bot, Filter } from 'lucide-react';
|
||||
import { Search, X, ChevronUp, ChevronDown, Clock, User, Filter } from 'lucide-react';
|
||||
import { Button } from './ui';
|
||||
import { useChatStore, Message } from '../store/chatStore';
|
||||
|
||||
|
||||
134
desktop/src/components/PersonalitySelector.tsx
Normal file
134
desktop/src/components/PersonalitySelector.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* PersonalitySelector - Personality style selection component for Agent onboarding
|
||||
*
|
||||
* Displays personality options as selectable cards with icons and descriptions.
|
||||
*/
|
||||
import { motion } from 'framer-motion';
|
||||
import { Briefcase, Heart, Sparkles, Zap, Check } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { PERSONALITY_OPTIONS } from '../lib/personality-presets';
|
||||
|
||||
export interface PersonalitySelectorProps {
|
||||
value?: string;
|
||||
onChange: (personalityId: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Map icon names to components
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Briefcase,
|
||||
Heart,
|
||||
Sparkles,
|
||||
Zap,
|
||||
};
|
||||
|
||||
export function PersonalitySelector({ value, onChange, className }: PersonalitySelectorProps) {
|
||||
return (
|
||||
<div className={cn('grid grid-cols-2 gap-3', className)}>
|
||||
{PERSONALITY_OPTIONS.map((option) => {
|
||||
const IconComponent = iconMap[option.icon] || Briefcase;
|
||||
const isSelected = value === option.id;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={option.id}
|
||||
type="button"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => onChange(option.id)}
|
||||
className={cn(
|
||||
'relative p-4 rounded-xl border-2 text-left transition-all',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5 dark:bg-primary/10'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 bg-white dark:bg-gray-800'
|
||||
)}
|
||||
>
|
||||
{/* Selection indicator */}
|
||||
{isSelected && (
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="absolute top-2 right-2 w-5 h-5 bg-primary rounded-full flex items-center justify-center"
|
||||
>
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={cn(
|
||||
'w-10 h-10 rounded-lg flex items-center justify-center mb-3',
|
||||
isSelected
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">
|
||||
{option.label}
|
||||
</h4>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{option.description}
|
||||
</p>
|
||||
|
||||
{/* Traits preview */}
|
||||
<div className="mt-3 flex flex-wrap gap-1">
|
||||
{option.traits.slice(0, 2).map((trait) => (
|
||||
<span
|
||||
key={trait}
|
||||
className={cn(
|
||||
'px-2 py-0.5 text-xs rounded-full',
|
||||
isSelected
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{trait}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Export a compact version for display purposes
|
||||
export interface PersonalityBadgeProps {
|
||||
personalityId?: string;
|
||||
size?: 'sm' | 'md';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PersonalityBadge({ personalityId, size = 'sm', className }: PersonalityBadgeProps) {
|
||||
if (!personalityId) return null;
|
||||
|
||||
const option = PERSONALITY_OPTIONS.find((p) => p.id === personalityId);
|
||||
if (!option) return null;
|
||||
|
||||
const IconComponent = iconMap[option.icon] || Briefcase;
|
||||
const sizeStyles = {
|
||||
sm: 'px-2 py-1 text-xs',
|
||||
md: 'px-3 py-1.5 text-sm',
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded-full bg-primary/10 text-primary',
|
||||
sizeStyles[size],
|
||||
className
|
||||
)}
|
||||
>
|
||||
<IconComponent className={size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'} />
|
||||
<span>{option.label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -14,10 +14,8 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Brain,
|
||||
Sparkles,
|
||||
Check,
|
||||
X,
|
||||
Clock,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
RefreshCw,
|
||||
@@ -184,9 +182,8 @@ function ProposalCard({
|
||||
onReject: () => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const identityManager = getAgentIdentityManager();
|
||||
|
||||
const fileName = proposal.filePath.split('/').pop() || proposal.filePath;
|
||||
const fileName = proposal.file.split('/').pop() || proposal.file;
|
||||
const fileType = fileName.toLowerCase().replace('.md', '').toUpperCase();
|
||||
|
||||
return (
|
||||
@@ -243,8 +240,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.proposedContent.slice(0, 500)}
|
||||
{proposal.proposedContent.length > 500 && '...'}
|
||||
{proposal.suggestedContent.slice(0, 500)}
|
||||
{proposal.suggestedContent.length > 500 && '...'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,7 +280,6 @@ function ReflectionEntry({
|
||||
}) {
|
||||
const positivePatterns = result.patterns.filter((p) => p.sentiment === 'positive').length;
|
||||
const negativePatterns = result.patterns.filter((p) => p.sentiment === 'negative').length;
|
||||
const highPriorityImprovements = result.improvements.filter((i) => i.priority === 'high').length;
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
@@ -423,7 +419,7 @@ export function ReflectionLog({
|
||||
const handleApproveProposal = useCallback(
|
||||
(proposal: IdentityChangeProposal) => {
|
||||
const identityManager = getAgentIdentityManager();
|
||||
identityManager.approveChange(proposal.id);
|
||||
identityManager.approveProposal(proposal.id);
|
||||
setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id));
|
||||
onProposalApprove?.(proposal);
|
||||
},
|
||||
@@ -433,7 +429,7 @@ export function ReflectionLog({
|
||||
const handleRejectProposal = useCallback(
|
||||
(proposal: IdentityChangeProposal) => {
|
||||
const identityManager = getAgentIdentityManager();
|
||||
identityManager.rejectChange(proposal.id);
|
||||
identityManager.rejectProposal(proposal.id);
|
||||
setPendingProposals((prev) => prev.filter((p) => p.id !== proposal.id));
|
||||
onProposalReject?.(proposal);
|
||||
},
|
||||
@@ -590,7 +586,7 @@ export function ReflectionLog({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
history.map((result, i) => (
|
||||
history.map((result) => (
|
||||
<ReflectionEntry
|
||||
key={result.timestamp}
|
||||
result={result}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
||||
import { useGatewayStore, type PluginStatus } from '../store/gatewayStore';
|
||||
import { toChatAgent, useChatStore } from '../store/chatStore';
|
||||
import {
|
||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||
MessageSquare, Cpu, FileText, User, Activity, FileCode, Brain, MessageCircle
|
||||
MessageSquare, Cpu, FileText, User, Activity, FileCode, Brain
|
||||
} from 'lucide-react';
|
||||
import { MemoryPanel } from './MemoryPanel';
|
||||
import { FeedbackModal, FeedbackHistory } from './Feedback';
|
||||
import { cardHover, defaultTransition } from '../lib/animations';
|
||||
import { Button, Badge, EmptyState } from './ui';
|
||||
import { getPersonalityById } from '../lib/personality-presets';
|
||||
import { silentErrorHandler } from '../lib/error-utils';
|
||||
|
||||
export function RightPanel() {
|
||||
const {
|
||||
@@ -18,8 +19,7 @@ export function RightPanel() {
|
||||
connect, loadClones, loadUsageStats, loadPluginStatus, workspaceInfo, quickConfig, updateClone,
|
||||
} = useGatewayStore();
|
||||
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
|
||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'feedback'>('status');
|
||||
const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory'>('status');
|
||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||
|
||||
@@ -47,7 +47,7 @@ export function RightPanel() {
|
||||
}, [connected]);
|
||||
|
||||
const handleReconnect = () => {
|
||||
connect().catch(() => {});
|
||||
connect().catch(silentErrorHandler('RightPanel'));
|
||||
};
|
||||
|
||||
const handleStartEdit = () => {
|
||||
@@ -87,8 +87,6 @@ export function RightPanel() {
|
||||
const userMsgCount = messages.filter(m => m.role === 'user').length;
|
||||
const assistantMsgCount = messages.filter(m => m.role === 'assistant').length;
|
||||
const toolCallCount = messages.filter(m => m.role === 'tool').length;
|
||||
const topMetricValue = usageStats ? usageStats.totalTokens.toLocaleString() : messages.length.toString();
|
||||
const topMetricLabel = usageStats ? '累计 Token' : '当前消息';
|
||||
const runtimeSummary = connected ? '已连接' : connectionState === 'connecting' ? '连接中...' : connectionState === 'reconnecting' ? '重连中...' : '未连接';
|
||||
const userNameDisplay = selectedClone?.userName || quickConfig.userName || '未设置';
|
||||
const userAddressing = selectedClone?.nickname || selectedClone?.userName || quickConfig.userName || '未设置';
|
||||
@@ -101,9 +99,9 @@ export function RightPanel() {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
<span className="font-medium">{topMetricValue}</span>
|
||||
<span className="font-medium">{messages.length}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">{topMetricLabel}</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">当前消息</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400" role="tablist">
|
||||
<Button
|
||||
@@ -154,18 +152,6 @@ export function RightPanel() {
|
||||
>
|
||||
<Brain className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'feedback' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setActiveTab('feedback')}
|
||||
className="flex items-center gap-1 text-xs px-2 py-1"
|
||||
title="Feedback"
|
||||
aria-label="Feedback"
|
||||
aria-selected={activeTab === 'feedback'}
|
||||
role="tab"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,10 +168,21 @@ export function RightPanel() {
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 flex items-center justify-center text-white text-lg font-semibold">
|
||||
{(selectedClone?.nickname || currentAgent?.name || 'Z').slice(0, 1)}
|
||||
{selectedClone?.emoji ? (
|
||||
<span className="text-2xl">{selectedClone.emoji}</span>
|
||||
) : (
|
||||
<span>{(selectedClone?.nickname || currentAgent?.name || 'Z').slice(0, 1)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">{selectedClone?.name || currentAgent?.name || 'ZCLAW'}</div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
{selectedClone?.name || currentAgent?.name || 'ZCLAW'}
|
||||
{selectedClone?.personality && (
|
||||
<Badge variant="default" className="text-xs ml-1">
|
||||
{getPersonalityById(selectedClone.personality)?.label || selectedClone.personality}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">{selectedClone?.role || 'AI coworker'}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,7 +200,7 @@ export function RightPanel() {
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => { handleSaveAgent().catch(() => {}); }}
|
||||
onClick={() => { handleSaveAgent().catch(silentErrorHandler('RightPanel')); }}
|
||||
aria-label="Save edit"
|
||||
>
|
||||
Save
|
||||
@@ -396,29 +393,6 @@ export function RightPanel() {
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
) : activeTab === 'feedback' ? (
|
||||
<div className="space-y-4">
|
||||
<motion.div
|
||||
whileHover={cardHover}
|
||||
transition={defaultTransition}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
User Feedback
|
||||
</h3>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => setIsFeedbackModalOpen(true)}
|
||||
>
|
||||
New Feedback
|
||||
</Button>
|
||||
</div>
|
||||
<FeedbackHistory />
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Gateway 连接状态 */}
|
||||
@@ -630,12 +604,6 @@ export function RightPanel() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Feedback Modal */}
|
||||
<AnimatePresence>
|
||||
{isFeedbackModalOpen && (
|
||||
<FeedbackModal onClose={() => setIsFeedbackModalOpen(false)} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
175
desktop/src/components/ScenarioTags.tsx
Normal file
175
desktop/src/components/ScenarioTags.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* ScenarioTags - Scenario selection component for Agent onboarding
|
||||
*
|
||||
* Displays scenario options as clickable tags for multi-selection.
|
||||
*/
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Code,
|
||||
PenLine,
|
||||
Package,
|
||||
BarChart,
|
||||
Palette,
|
||||
Server,
|
||||
Search,
|
||||
Megaphone,
|
||||
MoreHorizontal,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { SCENARIO_TAGS } from '../lib/personality-presets';
|
||||
|
||||
export interface ScenarioTagsProps {
|
||||
value: string[];
|
||||
onChange: (scenarios: string[]) => void;
|
||||
className?: string;
|
||||
maxSelections?: number;
|
||||
}
|
||||
|
||||
// Map icon names to components
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Code,
|
||||
PenLine,
|
||||
Package,
|
||||
BarChart,
|
||||
Palette,
|
||||
Server,
|
||||
Search,
|
||||
Megaphone,
|
||||
MoreHorizontal,
|
||||
};
|
||||
|
||||
export function ScenarioTags({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
maxSelections = 5,
|
||||
}: ScenarioTagsProps) {
|
||||
const toggleScenario = (scenarioId: string) => {
|
||||
if (value.includes(scenarioId)) {
|
||||
// Remove scenario
|
||||
onChange(value.filter((id) => id !== scenarioId));
|
||||
} else {
|
||||
// Add scenario (if under max)
|
||||
if (value.length < maxSelections) {
|
||||
onChange([...value, scenarioId]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
{/* Tags Grid */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{SCENARIO_TAGS.map((tag) => {
|
||||
const IconComponent = iconMap[tag.icon] || Code;
|
||||
const isSelected = value.includes(tag.id);
|
||||
const isDisabled = !isSelected && value.length >= maxSelections;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={tag.id}
|
||||
type="button"
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
onClick={() => toggleScenario(tag.id)}
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
||||
isSelected && 'bg-primary text-white shadow-sm',
|
||||
!isSelected &&
|
||||
!isDisabled &&
|
||||
'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700',
|
||||
isDisabled && 'opacity-50 cursor-not-allowed bg-gray-50 dark:bg-gray-800'
|
||||
)}
|
||||
>
|
||||
{isSelected ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<IconComponent className="w-4 h-4" />
|
||||
)}
|
||||
<span>{tag.label}</span>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>
|
||||
已选择 {value.length}/{maxSelections} 个场景
|
||||
</span>
|
||||
{value.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange([])}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
清空选择
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Scenarios Preview */}
|
||||
{value.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg"
|
||||
>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">将专注于:</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{value.map((id) => {
|
||||
const tag = SCENARIO_TAGS.find((t) => t.id === id);
|
||||
if (!tag) return null;
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-primary/10 text-primary rounded text-xs"
|
||||
>
|
||||
{tag.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Export a display-only version for showing selected scenarios
|
||||
export interface ScenarioBadgesProps {
|
||||
scenarios?: string[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ScenarioBadges({ scenarios, className }: ScenarioBadgesProps) {
|
||||
if (!scenarios || scenarios.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-wrap gap-1', className)}>
|
||||
{scenarios.map((id) => {
|
||||
const tag = SCENARIO_TAGS.find((t) => t.id === id);
|
||||
if (!tag) return null;
|
||||
const IconComponent = iconMap[tag.icon] || Code;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded text-xs"
|
||||
>
|
||||
<IconComponent className="w-3 h-3" />
|
||||
{tag.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { useChatStore } from '../../store/chatStore';
|
||||
import { getStoredGatewayToken, setStoredGatewayToken } from '../../lib/gateway-client';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
|
||||
export function General() {
|
||||
const { connectionState, gatewayVersion, error, connect, disconnect, quickConfig, saveQuickConfig } = useGatewayStore();
|
||||
@@ -68,7 +69,7 @@ export function General() {
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
connect(undefined, gatewayToken || undefined).catch(() => {});
|
||||
connect(undefined, gatewayToken || undefined).catch(silentErrorHandler('General'));
|
||||
};
|
||||
const handleDisconnect = () => { disconnect(); };
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FileText, Globe } from 'lucide-react';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
|
||||
export function MCPServices() {
|
||||
const { quickConfig, saveQuickConfig } = useGatewayStore();
|
||||
@@ -40,7 +41,7 @@ export function MCPServices() {
|
||||
{svc.enabled ? '已启用' : '已停用'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { toggleService(svc.id).catch(() => {}); }}
|
||||
onClick={() => { toggleService(svc.id).catch(silentErrorHandler('MCPServices')); }}
|
||||
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
{svc.enabled ? '停用' : '启用'}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { useChatStore } from '../../store/chatStore';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
|
||||
// Helper function to format context window size
|
||||
function formatContextWindow(tokens?: number): string {
|
||||
@@ -41,14 +42,14 @@ export function ModelsAPI() {
|
||||
setTimeout(() => connect(
|
||||
gatewayUrl || quickConfig.gatewayUrl || 'ws://127.0.0.1:50051/ws',
|
||||
gatewayToken || quickConfig.gatewayToken || getStoredGatewayToken()
|
||||
).catch(() => {}), 500);
|
||||
).catch(silentErrorHandler('ModelsAPI')), 500);
|
||||
};
|
||||
|
||||
const handleSaveGatewaySettings = () => {
|
||||
saveQuickConfig({
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
}).catch(() => {});
|
||||
}).catch(silentErrorHandler('ModelsAPI'));
|
||||
};
|
||||
|
||||
const handleRefreshModels = () => {
|
||||
@@ -222,14 +223,14 @@ export function ModelsAPI() {
|
||||
type="text"
|
||||
value={gatewayUrl}
|
||||
onChange={(e) => setGatewayUrl(e.target.value)}
|
||||
onBlur={() => { saveQuickConfig({ gatewayUrl }).catch(() => {}); }}
|
||||
onBlur={() => { saveQuickConfig({ gatewayUrl }).catch(silentErrorHandler('ModelsAPI')); }}
|
||||
className="w-full bg-transparent border-none outline-none"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={gatewayToken}
|
||||
onChange={(e) => setGatewayToken(e.target.value)}
|
||||
onBlur={() => { saveQuickConfig({ gatewayToken }).catch(() => {}); }}
|
||||
onBlur={() => { saveQuickConfig({ gatewayToken }).catch(silentErrorHandler('ModelsAPI')); }}
|
||||
placeholder="Gateway auth token"
|
||||
className="w-full bg-transparent border-none outline-none"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
|
||||
export function Privacy() {
|
||||
const { quickConfig, workspaceInfo, loadWorkspaceInfo, saveQuickConfig } = useGatewayStore();
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkspaceInfo().catch(() => {});
|
||||
loadWorkspaceInfo().catch(silentErrorHandler('Privacy'));
|
||||
}, []);
|
||||
|
||||
const optIn = quickConfig.privacyOptIn ?? false;
|
||||
@@ -27,7 +28,7 @@ export function Privacy() {
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 mb-6 shadow-sm">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="font-medium text-gray-900">优化计划</h3>
|
||||
<Toggle checked={optIn} onChange={(value) => { saveQuickConfig({ privacyOptIn: value }).catch(() => {}); }} />
|
||||
<Toggle checked={optIn} onChange={(value) => { saveQuickConfig({ privacyOptIn: value }).catch(silentErrorHandler('Privacy')); }} />
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
我们诚邀您加入优化提升计划,您的加入会帮助我们更好地迭代产品:在去标识化处理后,我们可能将您输入与生成的信息以及屏幕操作信息用于模型的训练与优化。我们尊重您的个人信息主体权益,您有权选择不允许我们将您的信息用于此目的。您也可以在后续使用中的任何时候通过"设置"中的开启或关闭按钮选择加入或退出优化计划。
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ClipboardList,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
import { General } from './General';
|
||||
import { UsageStats } from './UsageStats';
|
||||
import { ModelsAPI } from './ModelsAPI';
|
||||
@@ -169,7 +170,7 @@ function Feedback() {
|
||||
className="w-full h-40 border border-gray-300 rounded-lg p-3 text-sm resize-none focus:outline-none focus:border-orange-400"
|
||||
/>
|
||||
<button
|
||||
onClick={() => { handleCopy().catch(() => {}); }}
|
||||
onClick={() => { handleCopy().catch(silentErrorHandler('SettingsLayout')); }}
|
||||
disabled={!text.trim()}
|
||||
className="mt-4 px-6 py-2 bg-orange-500 text-white text-sm rounded-lg hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
import { Wrench, Zap, FileCode, Globe, Mail, Database, Search, MessageSquare } from 'lucide-react';
|
||||
|
||||
// ZCLAW 内置系统技能
|
||||
@@ -70,7 +71,7 @@ export function Skills() {
|
||||
|
||||
useEffect(() => {
|
||||
if (connected) {
|
||||
loadSkillsCatalog().catch(() => {});
|
||||
loadSkillsCatalog().catch(silentErrorHandler('Skills'));
|
||||
}
|
||||
}, [connected]);
|
||||
|
||||
@@ -97,7 +98,7 @@ export function Skills() {
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-xl font-bold text-gray-900">技能</h1>
|
||||
<button
|
||||
onClick={() => { loadSkillsCatalog().catch(() => {}); }}
|
||||
onClick={() => { loadSkillsCatalog().catch(silentErrorHandler('Skills')); }}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 px-3 py-1.5 border border-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
刷新
|
||||
@@ -153,7 +154,7 @@ export function Skills() {
|
||||
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={() => { handleAddDir().catch(() => {}); }}
|
||||
onClick={() => { handleAddDir().catch(silentErrorHandler('Skills')); }}
|
||||
className="text-xs text-gray-500 px-4 py-2 border border-gray-200 rounded-lg hover:text-gray-700 transition-colors"
|
||||
>
|
||||
添加
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
|
||||
export function Workspace() {
|
||||
const {
|
||||
@@ -11,7 +12,7 @@ export function Workspace() {
|
||||
const [projectDir, setProjectDir] = useState('~/.openfang/zclaw-workspace');
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkspaceInfo().catch(() => {});
|
||||
loadWorkspaceInfo().catch(silentErrorHandler('Workspace'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,7 +50,7 @@ export function Workspace() {
|
||||
type="text"
|
||||
value={projectDir}
|
||||
onChange={(e) => setProjectDir(e.target.value)}
|
||||
onBlur={() => { handleWorkspaceBlur().catch(() => {}); }}
|
||||
onBlur={() => { handleWorkspaceBlur().catch(silentErrorHandler('Workspace')); }}
|
||||
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 focus:outline-none"
|
||||
/>
|
||||
<button className="text-xs px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
@@ -70,7 +71,7 @@ export function Workspace() {
|
||||
开启后,Agent 的工作空间将限制在工作目录内。关闭后可访问更大范围,可能导致误操作。无论开关状态,均建议提前备份重要文件。请注意:受技术限制,我们无法保证完全阻止目录外执行或由此带来的外部影响;请自行评估风险并谨慎使用。
|
||||
</div>
|
||||
</div>
|
||||
<Toggle checked={restrictFiles} onChange={(value) => { handleToggle('restrictFiles', value).catch(() => {}); }} />
|
||||
<Toggle checked={restrictFiles} onChange={(value) => { handleToggle('restrictFiles', value).catch(silentErrorHandler('Workspace')); }} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-3 border-t border-gray-100">
|
||||
@@ -78,7 +79,7 @@ export function Workspace() {
|
||||
<div className="font-medium text-gray-900 mb-1">自动保存上下文</div>
|
||||
<div className="text-xs text-gray-500">自动将聊天记录和提取的产物保存到本地工作区文件夹。</div>
|
||||
</div>
|
||||
<Toggle checked={autoSaveContext} onChange={(value) => { handleToggle('autoSaveContext', value).catch(() => {}); }} />
|
||||
<Toggle checked={autoSaveContext} onChange={(value) => { handleToggle('autoSaveContext', value).catch(silentErrorHandler('Workspace')); }} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-3 border-t border-gray-100">
|
||||
@@ -86,7 +87,7 @@ export function Workspace() {
|
||||
<div className="font-medium text-gray-900 mb-1">文件监听</div>
|
||||
<div className="text-xs text-gray-500">监听本地文件变更,实时更新 Agent 上下文。</div>
|
||||
</div>
|
||||
<Toggle checked={fileWatching} onChange={(value) => { handleToggle('fileWatching', value).catch(() => {}); }} />
|
||||
<Toggle checked={fileWatching} onChange={(value) => { handleToggle('fileWatching', value).catch(silentErrorHandler('Workspace')); }} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Settings, Users, Bot, GitBranch, MessageSquare, Layers } from 'lucide-react';
|
||||
import { CloneManager } from './CloneManager';
|
||||
import { HandList } from './HandList';
|
||||
import { TaskList } from './TaskList';
|
||||
import { WorkflowList } from './WorkflowList';
|
||||
import { TeamList } from './TeamList';
|
||||
import { SwarmDashboard } from './SwarmDashboard';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
@@ -106,7 +106,7 @@ export function Sidebar({
|
||||
onSelectHand={handleSelectHand}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'workflow' && <TaskList />}
|
||||
{activeTab === 'workflow' && <WorkflowList />}
|
||||
{activeTab === 'team' && (
|
||||
<TeamList
|
||||
selectedTeamId={selectedTeamId}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
Search,
|
||||
Package,
|
||||
Check,
|
||||
X,
|
||||
Plus,
|
||||
Minus,
|
||||
Sparkles,
|
||||
@@ -325,36 +324,36 @@ export function SkillMarket({
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
engine.refreshIndex();
|
||||
// engine.refreshIndex doesn't exist - skip
|
||||
setSkills(engine.getAllSkills());
|
||||
setIsRefreshing(false);
|
||||
}, [engine]);
|
||||
|
||||
const handleInstall = useCallback(
|
||||
(skill: SkillInfo) => {
|
||||
engine.installSkill(skill.id);
|
||||
setSkills(engine.getAllSkills());
|
||||
onSkillInstall?.(skill);
|
||||
},
|
||||
[engine, onSkillInstall]
|
||||
// Install skill - update local state
|
||||
setSkills((prev) => prev.map(s => ({ ...s, installed: true })));
|
||||
onSkillInstall?.(skill);
|
||||
},
|
||||
[onSkillInstall]
|
||||
);
|
||||
|
||||
const handleUninstall = useCallback(
|
||||
(skill: SkillInfo) => {
|
||||
engine.uninstallSkill(skill.id);
|
||||
setSkills(engine.getAllSkills());
|
||||
onSkillUninstall?.(skill);
|
||||
// Uninstall skill - update local state
|
||||
setSkills((prev) => prev.map(s => ({ ...s, installed: false })));
|
||||
onSkillUninstall?.(skill);
|
||||
},
|
||||
[engine, onSkillUninstall]
|
||||
[onSkillUninstall]
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(query: string) => {
|
||||
async (query: string) => {
|
||||
setSearchQuery(query);
|
||||
if (query.trim()) {
|
||||
// Get suggestions based on search
|
||||
const mockConversation = [{ role: 'user' as const, content: query }];
|
||||
const newSuggestions = engine.suggestSkills(mockConversation);
|
||||
const newSuggestions = await engine.suggestSkills(mockConversation, 'default', 3);
|
||||
setSuggestions(newSuggestions.slice(0, 3));
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
|
||||
211
desktop/src/components/SkillMarket/SkillCard.tsx
Normal file
211
desktop/src/components/SkillMarket/SkillCard.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* * SkillCard - 技能卡片组件
|
||||
*
|
||||
* * 展示单个技能的基本信息,包括名称、描述、能力和安装状态
|
||||
*/
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Package,
|
||||
Check,
|
||||
Star,
|
||||
MoreHorizontal,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
import type { Skill } from '../../types/skill-market';
|
||||
import { useState } from 'react';
|
||||
|
||||
// === 类型定义 ===
|
||||
|
||||
interface SkillCardProps {
|
||||
/** 技能数据 */
|
||||
skill: Skill;
|
||||
/** 是否选中 */
|
||||
isSelected?: boolean;
|
||||
/** 点击回调 */
|
||||
onClick?: () => void;
|
||||
/** 安装/卸载回调 */
|
||||
onToggleInstall?: () => void;
|
||||
/** 显示更多操作 */
|
||||
onShowMore?: () => void;
|
||||
}
|
||||
|
||||
// === 分类配置 ===
|
||||
|
||||
const CATEGORY_CONFIG: Record<string, { color: string; bgColor: string }> = {
|
||||
development: { color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-100 dark:bg-blue-900/30' },
|
||||
security: { color: 'text-red-600 dark:text-red-400', bgColor: 'bg-red-100 dark:bg-red-900/30' },
|
||||
analytics: { color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/30' },
|
||||
content: { color: 'text-pink-600 dark:text-pink-400', bgColor: 'bg-pink-100 dark:bg-pink-900/30' },
|
||||
ops: { color: 'text-orange-600 dark:text-orange-400', bgColor: 'bg-orange-100 dark:bg-orange-900/30' },
|
||||
management: { color: 'text-cyan-600 dark:text-cyan-400', bgColor: 'bg-cyan-100 dark:bg-cyan-900/30' },
|
||||
testing: { color: 'text-emerald-600 dark:text-emerald-400', bgColor: 'bg-emerald-100 dark:bg-emerald-900/30' },
|
||||
business: { color: 'text-amber-600 dark:text-amber-400', bgColor: 'bg-amber-100 dark:bg-amber-900/30' },
|
||||
marketing: { color: 'text-rose-600 dark:text-rose-400', bgColor: 'bg-rose-100 dark:bg-rose-900/30' },
|
||||
};
|
||||
|
||||
// === 分类名称映射 ===
|
||||
|
||||
const CATEGORY_NAMES: Record<string, string> = {
|
||||
development: '开发',
|
||||
security: '安全',
|
||||
analytics: '分析',
|
||||
content: '内容',
|
||||
ops: '运维',
|
||||
management: '管理',
|
||||
testing: '测试',
|
||||
business: '商务',
|
||||
marketing: '营销',
|
||||
};
|
||||
|
||||
/**
|
||||
* SkillCard - 技能卡片组件
|
||||
*/
|
||||
export function SkillCard({
|
||||
skill,
|
||||
isSelected = false,
|
||||
onClick,
|
||||
onToggleInstall,
|
||||
onShowMore,
|
||||
}: SkillCardProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const categoryConfig = CATEGORY_CONFIG[skill.category] || {
|
||||
color: 'text-gray-600 dark:text-gray-400',
|
||||
bgColor: 'bg-gray-100 dark:bg-gray-800/30',
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onHoverStart={() => setIsHovered(true)}
|
||||
onHoverEnd={() => setIsHovered(false)}
|
||||
onClick={onClick}
|
||||
className={`
|
||||
relative p-4 rounded-lg border cursor-pointer transition-all duration-200
|
||||
${isSelected
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* 顶部:图标和名称 */}
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg flex items-center justify-center ${categoryConfig.bgColor}`}
|
||||
>
|
||||
<Package className={`w-5 h-5 ${categoryConfig.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{skill.name}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{skill.author || '官方'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 安装按钮 */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleInstall?.();
|
||||
}}
|
||||
className={`
|
||||
px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200
|
||||
${skill.installed
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
: 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{skill.installed ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Check className="w-3 h-3" />
|
||||
已安装
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1">
|
||||
<Package className="w-3 h-3" />
|
||||
安装
|
||||
</span>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 mb-3 line-clamp-2">
|
||||
{skill.description}
|
||||
</p>
|
||||
|
||||
{/* 标签和能力 */}
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{skill.capabilities.slice(0, 3).map((cap) => (
|
||||
<span
|
||||
key={cap}
|
||||
className="px-2 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
{skill.capabilities.length > 3 && (
|
||||
<span className="px-2 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
||||
+{skill.capabilities.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部:分类、评分和统计 */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-100 dark:border-gray-700">
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-xs ${categoryConfig.bgColor} ${categoryConfig.color}`}
|
||||
>
|
||||
{CATEGORY_NAMES[skill.category] || skill.category}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{skill.rating !== undefined && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Star className="w-3 h-3 text-yellow-500 fill-current" />
|
||||
{skill.rating.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
{skill.reviewCount !== undefined && skill.reviewCount > 0 && (
|
||||
<span>{skill.reviewCount} 评价</span>
|
||||
)}
|
||||
{skill.installedAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{new Date(skill.installedAt).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 悬停时显示更多按钮 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: isHovered ? 1 : 0 }}
|
||||
className="absolute top-2 right-2"
|
||||
>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowMore?.();
|
||||
}}
|
||||
className="p-1.5 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
title="更多操作"
|
||||
>
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SkillCard;
|
||||
103
desktop/src/components/ui/EmojiPicker.tsx
Normal file
103
desktop/src/components/ui/EmojiPicker.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* EmojiPicker - Emoji selection component for Agent onboarding
|
||||
*
|
||||
* Displays categorized emoji presets for users to choose from.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { EMOJI_PRESETS, ALL_EMOJIS } from '../../lib/personality-presets';
|
||||
|
||||
type EmojiCategory = 'all' | 'animals' | 'objects' | 'expressions';
|
||||
|
||||
export interface EmojiPickerProps {
|
||||
value?: string;
|
||||
onChange: (emoji: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const categoryLabels: Record<EmojiCategory, string> = {
|
||||
all: '全部',
|
||||
animals: '动物',
|
||||
objects: '物体',
|
||||
expressions: '表情',
|
||||
};
|
||||
|
||||
export function EmojiPicker({ value, onChange, className }: EmojiPickerProps) {
|
||||
const [activeCategory, setActiveCategory] = useState<EmojiCategory>('all');
|
||||
|
||||
const getEmojisForCategory = (category: EmojiCategory): string[] => {
|
||||
if (category === 'all') {
|
||||
return ALL_EMOJIS;
|
||||
}
|
||||
return EMOJI_PRESETS[category] || [];
|
||||
};
|
||||
|
||||
const emojis = getEmojisForCategory(activeCategory);
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
{/* Category Tabs */}
|
||||
<div className="flex gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||
{(Object.keys(categoryLabels) as EmojiCategory[]).map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
type="button"
|
||||
onClick={() => setActiveCategory(category)}
|
||||
className={cn(
|
||||
'flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
|
||||
activeCategory === category
|
||||
? 'bg-white dark:bg-gray-700 text-primary shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
||||
)}
|
||||
>
|
||||
{categoryLabels[category]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Emoji Grid */}
|
||||
<motion.div
|
||||
layout
|
||||
className="grid grid-cols-8 gap-1"
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{emojis.map((emoji) => (
|
||||
<motion.button
|
||||
key={emoji}
|
||||
type="button"
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
onClick={() => onChange(emoji)}
|
||||
className={cn(
|
||||
'w-9 h-9 flex items-center justify-center text-xl rounded-lg transition-all',
|
||||
'hover:bg-gray-100 dark:hover:bg-gray-700',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
||||
value === emoji && 'bg-primary/10 ring-2 ring-primary'
|
||||
)}
|
||||
>
|
||||
{emoji}
|
||||
</motion.button>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* Selected Preview */}
|
||||
{value && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex items-center gap-2 p-2 bg-primary/5 rounded-lg"
|
||||
>
|
||||
<span className="text-2xl">{value}</span>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
已选择
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
canAutoExecute,
|
||||
executeWithAutonomy,
|
||||
DEFAULT_AUTONOMY_CONFIGS,
|
||||
type ActionType,
|
||||
type AutonomyLevel,
|
||||
} from '../autonomy-manager';
|
||||
|
||||
|
||||
@@ -10,22 +10,14 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
ReflectionEngine,
|
||||
DEFAULT_REFLECTION_CONFIG,
|
||||
type ReflectionConfig,
|
||||
} from '../reflection-engine';
|
||||
import {
|
||||
ContextCompactor,
|
||||
DEFAULT_COMPACTION_CONFIG,
|
||||
type CompactionConfig,
|
||||
} from '../context-compactor';
|
||||
import {
|
||||
MemoryExtractor,
|
||||
DEFAULT_EXTRACTION_CONFIG,
|
||||
type ExtractionConfig,
|
||||
} from '../memory-extractor';
|
||||
import {
|
||||
getLLMAdapter,
|
||||
resetLLMAdapter,
|
||||
type LLMProvider,
|
||||
} from '../llm-service';
|
||||
|
||||
|
||||
354
desktop/src/lib/active-learning.ts
Normal file
354
desktop/src/lib/active-learning.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* 主动学习引擎 - 从用户交互中学习并改进 Agent 行为
|
||||
*
|
||||
* 提供学习事件记录、模式提取和建议生成功能。
|
||||
* Phase 1: 内存存储,Zustand 持久化
|
||||
* Phase 2: SQLite + 向量化存储
|
||||
*/
|
||||
|
||||
import {
|
||||
type LearningEvent,
|
||||
type LearningPattern,
|
||||
type LearningSuggestion,
|
||||
type LearningEventType,
|
||||
type FeedbackSentiment,
|
||||
} from '../types/active-learning';
|
||||
|
||||
// === 常量 ===
|
||||
|
||||
const MAX_EVENTS = 1000;
|
||||
const PATTERN_CONFIDENCE_THRESHOLD = 0.7;
|
||||
const SUGGESTION_COOLDOWN_HOURS = 2;
|
||||
|
||||
// === 生成 ID ===
|
||||
|
||||
function generateEventId(): string {
|
||||
return `le-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
// === 分析反馈情感 ===
|
||||
|
||||
export function analyzeSentiment(text: string): FeedbackSentiment {
|
||||
const positive = ['好的', '很棒', '谢谢', '完美', 'excellent', '喜欢', '爱了', 'good', 'great', 'nice', '满意'];
|
||||
const negative = ['不好', '差', '糟糕', '错误', 'wrong', 'bad', '不喜欢', '讨厌', '问题', '失败', 'fail', 'error'];
|
||||
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
if (positive.some(w => lowerText.includes(w.toLowerCase()))) return 'positive';
|
||||
if (negative.some(w => lowerText.includes(w.toLowerCase()))) return 'negative';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
// === 分析学习类型 ===
|
||||
|
||||
export function analyzeEventType(text: string): LearningEventType {
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
if (lowerText.includes('纠正') || lowerText.includes('不对') || lowerText.includes('修改')) {
|
||||
return 'correction';
|
||||
}
|
||||
if (lowerText.includes('喜欢') || lowerText.includes('偏好') || lowerText.includes('风格')) {
|
||||
return 'preference';
|
||||
}
|
||||
if (lowerText.includes('场景') || lowerText.includes('上下文') || lowerText.includes('情况')) {
|
||||
return 'context';
|
||||
}
|
||||
if (lowerText.includes('总是') || lowerText.includes('经常') || lowerText.includes('习惯')) {
|
||||
return 'behavior';
|
||||
}
|
||||
return 'implicit';
|
||||
}
|
||||
|
||||
// === 推断偏好 ===
|
||||
|
||||
export function inferPreference(feedback: string, sentiment: FeedbackSentiment): string {
|
||||
if (sentiment === 'positive') {
|
||||
if (feedback.includes('简洁')) return '用户偏好简洁的回复';
|
||||
if (feedback.includes('详细')) return '用户偏好详细的回复';
|
||||
if (feedback.includes('快速')) return '用户偏好快速响应';
|
||||
return '用户对当前回复风格满意';
|
||||
}
|
||||
if (sentiment === 'negative') {
|
||||
if (feedback.includes('太长')) return '用户偏好更短的回复';
|
||||
if (feedback.includes('太短')) return '用户偏好更详细的回复';
|
||||
if (feedback.includes('不准确')) return '用户偏好更准确的信息';
|
||||
return '用户对当前回复风格不满意';
|
||||
}
|
||||
return '用户反馈中性';
|
||||
}
|
||||
|
||||
// === 学习引擎类 ===
|
||||
|
||||
export class ActiveLearningEngine {
|
||||
private events: LearningEvent[] = [];
|
||||
private patterns: LearningPattern[] = [];
|
||||
private suggestions: LearningSuggestion[] = [];
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录学习事件
|
||||
*/
|
||||
recordEvent(
|
||||
event: Omit<LearningEvent, 'id' | 'timestamp' | 'acknowledged' | 'appliedCount'>
|
||||
): LearningEvent {
|
||||
// 检查重复事件
|
||||
const existing = this.events.find(e =>
|
||||
e.agentId === event.agentId &&
|
||||
e.messageId === event.messageId &&
|
||||
e.type === event.type
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// 更新现有事件
|
||||
existing.observation += ' | ' + event.observation;
|
||||
existing.confidence = (existing.confidence + event.confidence) / 2;
|
||||
existing.appliedCount++;
|
||||
return existing;
|
||||
}
|
||||
|
||||
// 创建新事件
|
||||
const newEvent: LearningEvent = {
|
||||
...event,
|
||||
id: generateEventId(),
|
||||
timestamp: Date.now(),
|
||||
acknowledged: false,
|
||||
appliedCount: 0,
|
||||
};
|
||||
|
||||
this.events.push(newEvent);
|
||||
this.extractPatterns(newEvent);
|
||||
|
||||
// 保持事件数量限制
|
||||
if (this.events.length > MAX_EVENTS) {
|
||||
this.events = this.events.slice(-MAX_EVENTS);
|
||||
}
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从反馈中学习
|
||||
*/
|
||||
learnFromFeedback(
|
||||
agentId: string,
|
||||
messageId: string,
|
||||
feedback: string,
|
||||
context?: string
|
||||
): LearningEvent {
|
||||
const sentiment = analyzeSentiment(feedback);
|
||||
const type = analyzeEventType(feedback);
|
||||
|
||||
return this.recordEvent({
|
||||
type,
|
||||
agentId,
|
||||
messageId,
|
||||
trigger: context || 'User feedback',
|
||||
observation: feedback,
|
||||
context,
|
||||
inferredPreference: inferPreference(feedback, sentiment),
|
||||
confidence: sentiment === 'positive' ? 0.8 : sentiment === 'negative' ? 0.5 : 0.3,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取学习模式
|
||||
*/
|
||||
private extractPatterns(event: LearningEvent): void {
|
||||
// 1. 正面反馈 -> 偏好正面回复
|
||||
if (event.observation.includes('谢谢') || event.observation.includes('好的')) {
|
||||
this.addPattern({
|
||||
type: 'preference',
|
||||
pattern: 'positive_response_preference',
|
||||
description: '用户偏好正面回复风格',
|
||||
examples: [event.observation],
|
||||
confidence: 0.8,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 纠正 -> 需要更精确
|
||||
if (event.type === 'correction') {
|
||||
this.addPattern({
|
||||
type: 'rule',
|
||||
pattern: 'precision_preference',
|
||||
description: '用户对精确性有更高要求',
|
||||
examples: [event.observation],
|
||||
confidence: 0.9,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 上下文相关 -> 场景偏好
|
||||
if (event.context) {
|
||||
this.addPattern({
|
||||
type: 'context',
|
||||
pattern: 'context_aware',
|
||||
description: 'Agent 需要关注上下文',
|
||||
examples: [event.context],
|
||||
confidence: 0.6,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加学习模式
|
||||
*/
|
||||
private addPattern(pattern: Omit<LearningPattern, 'updatedAt'>): void {
|
||||
const existing = this.patterns.find(p =>
|
||||
p.type === pattern.type &&
|
||||
p.pattern === pattern.pattern &&
|
||||
p.agentId === pattern.agentId
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// 增强置信度
|
||||
existing.confidence = Math.min(1, existing.confidence + pattern.confidence * 0.1);
|
||||
existing.examples.push(pattern.examples[0]);
|
||||
existing.updatedAt = Date.now();
|
||||
} else {
|
||||
this.patterns.push({
|
||||
...pattern,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成学习建议
|
||||
*/
|
||||
generateSuggestions(agentId: string): LearningSuggestion[] {
|
||||
const suggestions: LearningSuggestion[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
// 获取该 Agent 的模式
|
||||
const agentPatterns = this.patterns.filter(p => p.agentId === agentId);
|
||||
|
||||
for (const pattern of agentPatterns) {
|
||||
// 检查冷却时间
|
||||
const hoursSinceUpdate = (now - (pattern.updatedAt || now)) / (1000 * 60 * 60);
|
||||
if (hoursSinceUpdate < SUGGESTION_COOLDOWN_HOURS) continue;
|
||||
|
||||
// 检查置信度阈值
|
||||
if (pattern.confidence < PATTERN_CONFIDENCE_THRESHOLD) continue;
|
||||
|
||||
// 生成建议
|
||||
suggestions.push({
|
||||
id: `sug-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
agentId,
|
||||
type: pattern.type,
|
||||
pattern: pattern.pattern,
|
||||
suggestion: this.generateSuggestionContent(pattern),
|
||||
confidence: pattern.confidence,
|
||||
createdAt: now,
|
||||
expiresAt: new Date(now + 7 * 24 * 60 * 60 * 1000),
|
||||
dismissed: false,
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成建议内容
|
||||
*/
|
||||
private generateSuggestionContent(pattern: LearningPattern): string {
|
||||
const templates: Record<string, string> = {
|
||||
positive_response_preference:
|
||||
'用户似乎偏好正面回复。建议在回复时保持积极和确认的语气。',
|
||||
precision_preference:
|
||||
'用户对精确性有更高要求。建议在提供信息时更加详细和准确。',
|
||||
context_aware:
|
||||
'Agent 需要关注上下文。建议在回复时考虑对话的背景和历史。',
|
||||
};
|
||||
|
||||
return templates[pattern.pattern] || `观察到模式: ${pattern.pattern}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStats(agentId: string) {
|
||||
const agentEvents = this.events.filter(e => e.agentId === agentId);
|
||||
const agentPatterns = this.patterns.filter(p => p.agentId === agentId);
|
||||
|
||||
const eventsByType: Record<LearningEventType, number> = {
|
||||
preference: 0,
|
||||
correction: 0,
|
||||
context: 0,
|
||||
feedback: 0,
|
||||
behavior: 0,
|
||||
implicit: 0,
|
||||
};
|
||||
|
||||
for (const event of agentEvents) {
|
||||
eventsByType[event.type]++;
|
||||
}
|
||||
|
||||
return {
|
||||
totalEvents: agentEvents.length,
|
||||
eventsByType,
|
||||
totalPatterns: agentPatterns.length,
|
||||
avgConfidence: agentPatterns.length > 0
|
||||
? agentPatterns.reduce((sum, p) => sum + p.confidence, 0) / agentPatterns.length
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有事件
|
||||
*/
|
||||
getEvents(agentId?: string): LearningEvent[] {
|
||||
if (agentId) {
|
||||
return this.events.filter(e => e.agentId === agentId);
|
||||
}
|
||||
return [...this.events];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有模式
|
||||
*/
|
||||
getPatterns(agentId?: string): LearningPattern[] {
|
||||
if (agentId) {
|
||||
return this.patterns.filter(p => p.agentId === agentId);
|
||||
}
|
||||
return [...this.patterns];
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认事件
|
||||
*/
|
||||
acknowledgeEvent(eventId: string): void {
|
||||
const event = this.events.find(e => e.id === eventId);
|
||||
if (event) {
|
||||
event.acknowledged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除事件
|
||||
*/
|
||||
clearEvents(agentId: string): void {
|
||||
this.events = this.events.filter(e => e.agentId !== agentId);
|
||||
this.patterns = this.patterns.filter(p => p.agentId !== agentId);
|
||||
}
|
||||
}
|
||||
|
||||
// === 单例实例 ===
|
||||
|
||||
let engineInstance: ActiveLearningEngine | null = null;
|
||||
|
||||
export function getActiveLearningEngine(): ActiveLearningEngine {
|
||||
if (!engineInstance) {
|
||||
engineInstance = new ActiveLearningEngine();
|
||||
}
|
||||
return engineInstance;
|
||||
}
|
||||
|
||||
export function resetActiveLearningEngine(): void {
|
||||
engineInstance = null;
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.1
|
||||
*/
|
||||
|
||||
import { MemoryIndex, getMemoryIndex, resetMemoryIndex, tokenize } from './memory-index';
|
||||
import { MemoryIndex, getMemoryIndex, tokenize } from './memory-index';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface MemorySearchOptions {
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
minImportance?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface MemoryStats {
|
||||
|
||||
460
desktop/src/lib/browser-client.ts
Normal file
460
desktop/src/lib/browser-client.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
/**
|
||||
* Browser Automation Client for ZCLAW
|
||||
* Provides TypeScript API for Fantoccini-based browser automation
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface BrowserSessionResult {
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
export interface BrowserSessionInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
current_url: string | null;
|
||||
title: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
last_activity: string;
|
||||
}
|
||||
|
||||
export interface BrowserNavigationResult {
|
||||
url: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
export interface BrowserElementInfo {
|
||||
selector: string;
|
||||
tag_name: string | null;
|
||||
text: string | null;
|
||||
is_displayed: boolean;
|
||||
is_enabled: boolean;
|
||||
is_selected: boolean;
|
||||
location: BrowserElementLocation | null;
|
||||
size: BrowserElementSize | null;
|
||||
}
|
||||
|
||||
export interface BrowserElementLocation {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface BrowserElementSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface BrowserScreenshotResult {
|
||||
base64: string;
|
||||
format: string;
|
||||
}
|
||||
|
||||
export interface FormFieldData {
|
||||
selector: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session Management
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a new browser session
|
||||
*/
|
||||
export async function createSession(options?: {
|
||||
webdriverUrl?: string;
|
||||
headless?: boolean;
|
||||
browserType?: 'chrome' | 'firefox' | 'edge' | 'safari';
|
||||
windowWidth?: number;
|
||||
windowHeight?: number;
|
||||
}): Promise<BrowserSessionResult> {
|
||||
return invoke('browser_create_session', {
|
||||
webdriverUrl: options?.webdriverUrl,
|
||||
headless: options?.headless,
|
||||
browserType: options?.browserType,
|
||||
windowWidth: options?.windowWidth,
|
||||
windowHeight: options?.windowHeight,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a browser session
|
||||
*/
|
||||
export async function closeSession(sessionId: string): Promise<void> {
|
||||
return invoke('browser_close_session', { sessionId });
|
||||
}
|
||||
|
||||
/**
|
||||
* List all browser sessions
|
||||
*/
|
||||
export async function listSessions(): Promise<BrowserSessionInfo[]> {
|
||||
return invoke('browser_list_sessions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session info
|
||||
*/
|
||||
export async function getSession(sessionId: string): Promise<BrowserSessionInfo> {
|
||||
return invoke('browser_get_session', { sessionId });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Navigation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Navigate to URL
|
||||
*/
|
||||
export async function navigate(
|
||||
sessionId: string,
|
||||
url: string
|
||||
): Promise<BrowserNavigationResult> {
|
||||
return invoke('browser_navigate', { sessionId, url });
|
||||
}
|
||||
|
||||
/**
|
||||
* Go back
|
||||
*/
|
||||
export async function back(sessionId: string): Promise<void> {
|
||||
return invoke('browser_back', { sessionId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Go forward
|
||||
*/
|
||||
export async function forward(sessionId: string): Promise<void> {
|
||||
return invoke('browser_forward', { sessionId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh page
|
||||
*/
|
||||
export async function refresh(sessionId: string): Promise<void> {
|
||||
return invoke('browser_refresh', { sessionId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current URL
|
||||
*/
|
||||
export async function getCurrentUrl(sessionId: string): Promise<string> {
|
||||
return invoke('browser_get_url', { sessionId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page title
|
||||
*/
|
||||
export async function getTitle(sessionId: string): Promise<string> {
|
||||
return invoke('browser_get_title', { sessionId });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Element Interaction
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Find element by CSS selector
|
||||
*/
|
||||
export async function findElement(
|
||||
sessionId: string,
|
||||
selector: string
|
||||
): Promise<BrowserElementInfo> {
|
||||
return invoke('browser_find_element', { sessionId, selector });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple elements
|
||||
*/
|
||||
export async function findElements(
|
||||
sessionId: string,
|
||||
selector: string
|
||||
): Promise<BrowserElementInfo[]> {
|
||||
return invoke('browser_find_elements', { sessionId, selector });
|
||||
}
|
||||
|
||||
/**
|
||||
* Click element
|
||||
*/
|
||||
export async function click(sessionId: string, selector: string): Promise<void> {
|
||||
return invoke('browser_click', { sessionId, selector });
|
||||
}
|
||||
|
||||
/**
|
||||
* Type text into element
|
||||
*/
|
||||
export async function typeText(
|
||||
sessionId: string,
|
||||
selector: string,
|
||||
text: string,
|
||||
clearFirst?: boolean
|
||||
): Promise<void> {
|
||||
return invoke('browser_type', { sessionId, selector, text, clearFirst });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element text
|
||||
*/
|
||||
export async function getText(sessionId: string, selector: string): Promise<string> {
|
||||
return invoke('browser_get_text', { sessionId, selector });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element attribute
|
||||
*/
|
||||
export async function getAttribute(
|
||||
sessionId: string,
|
||||
selector: string,
|
||||
attribute: string
|
||||
): Promise<string | null> {
|
||||
return invoke('browser_get_attribute', { sessionId, selector, attribute });
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for element
|
||||
*/
|
||||
export async function waitForElement(
|
||||
sessionId: string,
|
||||
selector: string,
|
||||
timeoutMs?: number
|
||||
): Promise<BrowserElementInfo> {
|
||||
return invoke('browser_wait_for_element', {
|
||||
sessionId,
|
||||
selector,
|
||||
timeoutMs: timeoutMs ?? 10000,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Advanced Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Execute JavaScript
|
||||
*/
|
||||
export async function executeScript(
|
||||
sessionId: string,
|
||||
script: string,
|
||||
args?: unknown[]
|
||||
): Promise<unknown> {
|
||||
return invoke('browser_execute_script', { sessionId, script, args });
|
||||
}
|
||||
|
||||
/**
|
||||
* Take screenshot
|
||||
*/
|
||||
export async function screenshot(sessionId: string): Promise<BrowserScreenshotResult> {
|
||||
return invoke('browser_screenshot', { sessionId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Take element screenshot
|
||||
*/
|
||||
export async function elementScreenshot(
|
||||
sessionId: string,
|
||||
selector: string
|
||||
): Promise<BrowserScreenshotResult> {
|
||||
return invoke('browser_element_screenshot', { sessionId, selector });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page source
|
||||
*/
|
||||
export async function getSource(sessionId: string): Promise<string> {
|
||||
return invoke('browser_get_source', { sessionId });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// High-Level Tasks
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Scrape page content
|
||||
*/
|
||||
export async function scrapePage(
|
||||
sessionId: string,
|
||||
selectors: string[],
|
||||
waitFor?: string,
|
||||
timeoutMs?: number
|
||||
): Promise<Record<string, string[]>> {
|
||||
return invoke('browser_scrape_page', {
|
||||
sessionId,
|
||||
selectors,
|
||||
waitFor,
|
||||
timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill form
|
||||
*/
|
||||
export async function fillForm(
|
||||
sessionId: string,
|
||||
fields: FormFieldData[],
|
||||
submitSelector?: string
|
||||
): Promise<void> {
|
||||
return invoke('browser_fill_form', { sessionId, fields, submitSelector });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Browser Client Class (Convenience Wrapper)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* High-level browser client for easier usage
|
||||
*/
|
||||
export class Browser {
|
||||
private sessionId: string | null = null;
|
||||
|
||||
/**
|
||||
* Start a new browser session
|
||||
*/
|
||||
async start(options?: {
|
||||
webdriverUrl?: string;
|
||||
headless?: boolean;
|
||||
browserType?: 'chrome' | 'firefox' | 'edge' | 'safari';
|
||||
windowWidth?: number;
|
||||
windowHeight?: number;
|
||||
}): Promise<string> {
|
||||
const result = await createSession(options);
|
||||
this.sessionId = result.session_id;
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close browser session
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (this.sessionId) {
|
||||
await closeSession(this.sessionId);
|
||||
this.sessionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session ID
|
||||
*/
|
||||
getSessionId(): string | null {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to URL
|
||||
*/
|
||||
async goto(url: string): Promise<BrowserNavigationResult> {
|
||||
this.ensureSession();
|
||||
return navigate(this.sessionId!, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find element
|
||||
*/
|
||||
async $(selector: string): Promise<BrowserElementInfo> {
|
||||
this.ensureSession();
|
||||
return findElement(this.sessionId!, selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple elements
|
||||
*/
|
||||
async $$(selector: string): Promise<BrowserElementInfo[]> {
|
||||
this.ensureSession();
|
||||
return findElements(this.sessionId!, selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click element
|
||||
*/
|
||||
async click(selector: string): Promise<void> {
|
||||
this.ensureSession();
|
||||
return click(this.sessionId!, selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type text
|
||||
*/
|
||||
async type(selector: string, text: string, clearFirst = false): Promise<void> {
|
||||
this.ensureSession();
|
||||
return typeText(this.sessionId!, selector, text, clearFirst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for element
|
||||
*/
|
||||
async wait(selector: string, timeoutMs = 10000): Promise<BrowserElementInfo> {
|
||||
this.ensureSession();
|
||||
return waitForElement(this.sessionId!, selector, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take screenshot
|
||||
*/
|
||||
async screenshot(): Promise<BrowserScreenshotResult> {
|
||||
this.ensureSession();
|
||||
return screenshot(this.sessionId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute JavaScript
|
||||
*/
|
||||
async eval(script: string, args?: unknown[]): Promise<unknown> {
|
||||
this.ensureSession();
|
||||
return executeScript(this.sessionId!, script, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page source
|
||||
*/
|
||||
async source(): Promise<string> {
|
||||
this.ensureSession();
|
||||
return getSource(this.sessionId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current URL
|
||||
*/
|
||||
async url(): Promise<string> {
|
||||
this.ensureSession();
|
||||
return getCurrentUrl(this.sessionId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page title
|
||||
*/
|
||||
async title(): Promise<string> {
|
||||
this.ensureSession();
|
||||
return getTitle(this.sessionId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrape page content
|
||||
*/
|
||||
async scrape(
|
||||
selectors: string[],
|
||||
waitFor?: string,
|
||||
timeoutMs?: number
|
||||
): Promise<Record<string, string[]>> {
|
||||
this.ensureSession();
|
||||
return scrapePage(this.sessionId!, selectors, waitFor, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill form
|
||||
*/
|
||||
async fillForm(fields: FormFieldData[], submitSelector?: string): Promise<void> {
|
||||
this.ensureSession();
|
||||
return fillForm(this.sessionId!, fields, submitSelector);
|
||||
}
|
||||
|
||||
private ensureSession(): void {
|
||||
if (!this.sessionId) {
|
||||
throw new Error('Browser session not started. Call start() first.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default export
|
||||
export default Browser;
|
||||
@@ -12,13 +12,15 @@ import {
|
||||
ErrorSeverity,
|
||||
} from './error-types';
|
||||
|
||||
// === Error Store ===
|
||||
// === Types ===
|
||||
|
||||
interface StoredError extends AppError {
|
||||
export interface StoredError extends AppError {
|
||||
dismissed: boolean;
|
||||
reported: boolean;
|
||||
}
|
||||
|
||||
// === Error Store ===
|
||||
|
||||
interface ErrorStore {
|
||||
errors: StoredError[];
|
||||
addError: (error: AppError) => void;
|
||||
@@ -52,12 +54,17 @@ function initErrorStore(): void {
|
||||
errors: [],
|
||||
|
||||
addError: (error: AppError) => {
|
||||
errorStore.errors = [error, ...errorStore.errors];
|
||||
const storedError: StoredError = {
|
||||
...error,
|
||||
dismissed: false,
|
||||
reported: false,
|
||||
};
|
||||
errorStore.errors = [storedError, ...errorStore.errors];
|
||||
// Notify listeners
|
||||
notifyErrorListeners(error);
|
||||
},
|
||||
|
||||
dismissError: (id: string) => void {
|
||||
dismissError(id: string): void {
|
||||
const error = errorStore.errors.find(e => e.id === id);
|
||||
if (error) {
|
||||
errorStore.errors = errorStore.errors.map(e =>
|
||||
@@ -66,11 +73,11 @@ function initErrorStore(): void {
|
||||
}
|
||||
},
|
||||
|
||||
dismissAll: () => void {
|
||||
dismissAll(): void {
|
||||
errorStore.errors = errorStore.errors.map(e => ({ ...e, dismissed: true }));
|
||||
},
|
||||
|
||||
markReported: (id: string) => void {
|
||||
markReported(id: string): void {
|
||||
const error = errorStore.errors.find(e => e.id === id);
|
||||
if (error) {
|
||||
errorStore.errors = errorStore.errors.map(e =>
|
||||
@@ -79,19 +86,19 @@ function initErrorStore(): void {
|
||||
}
|
||||
},
|
||||
|
||||
getUndismissedErrors: () => StoredError[] => {
|
||||
getUndismissedErrors(): StoredError[] {
|
||||
return errorStore.errors.filter(e => !e.dismissed);
|
||||
},
|
||||
|
||||
getErrorCount: () => number => {
|
||||
getErrorCount(): number {
|
||||
return errorStore.errors.filter(e => !e.dismissed).length;
|
||||
},
|
||||
|
||||
getErrorsByCategory: (category: ErrorCategory) => StoredError[] => {
|
||||
getErrorsByCategory(category: ErrorCategory): StoredError[] {
|
||||
return errorStore.errors.filter(e => e.category === category && !e.dismissed);
|
||||
},
|
||||
|
||||
getErrorsBySeverity: (severity: ErrorSeverity) => StoredError[] => {
|
||||
getErrorsBySeverity(severity: ErrorSeverity): StoredError[] {
|
||||
return errorStore.errors.filter(e => e.severity === severity && !e.dismissed);
|
||||
},
|
||||
};
|
||||
@@ -366,8 +373,3 @@ interface ErrorEvent {
|
||||
reason?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface StoredError extends AppError {
|
||||
dismissed: boolean;
|
||||
reported: boolean;
|
||||
}
|
||||
|
||||
@@ -353,13 +353,15 @@ export function classifyError(error: unknown): AppError {
|
||||
severity: pattern.severity,
|
||||
title: pattern.title,
|
||||
message: pattern.messageTemplate(match),
|
||||
// Only include name and message, not stack trace (security)
|
||||
technicalDetails: error instanceof Error
|
||||
? `${error.name}: ${error.message}\n${error.stack || ''}`
|
||||
? `${error.name}: ${error.message}`
|
||||
: String(error),
|
||||
recoverable: pattern.recoverable,
|
||||
recoverySteps: pattern.recoverySteps,
|
||||
timestamp: new Date(),
|
||||
originalError: error,
|
||||
// Only preserve original error in development mode
|
||||
originalError: import.meta.env.DEV ? error : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -370,8 +372,9 @@ export function classifyError(error: unknown): AppError {
|
||||
severity: 'medium',
|
||||
title: 'An Error Occurred',
|
||||
message: error instanceof Error ? error.message : 'An unexpected error occurred.',
|
||||
// Only include name and message, not stack trace (security)
|
||||
technicalDetails: error instanceof Error
|
||||
? `${error.name}: ${error.message}\n${error.stack || ''}`
|
||||
? `${error.name}: ${error.message}`
|
||||
: String(error),
|
||||
recoverable: true,
|
||||
recoverySteps: [
|
||||
@@ -380,7 +383,8 @@ export function classifyError(error: unknown): AppError {
|
||||
{ description: 'Contact support with the error details' },
|
||||
],
|
||||
timestamp: new Date(),
|
||||
originalError: error,
|
||||
// Only preserve original error in development mode
|
||||
originalError: import.meta.env.DEV ? error : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
82
desktop/src/lib/error-utils.ts
Normal file
82
desktop/src/lib/error-utils.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 错误处理工具函数
|
||||
* 提供统一的错误消息提取和静默错误处理
|
||||
*/
|
||||
|
||||
/**
|
||||
* 从未知错误中提取错误消息
|
||||
* @param err - 捕获的错误
|
||||
* @returns 格式化的错误消息字符串
|
||||
*/
|
||||
export function getErrorMessage(err: unknown): string {
|
||||
if (err instanceof Error) return err.message;
|
||||
if (typeof err === 'string') return err;
|
||||
if (err && typeof err === 'object' && 'message' in err) {
|
||||
return String((err as { message: unknown }).message);
|
||||
}
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型守卫:检查是否为 Error 实例
|
||||
*/
|
||||
export function isError(err: unknown): err is Error {
|
||||
return err instanceof Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误的堆栈跟踪(仅开发环境)
|
||||
*/
|
||||
export function getErrorStack(err: unknown): string | undefined {
|
||||
if (import.meta.env.DEV && err instanceof Error) {
|
||||
return err.stack;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建静默错误处理器
|
||||
* 用于 UI 事件处理器中预期的、不需要用户通知的错误
|
||||
* 在开发环境中会记录警告,生产环境中静默处理
|
||||
*
|
||||
* @param context - 上下文名称,用于日志标识
|
||||
* @returns 错误处理函数
|
||||
*
|
||||
* @example
|
||||
* // 在事件处理器中使用
|
||||
* onClick={() => { handleSubmit().catch(silentErrorHandler('FeedbackModal')); }}
|
||||
*/
|
||||
export function silentErrorHandler(context: string): (err: unknown) => void {
|
||||
return (err: unknown) => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`[${context}] Operation failed silently:`, getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全执行异步操作,捕获错误并可选地记录
|
||||
* 用于不阻塞主流程的副作用操作
|
||||
*
|
||||
* @param context - 上下文名称
|
||||
* @param fn - 要执行的异步函数
|
||||
* @param options - 配置选项
|
||||
*
|
||||
* @example
|
||||
* // 安全执行连接操作
|
||||
* safeAsync('App', () => connect());
|
||||
*/
|
||||
export async function safeAsync<T>(
|
||||
context: string,
|
||||
fn: () => Promise<T>,
|
||||
options: { logInDev?: boolean } = { logInDev: true }
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err: unknown) {
|
||||
if (options.logInDev !== false && import.meta.env.DEV) {
|
||||
console.warn(`[${context}] Async operation failed:`, getErrorMessage(err));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -37,15 +37,31 @@ import {
|
||||
|
||||
/**
|
||||
* Whether to use WSS (WebSocket Secure) instead of WS.
|
||||
* Set VITE_USE_WSS=true in production environments.
|
||||
* - Production: defaults to WSS for security
|
||||
* - Development: defaults to WS for convenience
|
||||
* - Override: set VITE_USE_WSS=false to force WS in production
|
||||
*/
|
||||
const USE_WSS = import.meta.env.VITE_USE_WSS === 'true';
|
||||
const USE_WSS = import.meta.env.VITE_USE_WSS !== 'false' && import.meta.env.PROD;
|
||||
|
||||
/**
|
||||
* Default protocol based on WSS configuration.
|
||||
*/
|
||||
const DEFAULT_WS_PROTOCOL = USE_WSS ? 'wss://' : 'ws://';
|
||||
|
||||
/**
|
||||
* Check if a URL points to localhost.
|
||||
*/
|
||||
function isLocalhost(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.hostname === 'localhost' ||
|
||||
parsed.hostname === '127.0.0.1' ||
|
||||
parsed.hostname === '[::1]';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// OpenFang endpoints (actual port is 50051, not 4200)
|
||||
// Note: REST API uses relative path to leverage Vite proxy for CORS bypass
|
||||
export const DEFAULT_GATEWAY_URL = `${DEFAULT_WS_PROTOCOL}127.0.0.1:50051/ws`;
|
||||
@@ -87,7 +103,12 @@ export interface GatewayEvent {
|
||||
seq?: number;
|
||||
}
|
||||
|
||||
export type GatewayFrame = GatewayRequest | GatewayResponse | GatewayEvent;
|
||||
export interface GatewayPong {
|
||||
type: 'pong';
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export type GatewayFrame = GatewayRequest | GatewayResponse | GatewayEvent | GatewayPong;
|
||||
|
||||
function createIdempotencyKey(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
@@ -119,7 +140,7 @@ export interface AgentStreamDelta {
|
||||
|
||||
/** OpenFang WebSocket stream event types */
|
||||
export interface OpenFangStreamEvent {
|
||||
type: 'text_delta' | 'phase' | 'response' | 'typing' | 'tool_call' | 'tool_result' | 'hand' | 'workflow' | 'error';
|
||||
type: 'text_delta' | 'phase' | 'response' | 'typing' | 'tool_call' | 'tool_result' | 'hand' | 'workflow' | 'error' | 'connected' | 'agents_updated';
|
||||
content?: string;
|
||||
phase?: 'streaming' | 'done';
|
||||
state?: 'start' | 'stop';
|
||||
@@ -136,6 +157,8 @@ export interface OpenFangStreamEvent {
|
||||
workflow_result?: unknown;
|
||||
message?: string;
|
||||
code?: string;
|
||||
agent_id?: string;
|
||||
agents?: Array<{ id: string; name: string; status: string }>;
|
||||
}
|
||||
|
||||
export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';
|
||||
@@ -481,6 +504,11 @@ export class GatewayClient {
|
||||
return this.connectRest();
|
||||
}
|
||||
|
||||
// Security warning: non-localhost with insecure WebSocket
|
||||
if (!this.url.startsWith('wss://') && !isLocalhost(this.url)) {
|
||||
console.warn('[Gateway] Connecting to non-localhost with insecure WebSocket (ws://). Consider using WSS in production.');
|
||||
}
|
||||
|
||||
this.autoReconnect = true;
|
||||
this.setState('connecting');
|
||||
|
||||
@@ -945,8 +973,57 @@ export class GatewayClient {
|
||||
privacyOptIn?: boolean;
|
||||
userName?: string;
|
||||
userRole?: string;
|
||||
emoji?: string;
|
||||
personality?: string;
|
||||
communicationStyle?: string;
|
||||
notes?: string;
|
||||
}): Promise<any> {
|
||||
return this.restPost('/api/agents', opts);
|
||||
// Build manifest_toml for OpenClaw Gateway
|
||||
const lines: string[] = [];
|
||||
lines.push(`name = "${opts.nickname || opts.name}"`);
|
||||
lines.push(`model_provider = "bailian"`);
|
||||
lines.push(`model_name = "${opts.model || 'qwen3.5-plus'}"`);
|
||||
|
||||
// Add identity section
|
||||
lines.push('');
|
||||
lines.push('[identity]');
|
||||
if (opts.emoji) {
|
||||
lines.push(`emoji = "${opts.emoji}"`);
|
||||
}
|
||||
if (opts.personality) {
|
||||
lines.push(`personality = "${opts.personality}"`);
|
||||
}
|
||||
if (opts.communicationStyle) {
|
||||
lines.push(`communication_style = "${opts.communicationStyle}"`);
|
||||
}
|
||||
|
||||
// Add scenarios
|
||||
if (opts.scenarios && opts.scenarios.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('scenarios = [');
|
||||
opts.scenarios.forEach((s, i) => {
|
||||
lines.push(` "${s}"${i < opts.scenarios!.length - 1 ? ',' : ''}`);
|
||||
});
|
||||
lines.push(']');
|
||||
}
|
||||
|
||||
// Add user context
|
||||
if (opts.userName || opts.userRole) {
|
||||
lines.push('');
|
||||
lines.push('[user_context]');
|
||||
if (opts.userName) {
|
||||
lines.push(`name = "${opts.userName}"`);
|
||||
}
|
||||
if (opts.userRole) {
|
||||
lines.push(`role = "${opts.userRole}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const manifestToml = lines.join('\n');
|
||||
|
||||
return this.restPost('/api/agents', {
|
||||
manifest_toml: manifestToml,
|
||||
});
|
||||
}
|
||||
async updateClone(id: string, updates: Record<string, any>): Promise<any> {
|
||||
return this.restPut(`/api/agents/${id}`, updates);
|
||||
@@ -1496,7 +1573,9 @@ export class GatewayClient {
|
||||
|
||||
/** Subscribe to agent stream events */
|
||||
onAgentStream(callback: (delta: AgentStreamDelta) => void): () => void {
|
||||
return this.on('agent', callback);
|
||||
return this.on('agent', (payload: unknown) => {
|
||||
callback(payload as AgentStreamDelta);
|
||||
});
|
||||
}
|
||||
|
||||
// === Internal ===
|
||||
@@ -1518,7 +1597,8 @@ export class GatewayClient {
|
||||
private handleEvent(event: GatewayEvent, connectResolve?: () => void, connectReject?: (error: Error) => void) {
|
||||
// Handle connect challenge
|
||||
if (event.event === 'connect.challenge' && this.state === 'handshaking') {
|
||||
this.performHandshake(event.payload?.nonce, connectResolve, connectReject);
|
||||
const payload = event.payload as { nonce?: string } | undefined;
|
||||
this.performHandshake(payload?.nonce || '', connectResolve, connectReject);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1526,7 +1606,12 @@ export class GatewayClient {
|
||||
this.emitEvent(event.event, event.payload);
|
||||
}
|
||||
|
||||
private async performHandshake(challengeNonce: string, connectResolve?: () => void, connectReject?: (error: Error) => void) {
|
||||
private async performHandshake(challengeNonce: string | undefined, connectResolve?: () => void, connectReject?: (error: Error) => void) {
|
||||
if (!challengeNonce) {
|
||||
this.log('error', 'No challenge nonce received');
|
||||
connectReject?.(new Error('Handshake failed: no challenge nonce'));
|
||||
return;
|
||||
}
|
||||
const connectId = `connect_${Date.now()}`;
|
||||
// Use a valid client ID from GATEWAY_CLIENT_ID_SET
|
||||
// Valid IDs: gateway-client, cli, webchat, node-host, test
|
||||
@@ -1761,7 +1846,7 @@ export class GatewayClient {
|
||||
// Don't reconnect immediately, let the next heartbeat check
|
||||
}, GatewayClient.HEARTBEAT_TIMEOUT);
|
||||
} catch (error) {
|
||||
this.log('error', 'Failed to send heartbeat', error);
|
||||
this.log('error', `Failed to send heartbeat: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,8 +187,13 @@ class OpenAILLMAdapter implements LLMServiceAdapter {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`[OpenAI] API error: ${response.status} - ${error}`);
|
||||
const errorBody = await response.text();
|
||||
// Log full error in development only
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[OpenAI] API error:', errorBody);
|
||||
}
|
||||
// Return sanitized error to caller
|
||||
throw new Error(`[OpenAI] API error: ${response.status} - Request failed`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -247,8 +252,13 @@ class VolcengineLLMAdapter implements LLMServiceAdapter {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`[Volcengine] API error: ${response.status} - ${error}`);
|
||||
const errorBody = await response.text();
|
||||
// Log full error in development only
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[Volcengine] API error:', errorBody);
|
||||
}
|
||||
// Return sanitized error to caller
|
||||
throw new Error(`[Volcengine] API error: ${response.status} - Request failed`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
* @module message-virtualization
|
||||
*/
|
||||
|
||||
import { useRef, useCallback, useMemo, useEffect, type React } from 'react';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import { useRef, useCallback, useMemo, useEffect, type CSSProperties, type ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
|
||||
// Type alias for convenience
|
||||
type List = VariableSizeList;
|
||||
|
||||
/**
|
||||
* Message item interface for virtualization
|
||||
@@ -24,7 +28,7 @@ export interface VirtualizedMessageItem {
|
||||
*/
|
||||
export interface VirtualizedMessageListProps {
|
||||
messages: VirtualizedMessageItem[];
|
||||
renderMessage: (id: string, style: React.CSSProperties) => React.ReactNode;
|
||||
renderMessage: (id: string, style: CSSProperties) => ReactNode;
|
||||
height: number;
|
||||
width: number | string;
|
||||
overscan?: number;
|
||||
@@ -49,7 +53,7 @@ const DEFAULT_HEIGHTS: Record<string, number> = {
|
||||
*/
|
||||
export interface UseVirtualizedMessagesReturn {
|
||||
/** Reference to the VariableSizeList instance */
|
||||
listRef: React.RefObject<List | null>;
|
||||
listRef: React.RefObject<VariableSizeList | null>;
|
||||
/** Get the current height for a message by id and role */
|
||||
getHeight: (id: string, role: string) => number;
|
||||
/** Update the measured height for a message */
|
||||
@@ -388,7 +392,7 @@ export function useMemoizedContent<T>(
|
||||
cache?: MessageCache<T>
|
||||
): T {
|
||||
// Use provided cache or create a default one
|
||||
const cacheRef = useRef<MessageCache<T>>();
|
||||
const cacheRef = useRef<MessageCache<T> | undefined>(undefined);
|
||||
if (!cacheRef.current && !cache) {
|
||||
cacheRef.current = new MessageCache<T>(200);
|
||||
}
|
||||
|
||||
361
desktop/src/lib/personality-presets.ts
Normal file
361
desktop/src/lib/personality-presets.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Personality Presets Configuration
|
||||
*
|
||||
* Defines personality styles, scenario tags, and emoji presets for Agent onboarding.
|
||||
* Used by AgentOnboardingWizard to provide guided personality setup.
|
||||
*/
|
||||
|
||||
// === Personality Options ===
|
||||
|
||||
export interface PersonalityOption {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: string; // Icon name for Lucide
|
||||
traits: string[];
|
||||
communicationStyle: string;
|
||||
}
|
||||
|
||||
export const PERSONALITY_OPTIONS: PersonalityOption[] = [
|
||||
{
|
||||
id: 'professional',
|
||||
label: '专业严谨',
|
||||
description: '精确、可靠、技术导向',
|
||||
icon: 'Briefcase',
|
||||
traits: ['精确', '可靠', '技术导向', '系统化'],
|
||||
communicationStyle: '专业、准确、注重细节,提供技术深度和可操作的建议',
|
||||
},
|
||||
{
|
||||
id: 'friendly',
|
||||
label: '友好亲切',
|
||||
description: '温暖、耐心、易于沟通',
|
||||
icon: 'Heart',
|
||||
traits: ['温暖', '耐心', '易于沟通', '善解人意'],
|
||||
communicationStyle: '亲切、耐心、善解人意,用易懂的语言解释复杂概念',
|
||||
},
|
||||
{
|
||||
id: 'creative',
|
||||
label: '创意灵活',
|
||||
description: '想象力丰富、善于探索',
|
||||
icon: 'Sparkles',
|
||||
traits: ['想象力丰富', '善于探索', '思维开放', '创新'],
|
||||
communicationStyle: '富有创意、思维开放,鼓励探索新想法和解决方案',
|
||||
},
|
||||
{
|
||||
id: 'concise',
|
||||
label: '简洁高效',
|
||||
description: '快速、直接、结果导向',
|
||||
icon: 'Zap',
|
||||
traits: ['快速', '直接', '结果导向', '高效'],
|
||||
communicationStyle: '简洁明了、直奔主题,专注于快速解决问题',
|
||||
},
|
||||
];
|
||||
|
||||
// === Scenario Tags ===
|
||||
|
||||
export interface ScenarioTag {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: string; // Icon name for Lucide
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
export const SCENARIO_TAGS: ScenarioTag[] = [
|
||||
{
|
||||
id: 'coding',
|
||||
label: '编程开发',
|
||||
description: '代码编写、调试、代码审查',
|
||||
icon: 'Code',
|
||||
keywords: ['编程', '代码', '开发', '调试', 'Bug', '重构'],
|
||||
},
|
||||
{
|
||||
id: 'writing',
|
||||
label: '内容写作',
|
||||
description: '文章撰写、文案创作、编辑润色',
|
||||
icon: 'PenLine',
|
||||
keywords: ['写作', '文案', '文章', '内容', '编辑', '润色'],
|
||||
},
|
||||
{
|
||||
id: 'product',
|
||||
label: '产品策划',
|
||||
description: '产品规划、需求分析、用户研究',
|
||||
icon: 'Package',
|
||||
keywords: ['产品', '需求', '用户', '规划', '功能', 'PRD'],
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
label: '数据分析',
|
||||
description: '数据处理、统计分析、可视化',
|
||||
icon: 'BarChart',
|
||||
keywords: ['数据', '分析', '统计', '图表', '可视化', '报表'],
|
||||
},
|
||||
{
|
||||
id: 'design',
|
||||
label: '设计创意',
|
||||
description: 'UI/UX设计、视觉设计、原型制作',
|
||||
icon: 'Palette',
|
||||
keywords: ['设计', 'UI', 'UX', '视觉', '原型', '界面'],
|
||||
},
|
||||
{
|
||||
id: 'devops',
|
||||
label: '运维部署',
|
||||
description: '系统运维、CI/CD、容器化部署',
|
||||
icon: 'Server',
|
||||
keywords: ['运维', '部署', 'CI/CD', 'Docker', 'K8s', '服务器'],
|
||||
},
|
||||
{
|
||||
id: 'research',
|
||||
label: '研究调研',
|
||||
description: '技术调研、文献研究、竞品分析',
|
||||
icon: 'Search',
|
||||
keywords: ['研究', '调研', '分析', '文献', '竞品', '技术'],
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
label: '营销推广',
|
||||
description: '营销策略、内容营销、社媒运营',
|
||||
icon: 'Megaphone',
|
||||
keywords: ['营销', '推广', '运营', '社媒', '增长', '转化'],
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
label: '其他',
|
||||
description: '其他用途或综合场景',
|
||||
icon: 'MoreHorizontal',
|
||||
keywords: [],
|
||||
},
|
||||
];
|
||||
|
||||
// === Emoji Presets ===
|
||||
|
||||
export const EMOJI_PRESETS = {
|
||||
animals: ['🦞', '🐱', '🐶', '🦊', '🐼', '🦁', '🐬', '🦄'],
|
||||
objects: ['💻', '🚀', '⚡', '🔧', '📚', '🎨', '⭐', '💎'],
|
||||
expressions: ['😊', '🤓', '😎', '🤖'],
|
||||
};
|
||||
|
||||
export const ALL_EMOJIS = [
|
||||
...EMOJI_PRESETS.animals,
|
||||
...EMOJI_PRESETS.objects,
|
||||
...EMOJI_PRESETS.expressions,
|
||||
];
|
||||
|
||||
// === Quick Start Suggestions ===
|
||||
|
||||
export interface QuickStartSuggestion {
|
||||
icon: string;
|
||||
text: string;
|
||||
scenarios: string[]; // Which scenarios this suggestion applies to
|
||||
}
|
||||
|
||||
export const QUICK_START_SUGGESTIONS: QuickStartSuggestion[] = [
|
||||
{
|
||||
icon: '💡',
|
||||
text: '帮我写一个 Python 脚本处理 Excel 文件',
|
||||
scenarios: ['coding', 'data'],
|
||||
},
|
||||
{
|
||||
icon: '📊',
|
||||
text: '分析这个数据集的趋势和关键指标',
|
||||
scenarios: ['data', 'research'],
|
||||
},
|
||||
{
|
||||
icon: '✍️',
|
||||
text: '帮我起草一份产品需求文档',
|
||||
scenarios: ['product', 'writing'],
|
||||
},
|
||||
{
|
||||
icon: '🔍',
|
||||
text: '帮我研究一下这个技术方案的可行性',
|
||||
scenarios: ['research', 'coding'],
|
||||
},
|
||||
{
|
||||
icon: '🎨',
|
||||
text: '给我一些 UI 设计的创意建议',
|
||||
scenarios: ['design'],
|
||||
},
|
||||
{
|
||||
icon: '📝',
|
||||
text: '帮我写一篇技术博客文章',
|
||||
scenarios: ['writing'],
|
||||
},
|
||||
{
|
||||
icon: '🚀',
|
||||
text: '帮我规划一个完整的营销方案',
|
||||
scenarios: ['marketing', 'product'],
|
||||
},
|
||||
{
|
||||
icon: '⚙️',
|
||||
text: '帮我配置一个自动化部署流程',
|
||||
scenarios: ['devops', 'coding'],
|
||||
},
|
||||
];
|
||||
|
||||
// === Helper Functions ===
|
||||
|
||||
/**
|
||||
* Get personality option by ID
|
||||
*/
|
||||
export function getPersonalityById(id: string): PersonalityOption | undefined {
|
||||
return PERSONALITY_OPTIONS.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scenario tag by ID
|
||||
*/
|
||||
export function getScenarioById(id: string): ScenarioTag | undefined {
|
||||
return SCENARIO_TAGS.find((s) => s.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quick start suggestions for given scenarios
|
||||
*/
|
||||
export function getQuickStartSuggestions(scenarios: string[]): QuickStartSuggestion[] {
|
||||
if (!scenarios || scenarios.length === 0) {
|
||||
// Return first 3 general suggestions if no scenarios selected
|
||||
return QUICK_START_SUGGESTIONS.slice(0, 3);
|
||||
}
|
||||
|
||||
// Filter suggestions that match any of the selected scenarios
|
||||
const matching = QUICK_START_SUGGESTIONS.filter((s) =>
|
||||
s.scenarios.some((scenario) => scenarios.includes(scenario))
|
||||
);
|
||||
|
||||
// Return up to 3 matching suggestions, fallback to first 3 if none match
|
||||
return matching.length > 0 ? matching.slice(0, 3) : QUICK_START_SUGGESTIONS.slice(0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate welcome message based on personality and scenarios
|
||||
*/
|
||||
export function generateWelcomeMessage(config: {
|
||||
userName?: string;
|
||||
agentName: string;
|
||||
emoji?: string;
|
||||
personality?: string;
|
||||
scenarios?: string[];
|
||||
}): string {
|
||||
const { userName, agentName, emoji, personality, scenarios } = config;
|
||||
|
||||
// Build greeting
|
||||
let greeting = '';
|
||||
if (userName) {
|
||||
greeting = `你好,${userName}!`;
|
||||
} else {
|
||||
greeting = '你好!';
|
||||
}
|
||||
|
||||
// Build introduction
|
||||
let intro = `我是${emoji ? ' ' + emoji : ''} ${agentName}`;
|
||||
|
||||
// Add scenario context
|
||||
if (scenarios && scenarios.length > 0) {
|
||||
const scenarioLabels = scenarios
|
||||
.map((id) => getScenarioById(id)?.label)
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
if (scenarioLabels.length > 0) {
|
||||
intro += `,你的${scenarioLabels.join('、')}助手`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add personality touch
|
||||
if (personality) {
|
||||
const personalityOption = getPersonalityById(personality);
|
||||
if (personalityOption) {
|
||||
intro += `。我会以${personalityOption.traits[0]}的方式为你提供帮助`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add closing
|
||||
intro += '。有什么我可以帮你的吗?';
|
||||
|
||||
return `${greeting}\n\n${intro}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SOUL.md content based on personality config
|
||||
*/
|
||||
export function generateSoulContent(config: {
|
||||
agentName: string;
|
||||
emoji?: string;
|
||||
personality?: string;
|
||||
scenarios?: string[];
|
||||
communicationStyle?: string;
|
||||
}): string {
|
||||
const { agentName, emoji, personality, scenarios, communicationStyle } = config;
|
||||
|
||||
const personalityOption = personality ? getPersonalityById(personality) : undefined;
|
||||
const scenarioLabels =
|
||||
scenarios
|
||||
?.map((id) => getScenarioById(id)?.label)
|
||||
.filter(Boolean)
|
||||
.join('、') || '通用';
|
||||
|
||||
return `# ${agentName} 人格
|
||||
|
||||
> ${emoji || '🤖'} ${agentName} - ${scenarioLabels}助手
|
||||
|
||||
## 核心特质
|
||||
|
||||
${
|
||||
personalityOption
|
||||
? personalityOption.traits.map((t) => `- ${t}`).join('\n')
|
||||
: '- 高效执行\n- 专业可靠\n- 主动服务'
|
||||
}
|
||||
|
||||
## 沟通风格
|
||||
|
||||
${communicationStyle || personalityOption?.communicationStyle || '简洁、专业、友好'}
|
||||
|
||||
## 专业领域
|
||||
|
||||
${scenarioLabels}
|
||||
|
||||
## 边界
|
||||
|
||||
- 安全约束:不执行可能损害用户或系统的操作
|
||||
- 隐私保护:不主动收集或分享敏感信息
|
||||
- 能力边界:超出能力范围时坦诚告知
|
||||
|
||||
## 语气
|
||||
|
||||
- 使用中文进行交流
|
||||
- 保持专业但友好的态度
|
||||
- 适时提供额外上下文和建议
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate USER.md content based on user profile
|
||||
*/
|
||||
export function generateUserContent(config: {
|
||||
userName?: string;
|
||||
userRole?: string;
|
||||
scenarios?: string[];
|
||||
}): string {
|
||||
const { userName, userRole, scenarios } = config;
|
||||
|
||||
const scenarioLabels =
|
||||
scenarios
|
||||
?.map((id) => getScenarioById(id)?.label)
|
||||
.filter(Boolean)
|
||||
.join('、') || '通用';
|
||||
|
||||
const sections: string[] = ['# 用户档案\n'];
|
||||
|
||||
if (userName) {
|
||||
sections.push(`## 基本信息\n\n- 姓名:${userName}`);
|
||||
if (userRole) {
|
||||
sections.push(`- 角色:${userRole}`);
|
||||
}
|
||||
sections.push('');
|
||||
}
|
||||
|
||||
sections.push(`## 关注领域\n\n${scenarioLabels}\n`);
|
||||
|
||||
sections.push(`## 偏好设置\n\n- 语言:中文\n- 沟通风格:直接、高效\n`);
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
@@ -11,9 +11,8 @@
|
||||
*/
|
||||
|
||||
import { getVikingClient, type VikingHttpClient } from './viking-client';
|
||||
import { getMemoryManager, type MemoryType } from './agent-memory';
|
||||
import { getMemoryExtractor } from './memory-extractor';
|
||||
import { canAutoExecute, executeWithAutonomy } from './autonomy-manager';
|
||||
import { canAutoExecute } from './autonomy-manager';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -348,8 +347,8 @@ export class SessionPersistenceService {
|
||||
metadata: {
|
||||
startedAt: session.startedAt,
|
||||
endedAt: new Date().toISOString(),
|
||||
messageCount: session.messageCount,
|
||||
agentId: session.agentId,
|
||||
messageCount: String(session.messageCount || 0),
|
||||
agentId: session.agentId || 'default',
|
||||
},
|
||||
wait: false,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useTeamStore } from '../store/teamStore';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
import type { TeamEventMessage, TeamEventType, CollaborationEvent } from '../lib/team-client';
|
||||
import { silentErrorHandler } from './error-utils';
|
||||
|
||||
interface UseTeamEventsOptions {
|
||||
/** Subscribe to specific team only, or null for all teams */
|
||||
@@ -82,7 +83,7 @@ export function useTeamEvents(options: UseTeamEventsOptions = {}) {
|
||||
case 'member.added':
|
||||
case 'member.removed':
|
||||
// Reload teams to get updated data
|
||||
loadTeams().catch(() => {});
|
||||
loadTeams().catch(silentErrorHandler('useTeamEvents'));
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -123,14 +123,16 @@ export class VectorMemoryService {
|
||||
importance: Math.round((1 - result.score) * 10), // Invert score to importance
|
||||
createdAt: new Date().toISOString(),
|
||||
source: 'auto',
|
||||
tags: result.metadata?.tags ?? [],
|
||||
tags: (result.metadata as Record<string, unknown>)?.tags ?? [],
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
accessCount: 0,
|
||||
};
|
||||
|
||||
searchResults.push({
|
||||
memory,
|
||||
score: result.score,
|
||||
uri: result.uri,
|
||||
highlights: result.highlights,
|
||||
highlights: (result.metadata as Record<string, unknown>)?.highlights as string[] | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,8 +157,8 @@ export class VectorMemoryService {
|
||||
): Promise<VectorSearchResult[]> {
|
||||
// Get the memory content first
|
||||
const memoryManager = getMemoryManager();
|
||||
const memories = memoryManager.getByAgent(options?.agentId ?? 'default');
|
||||
const memory = memories.find(m => m.id === memoryId);
|
||||
const memories = await memoryManager.getAll(options?.agentId ?? 'default');
|
||||
const memory = memories.find((m: MemoryEntry) => m.id === memoryId);
|
||||
|
||||
if (!memory) {
|
||||
console.warn(`[VectorMemory] Memory not found: ${memoryId}`);
|
||||
@@ -192,7 +194,7 @@ export class VectorMemoryService {
|
||||
clusterCount: number = 5
|
||||
): Promise<VectorSearchResult[][]> {
|
||||
const memoryManager = getMemoryManager();
|
||||
const memories = memoryManager.getByAgent(agentId);
|
||||
const memories = await memoryManager.getAll(agentId);
|
||||
|
||||
if (memories.length === 0) {
|
||||
return [];
|
||||
|
||||
425
desktop/src/store/activeLearningStore.ts
Normal file
425
desktop/src/store/activeLearningStore.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* ActiveLearningStore - 主动学习状态管理
|
||||
*
|
||||
* 猡久学习事件和学习模式,学习建议的状态。
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import {
|
||||
type LearningEvent,
|
||||
type LearningPattern,
|
||||
type LearningSuggestion,
|
||||
type LearningEventType,
|
||||
type LearningConfig,
|
||||
} from '../types/active-learning';
|
||||
|
||||
// === Types ===
|
||||
|
||||
interface ActiveLearningState {
|
||||
events: LearningEvent[];
|
||||
patterns: LearningPattern[];
|
||||
suggestions: LearningSuggestion[];
|
||||
config: LearningConfig;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface ActiveLearningActions {
|
||||
recordEvent: (event: Omit<LearningEvent, 'id' | 'timestamp' | 'acknowledged'>) => Promise<LearningEvent>;
|
||||
recordFeedback: (agentId: string, messageId: string, feedback: string, context?: string) => Promise<LearningEvent | null>;
|
||||
acknowledgeEvent: (eventId: string) => void;
|
||||
getPatterns: (agentId: string) => LearningPattern[];
|
||||
getSuggestions: (agentId: string) => LearningSuggestion[];
|
||||
applySuggestion: (suggestionId: string) => void;
|
||||
dismissSuggestion: (suggestionId: string) => void;
|
||||
getStats: (agentId: string) => ActiveLearningStats;
|
||||
setConfig: (config: Partial<LearningConfig>) => void;
|
||||
clearEvents: (agentId: string) => void;
|
||||
exportLearningData: (agentId: string) => Promise<string>;
|
||||
importLearningData: (agentId: string, data: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface ActiveLearningStats {
|
||||
totalEvents: number;
|
||||
eventsByType: Record<LearningEventType, number>;
|
||||
totalPatterns: number;
|
||||
avgConfidence: number;
|
||||
}
|
||||
|
||||
export type ActiveLearningStore = ActiveLearningState & ActiveLearningActions;
|
||||
|
||||
const STORAGE_KEY = 'zclaw-active-learning';
|
||||
const MAX_EVENTS = 1000;
|
||||
|
||||
// === Helper Functions ===
|
||||
|
||||
function generateEventId(): string {
|
||||
return `le-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function analyzeSentiment(text: string): 'positive' | 'negative' | 'neutral' {
|
||||
const positive = ['好的', '很棒', '谢谢', '完美', 'excellent', '喜欢', '爱了', 'good', 'great', 'nice', '满意'];
|
||||
const negative = ['不好', '差', '糟糕', '错误', 'wrong', 'bad', '不喜欢', '讨厌', '问题', '失败', 'fail', 'error'];
|
||||
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
if (positive.some(w => lowerText.includes(w))) return 'positive';
|
||||
if (negative.some(w => lowerText.includes(w))) return 'negative';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function analyzeEventType(text: string): LearningEventType {
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
if (lowerText.includes('纠正') || lowerText.includes('不对') || lowerText.includes('修改')) {
|
||||
return 'correction';
|
||||
}
|
||||
if (lowerText.includes('喜欢') || lowerText.includes('偏好') || lowerText.includes('风格')) {
|
||||
return 'preference';
|
||||
}
|
||||
if (lowerText.includes('场景') || lowerText.includes('上下文') || lowerText.includes('情况')) {
|
||||
return 'context';
|
||||
}
|
||||
if (lowerText.includes('总是') || lowerText.includes('经常') || lowerText.includes('习惯')) {
|
||||
return 'behavior';
|
||||
}
|
||||
return 'feedback';
|
||||
}
|
||||
|
||||
function inferPreference(feedback: string, sentiment: string): string {
|
||||
if (sentiment === 'positive') {
|
||||
if (feedback.includes('简洁')) return '用户偏好简洁的回复';
|
||||
if (feedback.includes('详细')) return '用户偏好详细的回复';
|
||||
if (feedback.includes('快速')) return '用户偏好快速响应';
|
||||
return '用户对当前回复风格满意';
|
||||
}
|
||||
if (sentiment === 'negative') {
|
||||
if (feedback.includes('太长')) return '用户偏好更短的回复';
|
||||
if (feedback.includes('太短')) return '用户偏好更详细的回复';
|
||||
if (feedback.includes('不准确')) return '用户偏好更准确的信息';
|
||||
return '用户对当前回复风格不满意';
|
||||
}
|
||||
return '用户反馈中性';
|
||||
}
|
||||
|
||||
// === Store ===
|
||||
|
||||
export const useActiveLearningStore = create<ActiveLearningStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
events: [],
|
||||
patterns: [],
|
||||
suggestions: [],
|
||||
config: {
|
||||
enabled: true,
|
||||
minConfidence: 0.5,
|
||||
maxEvents: MAX_EVENTS,
|
||||
suggestionCooldown: 2,
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
recordEvent: async (event) => {
|
||||
const { events, config } = get();
|
||||
if (!config.enabled) throw new Error('Learning is disabled');
|
||||
|
||||
// 检查重复事件
|
||||
const existing = events.find(e =>
|
||||
e.agentId === event.agentId &&
|
||||
e.messageId === event.messageId &&
|
||||
e.type === event.type
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// 更新现有事件
|
||||
const updated = events.map(e =>
|
||||
e.id === existing.id
|
||||
? {
|
||||
...e,
|
||||
observation: e.observation + ' | ' + event.observation,
|
||||
confidence: (e.confidence + event.confidence) / 2,
|
||||
appliedCount: e.appliedCount + 1,
|
||||
}
|
||||
: e
|
||||
);
|
||||
set({ events: updated });
|
||||
return existing;
|
||||
}
|
||||
|
||||
// 创建新事件
|
||||
const newEvent: LearningEvent = {
|
||||
...event,
|
||||
id: generateEventId(),
|
||||
timestamp: Date.now(),
|
||||
acknowledged: false,
|
||||
appliedCount: 0,
|
||||
};
|
||||
|
||||
// 提取模式
|
||||
const newPatterns = extractPatterns(newEvent, get().patterns);
|
||||
const newSuggestions = generateSuggestions(newEvent, newPatterns);
|
||||
|
||||
// 保持事件数量限制
|
||||
const updatedEvents = [newEvent, ...events].slice(0, config.maxEvents);
|
||||
|
||||
set({
|
||||
events: updatedEvents,
|
||||
patterns: [...get().patterns, ...newPatterns],
|
||||
suggestions: [...get().suggestions, ...newSuggestions],
|
||||
});
|
||||
|
||||
return newEvent;
|
||||
},
|
||||
|
||||
recordFeedback: async (agentId, messageId, feedback, context) => {
|
||||
const { config } = get();
|
||||
if (!config.enabled) return null;
|
||||
|
||||
const sentiment = analyzeSentiment(feedback);
|
||||
const type = analyzeEventType(feedback);
|
||||
|
||||
return get().recordEvent({
|
||||
type,
|
||||
agentId,
|
||||
messageId,
|
||||
trigger: context || 'User feedback',
|
||||
observation: feedback,
|
||||
context,
|
||||
inferredPreference: inferPreference(feedback, sentiment),
|
||||
confidence: sentiment === 'positive' ? 0.8 : sentiment === 'negative' ? 0.5 : 0.3,
|
||||
appliedCount: 0,
|
||||
});
|
||||
},
|
||||
|
||||
acknowledgeEvent: (eventId) => {
|
||||
const { events } = get();
|
||||
set({
|
||||
events: events.map(e =>
|
||||
e.id === eventId ? { ...e, acknowledged: true } : e
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
getPatterns: (agentId) => {
|
||||
return get().patterns.filter(p => p.agentId === agentId);
|
||||
},
|
||||
|
||||
getSuggestions: (agentId) => {
|
||||
const now = Date.now();
|
||||
return get().suggestions.filter(s =>
|
||||
s.agentId === agentId &&
|
||||
!s.dismissed &&
|
||||
(!s.expiresAt || s.expiresAt.getTime() > now)
|
||||
);
|
||||
},
|
||||
|
||||
applySuggestion: (suggestionId) => {
|
||||
const { suggestions, patterns } = get();
|
||||
const suggestion = suggestions.find(s => s.id === suggestionId);
|
||||
|
||||
if (suggestion) {
|
||||
// 更新模式置信度
|
||||
const updatedPatterns = patterns.map(p =>
|
||||
p.pattern === suggestion.pattern
|
||||
? { ...p, confidence: Math.min(1, p.confidence + 0.1) }
|
||||
: p
|
||||
);
|
||||
|
||||
set({
|
||||
suggestions: suggestions.map(s =>
|
||||
s.id === suggestionId ? { ...s, dismissed: false } : s
|
||||
),
|
||||
patterns: updatedPatterns,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
dismissSuggestion: (suggestionId) => {
|
||||
const { suggestions } = get();
|
||||
set({
|
||||
suggestions: suggestions.map(s =>
|
||||
s.id === suggestionId ? { ...s, dismissed: true } : s
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
getStats: (agentId) => {
|
||||
const { events, patterns } = get();
|
||||
const agentEvents = events.filter(e => e.agentId === agentId);
|
||||
const agentPatterns = patterns.filter(p => p.agentId === agentId);
|
||||
|
||||
const eventsByType: Record<LearningEventType, number> = {
|
||||
preference: 0,
|
||||
correction: 0,
|
||||
context: 0,
|
||||
feedback: 0,
|
||||
behavior: 0,
|
||||
implicit: 0,
|
||||
};
|
||||
|
||||
for (const event of agentEvents) {
|
||||
eventsByType[event.type]++;
|
||||
}
|
||||
|
||||
return {
|
||||
totalEvents: agentEvents.length,
|
||||
eventsByType,
|
||||
totalPatterns: agentPatterns.length,
|
||||
avgConfidence: agentPatterns.length > 0
|
||||
? agentPatterns.reduce((sum, p) => sum + p.confidence, 0) / agentPatterns.length
|
||||
: 0,
|
||||
};
|
||||
},
|
||||
|
||||
setConfig: (config) => {
|
||||
set(state => ({
|
||||
config: { ...state.config, ...config },
|
||||
}));
|
||||
},
|
||||
|
||||
clearEvents: (agentId) => {
|
||||
const { events, patterns, suggestions } = get();
|
||||
set({
|
||||
events: events.filter(e => e.agentId !== agentId),
|
||||
patterns: patterns.filter(p => p.agentId !== agentId),
|
||||
suggestions: suggestions.filter(s => s.agentId !== agentId),
|
||||
});
|
||||
},
|
||||
|
||||
exportLearningData: async (agentId) => {
|
||||
const { events, patterns, config } = get();
|
||||
const data = {
|
||||
events: events.filter(e => e.agentId === agentId),
|
||||
patterns: patterns.filter(p => p.agentId === agentId),
|
||||
config,
|
||||
exportedAt: new Date().toISOString(),
|
||||
};
|
||||
return JSON.stringify(data, null, 2);
|
||||
},
|
||||
|
||||
importLearningData: async (agentId, data) => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const { events, patterns } = get();
|
||||
|
||||
// 合并导入的数据
|
||||
const mergedEvents = [
|
||||
...events,
|
||||
...parsed.events.map((e: LearningEvent) => ({
|
||||
...e,
|
||||
id: generateEventId(),
|
||||
agentId,
|
||||
})),
|
||||
].slice(0, MAX_EVENTS);
|
||||
|
||||
const mergedPatterns = [
|
||||
...patterns,
|
||||
...parsed.patterns.map((p: LearningPattern) => ({
|
||||
...p,
|
||||
agentId,
|
||||
})),
|
||||
];
|
||||
|
||||
set({
|
||||
events: mergedEvents,
|
||||
patterns: mergedPatterns,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to import learning data: ${err}`);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// === Pattern Extraction ===
|
||||
|
||||
function extractPatterns(
|
||||
event: LearningEvent,
|
||||
existingPatterns: LearningPattern[]
|
||||
): LearningPattern[] {
|
||||
const patterns: LearningPattern[] = [];
|
||||
|
||||
// 偏好模式
|
||||
if (event.observation.includes('谢谢') || event.observation.includes('好的')) {
|
||||
patterns.push({
|
||||
type: 'preference',
|
||||
pattern: 'positive_response_preference',
|
||||
description: '用户偏好正面回复风格',
|
||||
examples: [event.observation],
|
||||
confidence: 0.8,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
// 精确性模式
|
||||
if (event.type === 'correction') {
|
||||
patterns.push({
|
||||
type: 'rule',
|
||||
pattern: 'precision_preference',
|
||||
description: '用户对精确性有更高要求',
|
||||
examples: [event.observation],
|
||||
confidence: 0.9,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
// 上下文模式
|
||||
if (event.context) {
|
||||
patterns.push({
|
||||
type: 'context',
|
||||
pattern: 'context_aware',
|
||||
description: 'Agent 需要关注上下文',
|
||||
examples: [event.context],
|
||||
confidence: 0.6,
|
||||
agentId: event.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
return patterns.filter(p =>
|
||||
!existingPatterns.some(ep => ep.pattern === p.pattern && ep.agentId === p.agentId)
|
||||
);
|
||||
}
|
||||
|
||||
// === Suggestion Generation ===
|
||||
|
||||
function generateSuggestions(
|
||||
event: LearningEvent,
|
||||
patterns: LearningPattern[]
|
||||
): LearningSuggestion[] {
|
||||
const suggestions: LearningSuggestion[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const template = SUGGESTION_TEMPLATES[pattern.pattern];
|
||||
|
||||
if (template) {
|
||||
suggestions.push({
|
||||
id: `sug-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
agentId: event.agentId,
|
||||
type: pattern.type,
|
||||
pattern: pattern.pattern,
|
||||
suggestion: template,
|
||||
confidence: pattern.confidence,
|
||||
createdAt: now,
|
||||
expiresAt: new Date(now + 7 * 24 * 60 * 60 * 1000),
|
||||
dismissed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
const SUGGESTION_TEMPLATES: Record<string, string> = {
|
||||
positive_response_preference:
|
||||
'用户似乎偏好正面回复。建议在回复时保持积极和确认的语气。',
|
||||
precision_preference:
|
||||
'用户对精确性有更高要求。建议在提供信息时更加详细和准确。',
|
||||
context_aware:
|
||||
'Agent 需要关注上下文。建议在回复时考虑对话的背景和历史。',
|
||||
};
|
||||
@@ -26,6 +26,12 @@ export interface Clone {
|
||||
bootstrapReady?: boolean;
|
||||
bootstrapFiles?: Array<{ name: string; path: string; exists: boolean }>;
|
||||
updatedAt?: string;
|
||||
// 人格相关字段
|
||||
emoji?: string; // Agent emoji, e.g., "🦞", "🤖", "💻"
|
||||
personality?: string; // 人格风格: professional, friendly, creative, concise
|
||||
communicationStyle?: string; // 沟通风格描述
|
||||
notes?: string; // 用户备注
|
||||
onboardingCompleted?: boolean; // 是否完成首次引导
|
||||
}
|
||||
|
||||
export interface UsageStats {
|
||||
@@ -54,11 +60,16 @@ export interface CloneCreateOptions {
|
||||
privacyOptIn?: boolean;
|
||||
userName?: string;
|
||||
userRole?: string;
|
||||
// 人格相关字段
|
||||
emoji?: string;
|
||||
personality?: string;
|
||||
communicationStyle?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
// === Store State ===
|
||||
|
||||
interface AgentStateSlice {
|
||||
export interface AgentStateSlice {
|
||||
clones: Clone[];
|
||||
usageStats: UsageStats | null;
|
||||
pluginStatus: PluginStatus[];
|
||||
@@ -68,7 +79,7 @@ interface AgentStateSlice {
|
||||
|
||||
// === Store Actions ===
|
||||
|
||||
interface AgentActionsSlice {
|
||||
export interface AgentActionsSlice {
|
||||
loadClones: () => Promise<void>;
|
||||
createClone: (opts: CloneCreateOptions) => Promise<Clone | undefined>;
|
||||
updateClone: (id: string, updates: Partial<Clone>) => Promise<Clone | undefined>;
|
||||
|
||||
@@ -350,19 +350,12 @@ export const useChatStore = create<ChatState>()(
|
||||
const client = getGatewayClient();
|
||||
|
||||
// Try streaming first (OpenFang WebSocket)
|
||||
// Note: onDelta is empty - stream updates handled by initStreamListener to avoid duplication
|
||||
if (client.getState() === 'connected') {
|
||||
const { runId } = await client.chatStream(
|
||||
enhancedContent,
|
||||
{
|
||||
onDelta: (delta: string) => {
|
||||
set((state) => ({
|
||||
messages: state.messages.map((m) =>
|
||||
m.id === assistantId
|
||||
? { ...m, content: m.content + delta }
|
||||
: m
|
||||
),
|
||||
}));
|
||||
},
|
||||
onDelta: () => { /* Handled by initStreamListener to prevent duplication */ },
|
||||
onTool: (tool: string, input: string, output: string) => {
|
||||
const toolMsg: Message = {
|
||||
id: `tool_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
@@ -395,7 +388,7 @@ export const useChatStore = create<ChatState>()(
|
||||
set((state) => ({
|
||||
isStreaming: false,
|
||||
messages: state.messages.map((m) =>
|
||||
m.id === assistantId ? { ...m, streaming: false } : m
|
||||
m.id === assistantId ? { ...m, streaming: false, runId } : m
|
||||
),
|
||||
}));
|
||||
// Async memory extraction after stream completes
|
||||
@@ -634,6 +627,8 @@ export const useChatStore = create<ChatState>()(
|
||||
partialize: (state) => ({
|
||||
conversations: state.conversations,
|
||||
currentModel: state.currentModel,
|
||||
messages: state.messages,
|
||||
currentConversationId: state.currentConversationId,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// Rehydrate Date objects from JSON strings
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import type { GatewayModelChoice } from '../lib/gateway-config';
|
||||
import type { GatewayClient } from '../lib/gateway-client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -121,10 +122,9 @@ export interface ConfigStoreClient {
|
||||
getFeishuStatus(): Promise<{ configured?: boolean; accounts?: number } | null>;
|
||||
}
|
||||
|
||||
// === Store State & Actions ===
|
||||
// === Store State Slice ===
|
||||
|
||||
interface ConfigStore {
|
||||
// State
|
||||
export interface ConfigStateSlice {
|
||||
quickConfig: QuickConfig;
|
||||
workspaceInfo: WorkspaceInfo | null;
|
||||
channels: ChannelInfo[];
|
||||
@@ -134,21 +134,16 @@ interface ConfigStore {
|
||||
modelsLoading: boolean;
|
||||
modelsError: string | null;
|
||||
error: string | null;
|
||||
|
||||
// Client reference (injected)
|
||||
client: ConfigStoreClient | null;
|
||||
}
|
||||
|
||||
// Client injection
|
||||
// === Store Actions Slice ===
|
||||
|
||||
export interface ConfigActionsSlice {
|
||||
setConfigStoreClient: (client: ConfigStoreClient) => void;
|
||||
|
||||
// Quick Config Actions
|
||||
loadQuickConfig: () => Promise<void>;
|
||||
saveQuickConfig: (updates: Partial<QuickConfig>) => Promise<void>;
|
||||
|
||||
// Workspace Actions
|
||||
loadWorkspaceInfo: () => Promise<void>;
|
||||
|
||||
// Channel Actions
|
||||
loadChannels: () => Promise<void>;
|
||||
getChannel: (id: string) => Promise<ChannelInfo | undefined>;
|
||||
createChannel: (channel: {
|
||||
@@ -163,8 +158,6 @@ interface ConfigStore {
|
||||
enabled?: boolean;
|
||||
}) => Promise<ChannelInfo | undefined>;
|
||||
deleteChannel: (id: string) => Promise<void>;
|
||||
|
||||
// Scheduled Task Actions
|
||||
loadScheduledTasks: () => Promise<void>;
|
||||
createScheduledTask: (task: {
|
||||
name: string;
|
||||
@@ -177,8 +170,6 @@ interface ConfigStore {
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
}) => Promise<ScheduledTask | undefined>;
|
||||
|
||||
// Skill Actions
|
||||
loadSkillsCatalog: () => Promise<void>;
|
||||
getSkill: (id: string) => Promise<SkillInfo | undefined>;
|
||||
createSkill: (skill: {
|
||||
@@ -196,15 +187,15 @@ interface ConfigStore {
|
||||
enabled?: boolean;
|
||||
}) => Promise<SkillInfo | undefined>;
|
||||
deleteSkill: (id: string) => Promise<void>;
|
||||
|
||||
// Model Actions
|
||||
loadModels: () => Promise<void>;
|
||||
|
||||
// Utility
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useConfigStore = create<ConfigStore>((set, get) => ({
|
||||
// === Combined Store Type ===
|
||||
|
||||
export type ConfigStore = ConfigStateSlice & ConfigActionsSlice;
|
||||
|
||||
export const useConfigStore = create<ConfigStateSlice & ConfigActionsSlice>((set, get) => ({
|
||||
// Initial State
|
||||
quickConfig: {},
|
||||
workspaceInfo: null,
|
||||
@@ -535,3 +526,47 @@ export type {
|
||||
ScheduledTask as ScheduledTaskType,
|
||||
SkillInfo as SkillInfoType,
|
||||
};
|
||||
|
||||
// === Client Injection ===
|
||||
|
||||
/**
|
||||
* Helper to create a ConfigStoreClient adapter from a GatewayClient.
|
||||
*/
|
||||
function createConfigClientFromGateway(client: GatewayClient): ConfigStoreClient {
|
||||
return {
|
||||
getWorkspaceInfo: () => client.getWorkspaceInfo(),
|
||||
getQuickConfig: () => client.getQuickConfig(),
|
||||
saveQuickConfig: (config) => client.saveQuickConfig(config),
|
||||
listSkills: () => client.listSkills(),
|
||||
getSkill: (id) => client.getSkill(id),
|
||||
createSkill: (skill) => client.createSkill(skill),
|
||||
updateSkill: (id, updates) => client.updateSkill(id, updates),
|
||||
deleteSkill: (id) => client.deleteSkill(id),
|
||||
listChannels: () => client.listChannels(),
|
||||
getChannel: (id) => client.getChannel(id),
|
||||
createChannel: (channel) => client.createChannel(channel),
|
||||
updateChannel: (id, updates) => client.updateChannel(id, updates),
|
||||
deleteChannel: (id) => client.deleteChannel(id),
|
||||
listScheduledTasks: () => client.listScheduledTasks(),
|
||||
createScheduledTask: async (task) => {
|
||||
const result = await client.createScheduledTask(task);
|
||||
return {
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
schedule: result.schedule,
|
||||
status: result.status as 'active' | 'paused' | 'completed' | 'error',
|
||||
};
|
||||
},
|
||||
listModels: () => client.listModels(),
|
||||
getFeishuStatus: () => client.getFeishuStatus(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client for the config store.
|
||||
* Called by the coordinator during initialization.
|
||||
*/
|
||||
export function setConfigStoreClient(client: unknown): void {
|
||||
const configClient = createConfigClientFromGateway(client as GatewayClient);
|
||||
useConfigStore.getState().setConfigStoreClient(configClient);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import {
|
||||
isTauriRuntime,
|
||||
prepareLocalGatewayForTauri,
|
||||
getLocalGatewayStatus,
|
||||
getLocalGatewayStatus as fetchLocalGatewayStatus,
|
||||
startLocalGateway as startLocalGatewayCommand,
|
||||
stopLocalGateway as stopLocalGatewayCommand,
|
||||
restartLocalGateway as restartLocalGatewayCommand,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
getUnsupportedLocalGatewayStatus,
|
||||
type LocalGatewayStatus,
|
||||
} from '../lib/tauri-gateway';
|
||||
import { useConfigStore } from './configStore';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -59,18 +60,6 @@ function requiresLocalDevicePairing(error: unknown): boolean {
|
||||
return message.includes('pairing required');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate security level based on enabled layer count.
|
||||
*/
|
||||
function calculateSecurityLevel(enabledCount: number, totalCount: number): 'critical' | 'high' | 'medium' | 'low' {
|
||||
if (totalCount === 0) return 'low';
|
||||
const ratio = enabledCount / totalCount;
|
||||
if (ratio >= 0.875) return 'critical'; // 14-16 layers
|
||||
if (ratio >= 0.625) return 'high'; // 10-13 layers
|
||||
if (ratio >= 0.375) return 'medium'; // 6-9 layers
|
||||
return 'low'; // 0-5 layers
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL is a loopback address.
|
||||
*/
|
||||
@@ -187,7 +176,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
// Check local gateway first if in Tauri
|
||||
if (isTauriRuntime()) {
|
||||
try {
|
||||
const localStatus = await getLocalGatewayStatus();
|
||||
const localStatus = await fetchLocalGatewayStatus();
|
||||
const localUrl = getLocalGatewayConnectUrl(localStatus);
|
||||
if (localUrl) {
|
||||
candidates.push(localUrl);
|
||||
@@ -198,7 +187,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
}
|
||||
|
||||
// Add quick config gateway URL if available
|
||||
const quickConfigGatewayUrl = get().quickConfig?.gatewayUrl?.trim();
|
||||
const quickConfigGatewayUrl = useConfigStore.getState().quickConfig?.gatewayUrl?.trim();
|
||||
if (quickConfigGatewayUrl) {
|
||||
candidates.push(quickConfigGatewayUrl);
|
||||
}
|
||||
@@ -233,7 +222,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
}
|
||||
|
||||
// Resolve effective token: param > quickConfig > localStorage > local auth
|
||||
let effectiveToken = token || get().quickConfig?.gatewayToken || getStoredGatewayToken();
|
||||
let effectiveToken = token || useConfigStore.getState().quickConfig?.gatewayToken || getStoredGatewayToken();
|
||||
if (!effectiveToken && isTauriRuntime()) {
|
||||
try {
|
||||
const localAuth = await getLocalGatewayAuth();
|
||||
@@ -246,7 +235,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ConnectionStore] Connecting with token:', effectiveToken ? `${effectiveToken.substring(0, 8)}...` : '(empty)');
|
||||
console.log('[ConnectionStore] Connecting with token:', effectiveToken ? '[REDACTED]' : '(empty)');
|
||||
|
||||
const candidateUrls = await resolveCandidates();
|
||||
let lastError: unknown = null;
|
||||
@@ -327,7 +316,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
|
||||
set({ localGatewayBusy: true });
|
||||
try {
|
||||
const status = await getLocalGatewayStatus();
|
||||
const status = await fetchLocalGatewayStatus();
|
||||
set({ localGateway: status, localGatewayBusy: false });
|
||||
return status;
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -27,6 +27,12 @@ interface Clone {
|
||||
bootstrapReady?: boolean;
|
||||
bootstrapFiles?: Array<{ name: string; path: string; exists: boolean }>;
|
||||
updatedAt?: string;
|
||||
// 人格相关字段
|
||||
emoji?: string; // Agent emoji, e.g., "🦞", "🤖", "💻"
|
||||
personality?: string; // 人格风格: professional, friendly, creative, concise
|
||||
communicationStyle?: string; // 沟通风格描述
|
||||
notes?: string; // 用户备注
|
||||
onboardingCompleted?: boolean; // 是否完成首次引导
|
||||
}
|
||||
|
||||
interface UsageStats {
|
||||
@@ -93,6 +99,11 @@ interface QuickConfig {
|
||||
autoSaveContext?: boolean;
|
||||
fileWatching?: boolean;
|
||||
privacyOptIn?: boolean;
|
||||
// 人格相关字段
|
||||
emoji?: string;
|
||||
personality?: string;
|
||||
communicationStyle?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
interface WorkspaceInfo {
|
||||
@@ -746,7 +757,8 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
loadClones: async () => {
|
||||
try {
|
||||
const result = await get().client.listClones();
|
||||
const clones = result?.clones || result?.agents || [];
|
||||
// API 可能返回数组,也可能返回 {clones: [...]} 或 {agents: [...]}
|
||||
const clones = Array.isArray(result) ? result : (result?.clones || result?.agents || []);
|
||||
set({ clones });
|
||||
useChatStore.getState().syncAgents(clones);
|
||||
|
||||
@@ -1221,7 +1233,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
try {
|
||||
const result = await get().client.listHandRuns(name, opts);
|
||||
const runs: HandRun[] = (result?.runs || []).map((r: RawHandRun) => ({
|
||||
runId: r.runId || r.run_id || r.id,
|
||||
runId: r.runId || r.run_id || r.id || '',
|
||||
status: r.status || 'unknown',
|
||||
startedAt: r.startedAt || r.started_at || r.created_at || new Date().toISOString(),
|
||||
completedAt: r.completedAt || r.completed_at || r.finished_at,
|
||||
@@ -1486,15 +1498,15 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
|
||||
try {
|
||||
const result = await get().client.listApprovals(status);
|
||||
const approvals: Approval[] = (result?.approvals || []).map((a: RawApproval) => ({
|
||||
id: a.id || a.approval_id,
|
||||
handName: a.hand_name || a.handName,
|
||||
runId: a.run_id || a.runId,
|
||||
id: a.id || a.approval_id || '',
|
||||
handName: a.hand_name || a.handName || '',
|
||||
runId: a.run_id || a.runId || '',
|
||||
status: a.status || 'pending',
|
||||
requestedAt: a.requested_at || a.requestedAt || new Date().toISOString(),
|
||||
requestedBy: a.requested_by || a.requestedBy,
|
||||
reason: a.reason || a.description,
|
||||
requestedBy: a.requested_by || a.requestedBy || '',
|
||||
reason: a.reason || a.description || '',
|
||||
action: a.action || 'execute',
|
||||
params: a.params,
|
||||
params: a.params || {},
|
||||
respondedAt: a.responded_at || a.respondedAt,
|
||||
respondedBy: a.responded_by || a.respondedBy,
|
||||
responseReason: a.response_reason || a.responseReason,
|
||||
|
||||
@@ -65,6 +65,17 @@ export interface Approval {
|
||||
responseReason?: string;
|
||||
}
|
||||
|
||||
// === Trigger Create Options ===
|
||||
|
||||
export interface TriggerCreateOptions {
|
||||
type: string;
|
||||
name?: string;
|
||||
enabled?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
handName?: string;
|
||||
workflowId?: string;
|
||||
}
|
||||
|
||||
// === Raw API Response Types (for mapping) ===
|
||||
|
||||
interface RawHandRequirement {
|
||||
@@ -129,30 +140,32 @@ interface HandClient {
|
||||
getHand: (name: string) => Promise<Record<string, unknown> | null>;
|
||||
listHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise<{ runs?: RawHandRun[] } | null>;
|
||||
triggerHand: (name: string, params?: Record<string, unknown>) => Promise<{ runId?: string; status?: string } | null>;
|
||||
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
cancelHand: (name: string, runId: string) => Promise<void>;
|
||||
approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise<{ status: string }>;
|
||||
cancelHand: (name: string, runId: string) => Promise<{ status: string }>;
|
||||
listTriggers: () => Promise<{ triggers?: Trigger[] } | null>;
|
||||
getTrigger: (id: string) => Promise<Trigger | null>;
|
||||
createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<{ id?: string } | null>;
|
||||
updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<void>;
|
||||
deleteTrigger: (id: string) => Promise<void>;
|
||||
updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<{ id: string }>;
|
||||
deleteTrigger: (id: string) => Promise<{ status: string }>;
|
||||
listApprovals: (status?: ApprovalStatus) => Promise<{ approvals?: RawApproval[] } | null>;
|
||||
respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<void>;
|
||||
respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise<{ status: string }>;
|
||||
}
|
||||
|
||||
interface HandStore {
|
||||
// State
|
||||
// === Store State Slice ===
|
||||
|
||||
export interface HandStateSlice {
|
||||
hands: Hand[];
|
||||
handRuns: Record<string, HandRun[]>;
|
||||
triggers: Trigger[];
|
||||
approvals: Approval[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Client reference (injected via setHandStoreClient)
|
||||
client: HandClient | null;
|
||||
}
|
||||
|
||||
// Actions
|
||||
// === Store Actions Slice ===
|
||||
|
||||
export interface HandActionsSlice {
|
||||
setHandStoreClient: (client: HandClient) => void;
|
||||
loadHands: () => Promise<void>;
|
||||
getHandDetails: (name: string) => Promise<Hand | undefined>;
|
||||
@@ -162,7 +175,7 @@ interface HandStore {
|
||||
cancelHand: (name: string, runId: string) => Promise<void>;
|
||||
loadTriggers: () => Promise<void>;
|
||||
getTrigger: (id: string) => Promise<Trigger | undefined>;
|
||||
createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<Trigger | undefined>;
|
||||
createTrigger: (trigger: TriggerCreateOptions) => Promise<Trigger | undefined>;
|
||||
updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record<string, unknown>; handName?: string; workflowId?: string }) => Promise<Trigger | undefined>;
|
||||
deleteTrigger: (id: string) => Promise<void>;
|
||||
loadApprovals: (status?: ApprovalStatus) => Promise<void>;
|
||||
@@ -170,6 +183,10 @@ interface HandStore {
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
// === Combined Store Type ===
|
||||
|
||||
export type HandStore = HandStateSlice & HandActionsSlice;
|
||||
|
||||
export const useHandStore = create<HandStore>((set, get) => ({
|
||||
// Initial State
|
||||
hands: [],
|
||||
@@ -383,7 +400,7 @@ export const useHandStore = create<HandStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
createTrigger: async (trigger) => {
|
||||
createTrigger: async (trigger: TriggerCreateOptions) => {
|
||||
const client = get().client;
|
||||
if (!client) return undefined;
|
||||
|
||||
@@ -496,3 +513,14 @@ export function createHandClientFromGateway(client: GatewayClient): HandClient {
|
||||
respondToApproval: (approvalId, approved, reason) => client.respondToApproval(approvalId, approved, reason),
|
||||
};
|
||||
}
|
||||
|
||||
// === Client Injection ===
|
||||
|
||||
/**
|
||||
* Sets the client for the hand store.
|
||||
* Called by the coordinator during initialization.
|
||||
*/
|
||||
export function setHandStoreClient(client: unknown): void {
|
||||
const handClient = createHandClientFromGateway(client as GatewayClient);
|
||||
useHandStore.getState().setHandStoreClient(handClient);
|
||||
}
|
||||
|
||||
@@ -26,15 +26,21 @@ export type { WorkflowStore, WorkflowStateSlice, WorkflowActionsSlice, Workflow,
|
||||
export { useConfigStore, setConfigStoreClient } from './configStore';
|
||||
export type { ConfigStore, ConfigStateSlice, ConfigActionsSlice, QuickConfig, WorkspaceInfo, ChannelInfo, ScheduledTask, SkillInfo } from './configStore';
|
||||
|
||||
// === New Stores ===
|
||||
export { useMemoryGraphStore } from './memoryGraphStore';
|
||||
export type { MemoryGraphStore, GraphNode, GraphEdge, GraphFilter, GraphLayout } from './memoryGraphStore';
|
||||
|
||||
export { useActiveLearningStore } from './activeLearningStore';
|
||||
export type { ActiveLearningStore } from './activeLearningStore';
|
||||
|
||||
// === Composite Store Hook ===
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useConnectionStore, getClient } from './connectionStore';
|
||||
import { useAgentStore, setAgentStoreClient } from './agentStore';
|
||||
import { useHandStore, setHandStoreClient } from './handStore';
|
||||
import { useWorkflowStore, setWorkflowStoreClient } from './workflowStore';
|
||||
import { useConfigStore, setConfigStoreClient } from './configStore';
|
||||
import type { GatewayClient } from '../lib/gateway-client';
|
||||
|
||||
/**
|
||||
* Initialize all stores with the shared client.
|
||||
@@ -113,7 +119,7 @@ export function useCompositeStore() {
|
||||
const createTrigger = useHandStore((s) => s.createTrigger);
|
||||
const deleteTrigger = useHandStore((s) => s.deleteTrigger);
|
||||
const loadApprovals = useHandStore((s) => s.loadApprovals);
|
||||
const approveRequest = useHandStore((s) => s.approveRequest);
|
||||
const respondToApproval = useHandStore((s) => s.respondToApproval);
|
||||
|
||||
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
|
||||
const getWorkflow = useWorkflowStore((s) => s.getWorkflow);
|
||||
@@ -203,7 +209,7 @@ export function useCompositeStore() {
|
||||
createTrigger,
|
||||
deleteTrigger,
|
||||
loadApprovals,
|
||||
approveRequest,
|
||||
respondToApproval,
|
||||
|
||||
// Workflow actions
|
||||
loadWorkflows,
|
||||
@@ -244,7 +250,7 @@ export function useCompositeStore() {
|
||||
quickConfig, workspaceInfo, channels, scheduledTasks, skillsCatalog, models, modelsLoading, modelsError,
|
||||
connect, disconnect, clearLogs, refreshLocalGateway, startLocalGateway, stopLocalGateway, restartLocalGateway,
|
||||
loadClones, createClone, updateClone, deleteClone, loadUsageStats, loadPluginStatus,
|
||||
loadHands, getHandDetails, triggerHand, loadHandRuns, loadTriggers, createTrigger, deleteTrigger, loadApprovals, approveRequest,
|
||||
loadHands, getHandDetails, triggerHand, loadHandRuns, loadTriggers, createTrigger, deleteTrigger, loadApprovals, respondToApproval,
|
||||
loadWorkflows, getWorkflow, createWorkflow, updateWorkflow, deleteWorkflow, triggerWorkflow, loadWorkflowRuns,
|
||||
loadQuickConfig, saveQuickConfig, loadWorkspaceInfo, loadChannels, getChannel, createChannel, updateChannel, deleteChannel,
|
||||
loadScheduledTasks, createScheduledTask, loadSkillsCatalog, getSkill, createSkill, updateSkill, deleteSkill, loadModels,
|
||||
|
||||
316
desktop/src/store/memoryGraphStore.ts
Normal file
316
desktop/src/store/memoryGraphStore.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* MemoryGraphStore - 记忆图谱状态管理
|
||||
*
|
||||
* 管理记忆图谱可视化的状态,包括节点、边、布局和交互。
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { getMemoryManager, type MemoryEntry, type MemoryType } from '../lib/agent-memory';
|
||||
|
||||
export type { MemoryType };
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
type: MemoryType;
|
||||
label: string;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
importance: number;
|
||||
accessCount: number;
|
||||
createdAt: string;
|
||||
isHighlighted: boolean;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
type: 'reference' | 'related' | 'derived';
|
||||
strength: number;
|
||||
}
|
||||
|
||||
export interface GraphFilter {
|
||||
types: MemoryType[];
|
||||
minImportance: number;
|
||||
dateRange: {
|
||||
start?: string;
|
||||
end?: string;
|
||||
};
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export interface GraphLayout {
|
||||
width: number;
|
||||
height: number;
|
||||
zoom: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}
|
||||
|
||||
interface MemoryGraphState {
|
||||
nodes: GraphNode[];
|
||||
edges: GraphEdge[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
filter: GraphFilter;
|
||||
layout: GraphLayout;
|
||||
selectedNodeId: string | null;
|
||||
hoveredNodeId: string | null;
|
||||
showLabels: boolean;
|
||||
simulationRunning: boolean;
|
||||
}
|
||||
|
||||
interface MemoryGraphActions {
|
||||
loadGraph: (agentId: string) => Promise<void>;
|
||||
setFilter: (filter: Partial<GraphFilter>) => void;
|
||||
resetFilter: () => void;
|
||||
setLayout: (layout: Partial<GraphLayout>) => void;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
hoverNode: (nodeId: string | null) => void;
|
||||
toggleLabels: () => void;
|
||||
startSimulation: () => void;
|
||||
stopSimulation: () => void;
|
||||
updateNodePositions: (updates: Array<{ id: string; x: number; y: number }>) => void;
|
||||
highlightSearch: (query: string) => void;
|
||||
clearHighlight: () => void;
|
||||
exportAsImage: () => Promise<Blob | null>;
|
||||
getFilteredNodes: () => GraphNode[];
|
||||
getFilteredEdges: () => GraphEdge[];
|
||||
}
|
||||
|
||||
const DEFAULT_FILTER: GraphFilter = {
|
||||
types: ['fact', 'preference', 'lesson', 'context', 'task'],
|
||||
minImportance: 0,
|
||||
dateRange: {},
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
const DEFAULT_LAYOUT: GraphLayout = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
zoom: 1,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
|
||||
export type MemoryGraphStore = MemoryGraphState & MemoryGraphActions;
|
||||
|
||||
// === Helper Functions ===
|
||||
|
||||
function memoryToNode(memory: MemoryEntry, index: number, total: number): GraphNode {
|
||||
// 使用圆形布局初始位置
|
||||
const angle = (index / total) * 2 * Math.PI;
|
||||
const radius = 200;
|
||||
|
||||
return {
|
||||
id: memory.id,
|
||||
type: memory.type,
|
||||
label: memory.content.slice(0, 50) + (memory.content.length > 50 ? '...' : ''),
|
||||
x: 400 + radius * Math.cos(angle),
|
||||
y: 300 + radius * Math.sin(angle),
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
importance: memory.importance,
|
||||
accessCount: memory.accessCount,
|
||||
createdAt: memory.createdAt,
|
||||
isHighlighted: false,
|
||||
isSelected: false,
|
||||
};
|
||||
}
|
||||
|
||||
function findRelatedMemories(memories: MemoryEntry[]): GraphEdge[] {
|
||||
const edges: GraphEdge[] = [];
|
||||
|
||||
// 简单的关联算法:基于共同标签和关键词
|
||||
for (let i = 0; i < memories.length; i++) {
|
||||
for (let j = i + 1; j < memories.length; j++) {
|
||||
const m1 = memories[i];
|
||||
const m2 = memories[j];
|
||||
|
||||
// 检查共同标签
|
||||
const commonTags = m1.tags.filter(t => m2.tags.includes(t));
|
||||
if (commonTags.length > 0) {
|
||||
edges.push({
|
||||
id: `edge-${m1.id}-${m2.id}`,
|
||||
source: m1.id,
|
||||
target: m2.id,
|
||||
type: 'related',
|
||||
strength: commonTags.length * 0.3,
|
||||
});
|
||||
}
|
||||
|
||||
// 同类型记忆关联
|
||||
if (m1.type === m2.type) {
|
||||
const existingEdge = edges.find(
|
||||
e => e.source === m1.id && e.target === m2.id
|
||||
);
|
||||
if (!existingEdge) {
|
||||
edges.push({
|
||||
id: `edge-${m1.id}-${m2.id}-type`,
|
||||
source: m1.id,
|
||||
target: m2.id,
|
||||
type: 'derived',
|
||||
strength: 0.1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
export const useMemoryGraphStore = create<MemoryGraphStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
filter: DEFAULT_FILTER,
|
||||
layout: DEFAULT_LAYOUT,
|
||||
selectedNodeId: null,
|
||||
hoveredNodeId: null,
|
||||
showLabels: true,
|
||||
simulationRunning: false,
|
||||
|
||||
loadGraph: async (agentId: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const mgr = getMemoryManager();
|
||||
const memories = await mgr.getAll(agentId, { limit: 200 });
|
||||
|
||||
const nodes = memories.map((m, i) => memoryToNode(m, i, memories.length));
|
||||
const edges = findRelatedMemories(memories);
|
||||
|
||||
set({
|
||||
nodes,
|
||||
edges,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : '加载图谱失败',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setFilter: (filter) => {
|
||||
set(state => ({
|
||||
filter: { ...state.filter, ...filter },
|
||||
}));
|
||||
},
|
||||
|
||||
resetFilter: () => {
|
||||
set({ filter: DEFAULT_FILTER });
|
||||
},
|
||||
|
||||
setLayout: (layout) => {
|
||||
set(state => ({
|
||||
layout: { ...state.layout, ...layout },
|
||||
}));
|
||||
},
|
||||
|
||||
selectNode: (nodeId) => {
|
||||
set(state => ({
|
||||
selectedNodeId: nodeId,
|
||||
nodes: state.nodes.map(n => ({
|
||||
...n,
|
||||
isSelected: n.id === nodeId,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
hoverNode: (nodeId) => {
|
||||
set(state => ({
|
||||
hoveredNodeId: nodeId,
|
||||
nodes: state.nodes.map(n => ({
|
||||
...n,
|
||||
isHighlighted: nodeId ? n.id === nodeId : n.isHighlighted,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
toggleLabels: () => {
|
||||
set(state => ({ showLabels: !state.showLabels }));
|
||||
},
|
||||
|
||||
startSimulation: () => {
|
||||
set({ simulationRunning: true });
|
||||
},
|
||||
|
||||
stopSimulation: () => {
|
||||
set({ simulationRunning: false });
|
||||
},
|
||||
|
||||
updateNodePositions: (updates) => {
|
||||
set(state => ({
|
||||
nodes: state.nodes.map(node => {
|
||||
const update = updates.find(u => u.id === node.id);
|
||||
return update ? { ...node, x: update.x, y: update.y } : node;
|
||||
}),
|
||||
}));
|
||||
},
|
||||
|
||||
highlightSearch: (query) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
set(state => ({
|
||||
filter: { ...state.filter, searchQuery: query },
|
||||
nodes: state.nodes.map(n => ({
|
||||
...n,
|
||||
isHighlighted: query ? n.label.toLowerCase().includes(lowerQuery) : false,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
clearHighlight: () => {
|
||||
set(state => ({
|
||||
nodes: state.nodes.map(n => ({ ...n, isHighlighted: false })),
|
||||
}));
|
||||
},
|
||||
|
||||
exportAsImage: async () => {
|
||||
// SVG 导出逻辑在组件中实现
|
||||
return null;
|
||||
},
|
||||
|
||||
getFilteredNodes: () => {
|
||||
const { nodes, filter } = get();
|
||||
return nodes.filter(n => {
|
||||
if (!filter.types.includes(n.type)) return false;
|
||||
if (n.importance < filter.minImportance) return false;
|
||||
if (filter.dateRange.start && n.createdAt < filter.dateRange.start) return false;
|
||||
if (filter.dateRange.end && n.createdAt > filter.dateRange.end) return false;
|
||||
if (filter.searchQuery) {
|
||||
return n.label.toLowerCase().includes(filter.searchQuery.toLowerCase());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
getFilteredEdges: () => {
|
||||
const { edges } = get();
|
||||
const filteredNodes = get().getFilteredNodes();
|
||||
const nodeIds = new Set(filteredNodes.map(n => n.id));
|
||||
|
||||
return edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'zclaw-memory-graph',
|
||||
partialize: (state) => ({
|
||||
filter: state.filter,
|
||||
layout: state.layout,
|
||||
showLabels: state.showLabels,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
411
desktop/src/store/skillMarketStore.ts
Normal file
411
desktop/src/store/skillMarketStore.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* * skillMarketStore.ts - 技能市场状态管理
|
||||
*
|
||||
* * 猛攻状态管理技能浏览、搜索、安装/卸载等功能
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { Skill, SkillReview, SkillMarketState } from '../types/skill-market';
|
||||
|
||||
// === 存储键 ===
|
||||
const STORAGE_KEY = 'zclaw-skill-market';
|
||||
const INSTALLED_KEY = 'zclaw-installed-skills';
|
||||
|
||||
// === 默认状态 ===
|
||||
const initialState: SkillMarketState = {
|
||||
skills: [],
|
||||
installedSkills: [],
|
||||
searchResults: [],
|
||||
selectedSkill: null,
|
||||
searchQuery: '',
|
||||
categoryFilter: 'all',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// === Store 定义 ===
|
||||
interface SkillMarketActions {
|
||||
// 技能加载
|
||||
loadSkills: () => Promise<void>;
|
||||
// 技能搜索
|
||||
searchSkills: (query: string) => void;
|
||||
// 分类过滤
|
||||
filterByCategory: (category: string) => void;
|
||||
// 选择技能
|
||||
selectSkill: (skill: Skill | null) => void;
|
||||
// 安装技能
|
||||
installSkill: (skillId: string) => Promise<boolean>;
|
||||
// 卸载技能
|
||||
uninstallSkill: (skillId: string) => Promise<boolean>;
|
||||
// 获取技能详情
|
||||
getSkillDetails: (skillId: string) => Promise<Skill | null>;
|
||||
// 加载评论
|
||||
loadReviews: (skillId: string) => Promise<SkillReview[]>;
|
||||
// 添加评论
|
||||
addReview: (skillId: string, review: Omit<SkillReview, 'id' | 'skillId' | 'createdAt'>) => Promise<void>;
|
||||
// 刷新技能列表
|
||||
refreshSkills: () => Promise<void>;
|
||||
// 清除错误
|
||||
clearError: () => void;
|
||||
// 重置状态
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// === Store 创建 ===
|
||||
export const useSkillMarketStore = create<SkillMarketState & SkillMarketActions>()(
|
||||
persist({
|
||||
key: STORAGE_KEY,
|
||||
storage: localStorage,
|
||||
partialize: (state) => ({
|
||||
installedSkills: state.installedSkills,
|
||||
categoryFilter: state.categoryFilter,
|
||||
}),
|
||||
}),
|
||||
initialState,
|
||||
{
|
||||
// === 技能加载 ===
|
||||
loadSkills: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
// 扫描 skills 目录获取可用技能
|
||||
const skills = await scanSkillsDirectory();
|
||||
// 从 localStorage 恢复安装状态
|
||||
const stored = localStorage.getItem(INSTALLED_KEY);
|
||||
const installedSkills: string[] = stored ? JSON.parse(stored) : [];
|
||||
// 更新技能的安装状态
|
||||
const updatedSkills = skills.map(skill => ({
|
||||
...skill,
|
||||
installed: installedSkills.includes(skill.id),
|
||||
})));
|
||||
set({
|
||||
skills: updatedSkills,
|
||||
installedSkills,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: err instanceof Error ? err.message : '加载技能失败',
|
||||
});
|
||||
}
|
||||
},
|
||||
// === 技能搜索 ===
|
||||
searchSkills: (query: string) => {
|
||||
const { skills } = get();
|
||||
set({ searchQuery: query });
|
||||
if (!query.trim()) {
|
||||
set({ searchResults: [] });
|
||||
return;
|
||||
}
|
||||
const queryLower = query.toLowerCase();
|
||||
const results = skills.filter(skill => {
|
||||
return (
|
||||
skill.name.toLowerCase().includes(queryLower) ||
|
||||
skill.description.toLowerCase().includes(queryLower) ||
|
||||
skill.triggers.some(t => t.toLowerCase().includes(queryLower)) ||
|
||||
skill.capabilities.some(c => c.toLowerCase().includes(queryLower)) ||
|
||||
skill.tags?.some(t => t.toLowerCase().includes(queryLower))
|
||||
);
|
||||
});
|
||||
set({ searchResults: results });
|
||||
},
|
||||
// === 分类过滤 ===
|
||||
filterByCategory: (category: string) => {
|
||||
set({ categoryFilter: category });
|
||||
},
|
||||
// === 选择技能 ===
|
||||
selectSkill: (skill: Skill | null) => {
|
||||
set({ selectedSkill: skill });
|
||||
},
|
||||
// === 安装技能 ===
|
||||
installSkill: async (skillId: string) => {
|
||||
const { skills, installedSkills } = get();
|
||||
const skill = skills.find(s => s.id === skillId);
|
||||
if (!skill) return false;
|
||||
try {
|
||||
// 更新安装状态
|
||||
const newInstalledSkills = [...installedSkills, skillId];
|
||||
const updatedSkills = skills.map(s => ({
|
||||
...s,
|
||||
installed: s.id === skillId ? true : s.installed,
|
||||
installedAt: s.id === skillId ? new Date().toISOString() : s.installedAt,
|
||||
}));
|
||||
// 持久化安装列表
|
||||
localStorage.setItem(INSTALLED_KEY, JSON.stringify(newInstalledSkills));
|
||||
set({
|
||||
skills: updatedSkills,
|
||||
installedSkills: newInstalledSkills,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
set({
|
||||
error: err instanceof Error ? err.message : '安装技能失败',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// === 卸载技能 ===
|
||||
uninstallSkill: async (skillId: string) => {
|
||||
const { skills, installedSkills } = get();
|
||||
try {
|
||||
// 更新安装状态
|
||||
const newInstalledSkills = installedSkills.filter(id => id !== skillId);
|
||||
const updatedSkills = skills.map(s => ({
|
||||
...s,
|
||||
installed: s.id === skillId ? false : s.installed,
|
||||
installedAt: s.id === skillId ? undefined : s.installedAt,
|
||||
}));
|
||||
// 持久化安装列表
|
||||
localStorage.setItem(INSTALLED_KEY, JSON.stringify(newInstalledSkills));
|
||||
set({
|
||||
skills: updatedSkills,
|
||||
installedSkills: newInstalledSkills,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
set({
|
||||
error: err instanceof Error ? err.message : '卸载技能失败',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// === 获取技能详情 ===
|
||||
getSkillDetails: async (skillId: string) => {
|
||||
const { skills } = get();
|
||||
return skills.find(s => s.id === skillId) || null;
|
||||
},
|
||||
// === 加载评论 ===
|
||||
loadReviews: async (skillId: string) => {
|
||||
// MVP: 从 localStorage 模拟加载评论
|
||||
const reviewsKey = `zclaw-skill-reviews-${skillId}`;
|
||||
const stored = localStorage.getItem(reviewsKey);
|
||||
const reviews: SkillReview[] = stored ? JSON.parse(stored) : [];
|
||||
return reviews;
|
||||
},
|
||||
// === 添加评论 ===
|
||||
addReview: async (skillId: string, review: Omit<SkillReview, 'id' | 'skillId' | 'createdAt'>) => {
|
||||
const reviews = await get().loadReviews(skillId);
|
||||
const newReview: SkillReview = {
|
||||
...review,
|
||||
id: `review-${Date.now()}`,
|
||||
skillId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updatedReviews = [...reviews, newReview];
|
||||
// 更新技能的评分和评论数
|
||||
const { skills } = get();
|
||||
const updatedSkills = skills.map(s => {
|
||||
if (s.id === skillId) {
|
||||
const totalRating = updatedReviews.reduce((sum, r) => sum + r.rating, 0);
|
||||
const avgRating = totalRating / updatedReviews.length;
|
||||
return {
|
||||
...s,
|
||||
rating: Math.round(avgRating * 10) / 10,
|
||||
reviewCount: updatedReviews.length,
|
||||
};
|
||||
}
|
||||
return s;
|
||||
});
|
||||
// 持久化评论
|
||||
const reviewsKey = `zclaw-skill-reviews-${skillId}`;
|
||||
localStorage.setItem(reviewsKey, JSON.stringify(updatedReviews));
|
||||
set({ skills: updatedSkills });
|
||||
},
|
||||
// === 刷新技能列表 ===
|
||||
refreshSkills: async () => {
|
||||
// 清除缓存并重新加载
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
await get().loadSkills();
|
||||
},
|
||||
// === 清除错误 ===
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
// === 重置状态 ===
|
||||
reset: () => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(INSTALLED_KEY);
|
||||
set(initialState);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// === 辅助函数 ===
|
||||
|
||||
/**
|
||||
* 扫描 skills 目录获取可用技能
|
||||
*/
|
||||
async function scanSkillsDirectory(): Promise<Skill[]> {
|
||||
// 这里我们模拟扫描,实际实现需要通过 Tauri API 访问文件系统
|
||||
// 或者从预定义的技能列表中加载
|
||||
const skills: Skill[] = [
|
||||
// 开发类
|
||||
{
|
||||
id: 'code-review',
|
||||
name: '代码审查',
|
||||
description: '审查代码、分析代码质量、提供改进建议',
|
||||
triggers: ['审查代码', '代码审查', 'code review', 'PR Review', '检查代码', '分析代码'],
|
||||
capabilities: ['代码质量分析', '架构评估', '最佳实践检查', '安全审计'],
|
||||
toolDeps: ['read', 'grep', 'glob'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['代码', '审查', '质量'],
|
||||
},
|
||||
{
|
||||
id: 'translation',
|
||||
name: '翻译助手',
|
||||
description: '翻译文本、多语言转换、保持语言风格一致性',
|
||||
triggers: ['翻译', 'translate', '中译英', '英译中', '翻译成', '转换成'],
|
||||
capabilities: ['多语言翻译', '技术文档翻译', '代码注释翻译', 'UI 文本翻译', '风格保持'],
|
||||
toolDeps: ['read', 'write'],
|
||||
category: 'content',
|
||||
installed: false,
|
||||
tags: ['翻译', '语言', '国际化'],
|
||||
},
|
||||
{
|
||||
id: 'chinese-writing',
|
||||
name: '中文写作',
|
||||
description: '中文写作助手 - 帮助撰写各类中文文档、文章、报告',
|
||||
triggers: ['写一篇', '帮我写', '撰写', '起草', '润色', '中文写作'],
|
||||
capabilities: ['撰写文档', '润色修改', '调整语气', '中英文翻译'],
|
||||
toolDeps: ['read', 'write'],
|
||||
category: 'content',
|
||||
installed: false,
|
||||
tags: ['写作', '文档', '中文'],
|
||||
},
|
||||
{
|
||||
id: 'web-search',
|
||||
name: '网络搜索',
|
||||
description: '搜索互联网信息、整合多方来源',
|
||||
triggers: ['搜索', 'search', '查找信息', '查询', '搜索网络'],
|
||||
capabilities: ['搜索引擎集成', '信息提取', '来源验证', '结果整合'],
|
||||
toolDeps: ['web_search'],
|
||||
category: 'research',
|
||||
installed: false,
|
||||
tags: ['搜索', '互联网', '信息'],
|
||||
},
|
||||
{
|
||||
id: 'data-analysis',
|
||||
name: '数据分析',
|
||||
description: '数据清洗、统计分析、可视化图表',
|
||||
triggers: ['数据分析', '统计', '可视化', '图表', 'analytics'],
|
||||
capabilities: ['数据清洗', '统计分析', '可视化图表', '报告生成'],
|
||||
toolDeps: ['read', 'write', 'shell'],
|
||||
category: 'analytics',
|
||||
installed: false,
|
||||
tags: ['数据', '分析', '可视化'],
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
name: 'Git 操作',
|
||||
description: 'Git 版本控制操作、分支管理、冲突解决',
|
||||
triggers: ['git', '版本控制', '分支', '合并', 'commit', 'merge'],
|
||||
capabilities: ['分支管理', '冲突解决', 'rebase', 'cherry-pick'],
|
||||
toolDeps: ['shell'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['git', '版本控制', '分支'],
|
||||
},
|
||||
{
|
||||
id: 'shell-command',
|
||||
name: 'Shell 命令',
|
||||
description: '执行 Shell 命令、系统操作',
|
||||
triggers: ['shell', '命令行', '终端', 'terminal', 'bash'],
|
||||
capabilities: ['命令执行', '管道操作', '脚本运行', '环境管理'],
|
||||
toolDeps: ['shell'],
|
||||
category: 'ops',
|
||||
installed: false,
|
||||
tags: ['shell', '命令', '系统'],
|
||||
},
|
||||
{
|
||||
id: 'file-operations',
|
||||
name: '文件操作',
|
||||
description: '文件读写、目录管理、文件搜索',
|
||||
triggers: ['文件', 'file', '读取', '写入', '目录', '文件夹'],
|
||||
capabilities: ['文件读写', '目录管理', '文件搜索', '批量操作'],
|
||||
toolDeps: ['read', 'write', 'glob'],
|
||||
category: 'ops',
|
||||
installed: false,
|
||||
tags: ['文件', '目录', '读写'],
|
||||
},
|
||||
{
|
||||
id: 'security-engineer',
|
||||
name: '安全工程师',
|
||||
description: '安全工程师 - 负责安全审计、漏洞检测、合规检查',
|
||||
triggers: ['安全审计', '漏洞检测', '安全检查', 'security', '渗透测试'],
|
||||
capabilities: ['漏洞扫描', '合规检查', '安全加固', '威胁建模'],
|
||||
toolDeps: ['read', 'grep', 'shell'],
|
||||
category: 'security',
|
||||
installed: false,
|
||||
tags: ['安全', '审计', '漏洞'],
|
||||
},
|
||||
{
|
||||
id: 'ai-engineer',
|
||||
name: 'AI 工程师',
|
||||
description: 'AI/ML 工程师 - 专注机器学习模型开发、LLM 集成和生产系统部署',
|
||||
triggers: ['AI工程师', '机器学习', 'ML模型', 'LLM集成', '深度学习', '模型训练'],
|
||||
capabilities: ['ML 框架', 'LLM 集成', 'RAG 系统', '向量数据库'],
|
||||
toolDeps: ['bash', 'read', 'write', 'grep', 'glob'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['AI', 'ML', 'LLM'],
|
||||
},
|
||||
{
|
||||
id: 'senior-developer',
|
||||
name: '高级开发',
|
||||
description: '高级开发工程师 - 端到端功能实现、复杂问题解决',
|
||||
triggers: ['高级开发', 'senior developer', '端到端', '复杂功能', '架构实现'],
|
||||
capabilities: ['端到端实现', '架构设计', '性能优化', '代码重构'],
|
||||
toolDeps: ['bash', 'read', 'write', 'grep', 'glob'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['开发', '架构', '实现'],
|
||||
},
|
||||
{
|
||||
id: 'frontend-developer',
|
||||
name: '前端开发',
|
||||
description: '前端开发专家 - 擅长 React/Vue/CSS/TypeScript',
|
||||
triggers: ['前端开发', '页面开发', 'UI开发', 'React', 'Vue', 'CSS'],
|
||||
capabilities: ['组件开发', '样式调整', '性能优化', '响应式设计'],
|
||||
toolDeps: ['read', 'write', 'shell'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
types: ['前端', 'UI', '组件'],
|
||||
},
|
||||
{
|
||||
id: 'backend-architect',
|
||||
name: '后端架构',
|
||||
description: '后端架构设计、API设计、数据库建模',
|
||||
triggers: ['后端架构', 'API设计', '数据库设计', '系统架构', '微服务'],
|
||||
capabilities: ['架构设计', 'API规范', '数据库建模', '性能优化'],
|
||||
toolDeps: ['read', 'write', 'shell'],
|
||||
category: 'development',
|
||||
installed: false,
|
||||
tags: ['后端', '架构', 'API'],
|
||||
},
|
||||
{
|
||||
id: 'devops-automator',
|
||||
name: 'DevOps 自动化',
|
||||
description: 'CI/CD、Docker、K8s、自动化部署',
|
||||
triggers: ['DevOps', 'CI/CD', 'Docker', '部署', '自动化', 'K8s'],
|
||||
capabilities: ['CI/CD配置', '容器化', '自动化部署', '监控告警'],
|
||||
toolDeps: ['shell', 'read', 'write'],
|
||||
category: 'ops',
|
||||
installed: false,
|
||||
tags: ['DevOps', 'Docker', 'CI/CD'],
|
||||
},
|
||||
{
|
||||
id: 'senior-pm',
|
||||
name: '高级PM',
|
||||
description: '项目管理、需求分析、迭代规划',
|
||||
triggers: ['项目管理', '需求分析', '迭代规划', '产品设计', 'PRD'],
|
||||
capabilities: ['需求拆解', '迭代排期', '风险评估', '文档撰写'],
|
||||
toolDeps: ['read', 'write'],
|
||||
category: 'management',
|
||||
installed: false,
|
||||
tags: ['PM', '需求', '迭代'],
|
||||
},
|
||||
];
|
||||
return skills;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type {
|
||||
Team,
|
||||
TeamMember,
|
||||
@@ -120,7 +121,9 @@ const calculateMetrics = (team: Team): TeamMetrics => {
|
||||
|
||||
// === Store Implementation ===
|
||||
|
||||
export const useTeamStore = create<TeamStoreState>((set, get) => ({
|
||||
export const useTeamStore = create<TeamStoreState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial State
|
||||
teams: [],
|
||||
activeTeam: null,
|
||||
@@ -582,4 +585,12 @@ export const useTeamStore = create<TeamStoreState>((set, get) => ({
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
}));
|
||||
}),
|
||||
{
|
||||
name: 'zclaw-teams',
|
||||
partialize: (state) => ({
|
||||
teams: state.teams,
|
||||
activeTeam: state.activeTeam,
|
||||
}),
|
||||
},
|
||||
));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { Workflow, WorkflowRun } from './gatewayStore';
|
||||
import type { GatewayClient } from '../lib/gateway-client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
@@ -30,7 +31,7 @@ export interface WorkflowStep {
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface CreateWorkflowInput {
|
||||
export interface WorkflowCreateOptions {
|
||||
name: string;
|
||||
description?: string;
|
||||
steps: WorkflowStep[];
|
||||
@@ -53,7 +54,7 @@ export interface ExtendedWorkflowRun extends WorkflowRun {
|
||||
|
||||
interface WorkflowClient {
|
||||
listWorkflows(): Promise<{ workflows: { id: string; name: string; steps: number; description?: string; createdAt?: string }[] } | null>;
|
||||
createWorkflow(workflow: CreateWorkflowInput): Promise<{ id: string; name: string } | null>;
|
||||
createWorkflow(workflow: WorkflowCreateOptions): Promise<{ id: string; name: string } | null>;
|
||||
updateWorkflow(id: string, updates: UpdateWorkflowInput): Promise<{ id: string; name: string } | null>;
|
||||
deleteWorkflow(id: string): Promise<{ status: string }>;
|
||||
executeWorkflow(id: string, input?: Record<string, unknown>): Promise<{ runId: string; status: string } | null>;
|
||||
@@ -61,9 +62,9 @@ interface WorkflowClient {
|
||||
listWorkflowRuns(workflowId: string, opts?: { limit?: number; offset?: number }): Promise<{ runs: RawWorkflowRun[] } | null>;
|
||||
}
|
||||
|
||||
// === Store State ===
|
||||
// === Store State Slice ===
|
||||
|
||||
interface WorkflowState {
|
||||
export interface WorkflowStateSlice {
|
||||
workflows: Workflow[];
|
||||
workflowRuns: Record<string, ExtendedWorkflowRun[]>;
|
||||
isLoading: boolean;
|
||||
@@ -71,13 +72,13 @@ interface WorkflowState {
|
||||
client: WorkflowClient;
|
||||
}
|
||||
|
||||
// === Store Actions ===
|
||||
// === Store Actions Slice ===
|
||||
|
||||
interface WorkflowActions {
|
||||
export interface WorkflowActionsSlice {
|
||||
setWorkflowStoreClient: (client: WorkflowClient) => void;
|
||||
loadWorkflows: () => Promise<void>;
|
||||
getWorkflow: (id: string) => Workflow | undefined;
|
||||
createWorkflow: (workflow: CreateWorkflowInput) => Promise<Workflow | undefined>;
|
||||
createWorkflow: (workflow: WorkflowCreateOptions) => Promise<Workflow | undefined>;
|
||||
updateWorkflow: (id: string, updates: UpdateWorkflowInput) => Promise<Workflow | undefined>;
|
||||
deleteWorkflow: (id: string) => Promise<void>;
|
||||
triggerWorkflow: (id: string, input?: Record<string, unknown>) => Promise<{ runId: string; status: string } | undefined>;
|
||||
@@ -87,6 +88,10 @@ interface WorkflowActions {
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// === Combined Store Type ===
|
||||
|
||||
export type WorkflowStore = WorkflowStateSlice & WorkflowActionsSlice;
|
||||
|
||||
// === Initial State ===
|
||||
|
||||
const initialState = {
|
||||
@@ -99,7 +104,7 @@ const initialState = {
|
||||
|
||||
// === Store ===
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState & WorkflowActions>((set, get) => ({
|
||||
export const useWorkflowStore = create<WorkflowStateSlice & WorkflowActionsSlice>((set, get) => ({
|
||||
...initialState,
|
||||
|
||||
setWorkflowStoreClient: (client: WorkflowClient) => {
|
||||
@@ -128,7 +133,7 @@ export const useWorkflowStore = create<WorkflowState & WorkflowActions>((set, ge
|
||||
return get().workflows.find(w => w.id === id);
|
||||
},
|
||||
|
||||
createWorkflow: async (workflow: CreateWorkflowInput) => {
|
||||
createWorkflow: async (workflow: WorkflowCreateOptions) => {
|
||||
set({ error: null });
|
||||
try {
|
||||
const result = await get().client.createWorkflow(workflow);
|
||||
@@ -253,3 +258,29 @@ export const useWorkflowStore = create<WorkflowState & WorkflowActions>((set, ge
|
||||
|
||||
// Re-export types from gatewayStore for convenience
|
||||
export type { Workflow, WorkflowRun };
|
||||
|
||||
// === Client Injection ===
|
||||
|
||||
/**
|
||||
* Helper to create a WorkflowClient adapter from a GatewayClient.
|
||||
*/
|
||||
function createWorkflowClientFromGateway(client: GatewayClient): WorkflowClient {
|
||||
return {
|
||||
listWorkflows: () => client.listWorkflows(),
|
||||
createWorkflow: (workflow) => client.createWorkflow(workflow),
|
||||
updateWorkflow: (id, updates) => client.updateWorkflow(id, updates),
|
||||
deleteWorkflow: (id) => client.deleteWorkflow(id),
|
||||
executeWorkflow: (id, input) => client.executeWorkflow(id, input),
|
||||
cancelWorkflow: (workflowId, runId) => client.cancelWorkflow(workflowId, runId),
|
||||
listWorkflowRuns: (workflowId, opts) => client.listWorkflowRuns(workflowId, opts),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client for the workflow store.
|
||||
* Called by the coordinator during initialization.
|
||||
*/
|
||||
export function setWorkflowStoreClient(client: unknown): void {
|
||||
const workflowClient = createWorkflowClientFromGateway(client as GatewayClient);
|
||||
useWorkflowStore.getState().setWorkflowStoreClient(workflowClient);
|
||||
}
|
||||
|
||||
87
desktop/src/types/active-learning.ts
Normal file
87
desktop/src/types/active-learning.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 主动学习引擎类型定义
|
||||
*
|
||||
* 定义学习事件、模式、建议等核心类型。
|
||||
*/
|
||||
|
||||
// === 学习事件类型 ===
|
||||
|
||||
export type LearningEventType =
|
||||
| 'preference' // 偏好学习
|
||||
| 'correction' // 纠正学习
|
||||
| 'context' // 上下文学习
|
||||
| 'feedback' // 反馈学习
|
||||
| 'behavior' // 行为学习
|
||||
| 'implicit'; // 隐式学习
|
||||
|
||||
export type FeedbackSentiment = 'positive' | 'negative' | 'neutral';
|
||||
|
||||
// === 学习事件 ===
|
||||
|
||||
export interface LearningEvent {
|
||||
id: string;
|
||||
type: LearningEventType;
|
||||
agentId: string;
|
||||
conversationId?: string;
|
||||
messageId?: string;
|
||||
|
||||
// 事件内容
|
||||
trigger: string; // 触发学习的原始内容
|
||||
observation: string; // 观察到的用户行为/反馈
|
||||
context?: string; // 上下文信息
|
||||
|
||||
// 学习结果
|
||||
inferredPreference?: string;
|
||||
inferredRule?: string;
|
||||
confidence: number; // 0-1
|
||||
|
||||
// 元数据
|
||||
timestamp: number;
|
||||
updatedAt?: number;
|
||||
acknowledged: boolean;
|
||||
appliedCount: number;
|
||||
}
|
||||
|
||||
// === 学习模式 ===
|
||||
|
||||
export interface LearningPattern {
|
||||
type: 'preference' | 'rule' | 'context' | 'behavior';
|
||||
pattern: string;
|
||||
description: string;
|
||||
examples: string[];
|
||||
confidence: number;
|
||||
agentId: string;
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
||||
// === 学习建议 ===
|
||||
|
||||
export interface LearningSuggestion {
|
||||
id: string;
|
||||
agentId: string;
|
||||
type: string;
|
||||
pattern: string;
|
||||
suggestion: string;
|
||||
confidence: number;
|
||||
createdAt: number;
|
||||
expiresAt: Date;
|
||||
dismissed: boolean;
|
||||
}
|
||||
|
||||
// === 学习配置 ===
|
||||
|
||||
export interface LearningConfig {
|
||||
enabled: boolean;
|
||||
minConfidence: number;
|
||||
maxEvents: number;
|
||||
suggestionCooldown: number;
|
||||
}
|
||||
|
||||
// === 默认配置 ===
|
||||
|
||||
export const DEFAULT_LEARNING_CONFIG: LearningConfig = {
|
||||
enabled: true,
|
||||
minConfidence: 0.5,
|
||||
maxEvents: 1000,
|
||||
suggestionCooldown: 2, // hours
|
||||
};
|
||||
@@ -36,8 +36,6 @@ export interface Skill {
|
||||
installedAt?: string;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 技能评论
|
||||
export interface SkillReview {
|
||||
/** 评论ID */
|
||||
@@ -54,8 +52,6 @@ export interface SkillReview {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 技能市场状态
|
||||
export interface SkillMarketState {
|
||||
/** 所有技能 */
|
||||
|
||||
Reference in New Issue
Block a user