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:
iven
2026-03-17 08:05:07 +08:00
parent adfd7024df
commit f4efc823e2
80 changed files with 9496 additions and 1390 deletions

View File

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