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