refactor(skills): add skill-adapter and refactor SkillMarket

- Add skill-adapter.ts to bridge configStore and UI skill formats
- Refactor SkillMarket to use new skill-adapter instead of skill-discovery
- Add health check state to connectionStore
- Update multiple components with improved typing
- Clean up test artifacts and add new test results
- Update README and add skill-market-mvp plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-21 00:28:03 +08:00
parent 54ccc0a7b0
commit 48a430fc97
50 changed files with 1523 additions and 360 deletions

View File

@@ -28,7 +28,7 @@ import {
X,
Loader2
} from 'lucide-react';
import { useGatewayStore, AuditLogEntry } from '../store/gatewayStore';
import { useSecurityStore, AuditLogEntry } from '../store/securityStore';
import { getGatewayClient } from '../lib/gateway-client';
@@ -511,7 +511,9 @@ function HashChainVisualization({ logs, selectedIndex, onSelect, brokenAtIndex }
// === Main Component ===
export function AuditLogsPanel() {
const { auditLogs, loadAuditLogs, isLoading } = useGatewayStore();
const auditLogs = useSecurityStore((s) => s.auditLogs);
const loadAuditLogs = useSecurityStore((s) => s.loadAuditLogs);
const isLoading = useSecurityStore((s) => s.auditLogsLoading);
const client = getGatewayClient();
// State

View File

@@ -8,8 +8,8 @@
*/
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useWorkflowStore } from '../../store/workflowStore';
import { useHandStore } from '../../store/handStore';
import { useWorkflowStore, type Workflow } from '../../store/workflowStore';
import {
type AutomationItem,
type CategoryType,
@@ -51,15 +51,14 @@ export function AutomationPanel({
onSelect,
showBatchActions = true,
}: AutomationPanelProps) {
// Store state - use gatewayStore which has the actual data
const hands = useGatewayStore(s => s.hands);
const workflows = useGatewayStore(s => s.workflows);
const isLoading = useGatewayStore(s => s.isLoading);
const loadHands = useGatewayStore(s => s.loadHands);
const loadWorkflows = useGatewayStore(s => s.loadWorkflows);
const triggerHand = useGatewayStore(s => s.triggerHand);
// workflowStore for triggerWorkflow (not in gatewayStore)
const triggerWorkflow = useWorkflowStore(s => s.triggerWorkflow);
// Store state - use domain stores
const hands = useHandStore((s) => s.hands);
const workflows = useWorkflowStore((s) => s.workflows);
const isLoading = useHandStore((s) => s.isLoading) || useWorkflowStore((s) => s.isLoading);
const loadHands = useHandStore((s) => s.loadHands);
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
const triggerHand = useHandStore((s) => s.triggerHand);
const triggerWorkflow = useWorkflowStore((s) => s.triggerWorkflow);
// UI state
const [selectedCategory, setSelectedCategory] = useState<CategoryType>(initialCategory);

View File

@@ -1,5 +1,7 @@
import { useEffect } from 'react';
import { useGatewayStore } from '../store/gatewayStore';
import { useConnectionStore } from '../store/connectionStore';
import { useAgentStore } from '../store/agentStore';
import { useConfigStore } from '../store/configStore';
import { Radio, RefreshCw, MessageCircle, Settings } from 'lucide-react';
const CHANNEL_ICONS: Record<string, string> = {
@@ -20,7 +22,10 @@ interface ChannelListProps {
}
export function ChannelList({ onOpenSettings }: ChannelListProps) {
const { channels, connectionState, loadChannels, loadPluginStatus } = useGatewayStore();
const connectionState = useConnectionStore((s) => s.connectionState);
const loadPluginStatus = useAgentStore((s) => s.loadPluginStatus);
const channels = useConfigStore((s) => s.channels);
const loadChannels = useConfigStore((s) => s.loadChannels);
const connected = connectionState === 'connected';

View File

@@ -1,7 +1,9 @@
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 { useConnectionStore } from '../store/connectionStore';
import { useAgentStore } from '../store/agentStore';
import { useConfigStore } from '../store/configStore';
import { Paperclip, ChevronDown, Terminal, SquarePen, ArrowUp, MessageSquare, Download, Copy, Check } from 'lucide-react';
import { Button, EmptyState } from './ui';
import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations';
@@ -14,7 +16,9 @@ export function ChatArea() {
sendMessage: sendToGateway, setCurrentModel, initStreamListener,
newConversation,
} = useChatStore();
const { connectionState, clones, models } = useGatewayStore();
const connectionState = useConnectionStore((s) => s.connectionState);
const clones = useAgentStore((s) => s.clones);
const models = useConfigStore((s) => s.models);
const [input, setInput] = useState('');
const [showModelPicker, setShowModelPicker] = useState(false);

View File

@@ -8,7 +8,8 @@
*/
import { useState, useEffect, useCallback } from 'react';
import { useGatewayStore } from '../store/gatewayStore';
import { useHandStore } from '../store/handStore';
import { useWorkflowStore } from '../store/workflowStore';
import {
Zap,
X,
@@ -144,7 +145,12 @@ const eventTypeOptions = [
// === Component ===
export function CreateTriggerModal({ isOpen, onClose, onSuccess }: CreateTriggerModalProps) {
const { hands, workflows, createTrigger, loadHands, loadWorkflows } = useGatewayStore();
// Store state - use domain stores
const hands = useHandStore((s) => s.hands);
const workflows = useWorkflowStore((s) => s.workflows);
const createTrigger = useHandStore((s) => s.createTrigger);
const loadHands = useHandStore((s) => s.loadHands);
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
const [formData, setFormData] = useState<TriggerFormData>(initialFormData);
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);

View File

@@ -1,7 +1,9 @@
import { ReactNode, useEffect, useMemo, useState } from 'react';
import { motion } from 'framer-motion';
import { getStoredGatewayUrl } from '../lib/gateway-client';
import { useGatewayStore, type PluginStatus } from '../store/gatewayStore';
import { useConnectionStore } from '../store/connectionStore';
import { useAgentStore, type PluginStatus } from '../store/agentStore';
import { useConfigStore } from '../store/configStore';
import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
import {
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
@@ -79,10 +81,25 @@ import { getPersonalityById } from '../lib/personality-presets';
import { silentErrorHandler } from '../lib/error-utils';
export function RightPanel() {
const {
connectionState, gatewayVersion, error, clones, usageStats, pluginStatus,
connect, loadClones, loadUsageStats, loadPluginStatus, workspaceInfo, quickConfig, updateClone,
} = useGatewayStore();
// Connection store
const connectionState = useConnectionStore((s) => s.connectionState);
const gatewayVersion = useConnectionStore((s) => s.gatewayVersion);
const error = useConnectionStore((s) => s.error);
const connect = useConnectionStore((s) => s.connect);
// Agent store
const clones = useAgentStore((s) => s.clones);
const usageStats = useAgentStore((s) => s.usageStats);
const pluginStatus = useAgentStore((s) => s.pluginStatus);
const loadClones = useAgentStore((s) => s.loadClones);
const loadUsageStats = useAgentStore((s) => s.loadUsageStats);
const loadPluginStatus = useAgentStore((s) => s.loadPluginStatus);
const updateClone = useAgentStore((s) => s.updateClone);
// Config store
const workspaceInfo = useConfigStore((s) => s.workspaceInfo);
const quickConfig = useConfigStore((s) => s.quickConfig);
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'learning'>('status');
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');

View File

@@ -7,7 +7,10 @@
*/
import { useState, useEffect, useCallback } from 'react';
import { useGatewayStore, type Workflow } from '../store/gatewayStore';
import { useHandStore } from '../store/handStore';
import { useWorkflowStore, type Workflow } from '../store/workflowStore';
import { useAgentStore } from '../store/agentStore';
import { useConfigStore } from '../store/configStore';
import { WorkflowEditor } from './WorkflowEditor';
import { WorkflowHistory } from './WorkflowHistory';
import { TriggersPanel } from './TriggersPanel';
@@ -139,7 +142,14 @@ interface CreateJobModalProps {
}
function CreateJobModal({ isOpen, onClose, onSuccess }: CreateJobModalProps) {
const { hands, workflows, clones, createScheduledTask, loadHands, loadWorkflows, loadClones } = useGatewayStore();
// Store state - use domain stores
const hands = useHandStore((s) => s.hands);
const workflows = useWorkflowStore((s) => s.workflows);
const clones = useAgentStore((s) => s.clones);
const createScheduledTask = useConfigStore((s) => s.createScheduledTask);
const loadHands = useHandStore((s) => s.loadHands);
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
const loadClones = useAgentStore((s) => s.loadClones);
const [formData, setFormData] = useState<JobFormData>(initialFormData);
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -637,15 +647,14 @@ function CreateJobModal({ isOpen, onClose, onSuccess }: CreateJobModalProps) {
// === Main SchedulerPanel Component ===
export function SchedulerPanel() {
const {
scheduledTasks,
loadScheduledTasks,
workflows,
loadWorkflows,
createWorkflow,
executeWorkflow,
isLoading,
} = useGatewayStore();
// Store state - use domain stores
const scheduledTasks = useConfigStore((s) => s.scheduledTasks);
const loadScheduledTasks = useConfigStore((s) => s.loadScheduledTasks);
const workflows = useWorkflowStore((s) => s.workflows);
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
const createWorkflow = useWorkflowStore((s) => s.createWorkflow);
const executeWorkflow = useWorkflowStore((s) => s.triggerWorkflow);
const isLoading = useHandStore((s) => s.isLoading) || useWorkflowStore((s) => s.isLoading) || useConfigStore((s) => s.isLoading);
const [activeTab, setActiveTab] = useState<TabType>('scheduled');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isWorkflowEditorOpen, setIsWorkflowEditorOpen] = useState(false);

View File

@@ -26,8 +26,9 @@ import {
Wifi,
WifiOff,
} from 'lucide-react';
import type { SecurityLayer, SecurityStatus } from '../store/gatewayStore';
import { useGatewayStore } from '../store/gatewayStore';
import type { SecurityLayer, SecurityStatus } from '../store/securityStore';
import { useSecurityStore } from '../store/securityStore';
import { useConnectionStore } from '../store/connectionStore';
// OpenFang 16-layer security architecture definitions
export const SECURITY_LAYERS: Array<{
@@ -522,7 +523,10 @@ interface SecurityStatusPanelProps {
}
export function SecurityStatusPanel({ className = '' }: SecurityStatusPanelProps) {
const { securityStatus, securityStatusLoading, loadSecurityStatus, connectionState } = useGatewayStore();
const securityStatus = useSecurityStore((s) => s.securityStatus);
const securityStatusLoading = useSecurityStore((s) => s.securityStatusLoading);
const loadSecurityStatus = useSecurityStore((s) => s.loadSecurityStatus);
const connectionState = useConnectionStore((s) => s.connectionState);
const [localStatus, setLocalStatus] = useState<SecurityStatus>(getDefaultSecurityStatus());
const [refreshing, setRefreshing] = useState(false);

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { Shield, ShieldCheck, ShieldAlert, ShieldX, RefreshCw, Loader2, AlertCircle } from 'lucide-react';
import { useGatewayStore } from '../store/gatewayStore';
import { useConnectionStore } from '../store/connectionStore';
import { useSecurityStore } from '../store/securityStore';
// OpenFang 16-layer security architecture names (Chinese)
const SECURITY_LAYER_NAMES: Record<string, string> = {
@@ -75,13 +76,11 @@ function getSecurityLabel(level: 'critical' | 'high' | 'medium' | 'low') {
}
export function SecurityStatus() {
const {
connectionState,
securityStatus,
securityStatusLoading,
securityStatusError,
loadSecurityStatus,
} = useGatewayStore();
const connectionState = useConnectionStore((s) => s.connectionState);
const securityStatus = useSecurityStore((s) => s.securityStatus);
const securityStatusLoading = useSecurityStore((s) => s.securityStatusLoading);
const securityStatusError = useSecurityStore((s) => s.securityStatusError);
const loadSecurityStatus = useSecurityStore((s) => s.loadSecurityStatus);
const connected = connectionState === 'connected';
useEffect(() => {

View File

@@ -1,12 +1,19 @@
import { useState, useEffect } from 'react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
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();
const { currentModel } = useChatStore();
const connectionState = useConnectionStore((s) => s.connectionState);
const gatewayVersion = useConnectionStore((s) => s.gatewayVersion);
const error = useConnectionStore((s) => s.error);
const connect = useConnectionStore((s) => s.connect);
const disconnect = useConnectionStore((s) => s.disconnect);
const quickConfig = useConfigStore((s) => s.quickConfig);
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
const currentModel = useChatStore((s) => s.currentModel);
const [theme, setTheme] = useState<'light' | 'dark'>(quickConfig.theme || 'light');
const [autoStart, setAutoStart] = useState(quickConfig.autoStart ?? false);
const [showToolCalls, setShowToolCalls] = useState(quickConfig.showToolCalls ?? false);

View File

@@ -1,6 +1,8 @@
import { useEffect } from 'react';
import { Radio, RefreshCw, MessageCircle, Settings2 } from 'lucide-react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { useAgentStore } from '../../store/agentStore';
const CHANNEL_ICONS: Record<string, string> = {
feishu: '飞',
@@ -9,7 +11,10 @@ const CHANNEL_ICONS: Record<string, string> = {
};
export function IMChannels() {
const { channels, connectionState, loadChannels, loadPluginStatus } = useGatewayStore();
const channels = useConfigStore((s) => s.channels);
const loadChannels = useConfigStore((s) => s.loadChannels);
const connectionState = useConnectionStore((s) => s.connectionState);
const loadPluginStatus = useAgentStore((s) => s.loadPluginStatus);
const connected = connectionState === 'connected';
const loading = connectionState === 'connecting' || connectionState === 'reconnecting' || connectionState === 'handshaking';

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
import { useGatewayStore } from '../../store/gatewayStore';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { useChatStore } from '../../store/chatStore';
import { silentErrorHandler } from '../../lib/error-utils';
import { Plus, Pencil, Trash2, Star, Eye, EyeOff, AlertCircle, X } from 'lucide-react';
@@ -53,7 +54,11 @@ function saveCustomModels(models: CustomModel[]): void {
}
export function ModelsAPI() {
const { connectionState, connect, disconnect, quickConfig, loadModels } = useGatewayStore();
const connectionState = useConnectionStore((s) => s.connectionState);
const connect = useConnectionStore((s) => s.connect);
const disconnect = useConnectionStore((s) => s.disconnect);
const quickConfig = useConfigStore((s) => s.quickConfig);
const loadModels = useConfigStore((s) => s.loadModels);
const { currentModel, setCurrentModel } = useChatStore();
const [gatewayUrl, setGatewayUrl] = useState(getStoredGatewayUrl());
const [gatewayToken, setGatewayToken] = useState(quickConfig.gatewayToken || getStoredGatewayToken());

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useSecurityStore } from '../../store/securityStore';
import {
Settings as SettingsIcon,
BarChart3,
@@ -15,6 +15,7 @@ import {
HelpCircle,
ClipboardList,
Clock,
Heart,
} from 'lucide-react';
import { silentErrorHandler } from '../../lib/error-utils';
import { General } from './General';
@@ -31,6 +32,7 @@ import { AuditLogsPanel } from '../AuditLogsPanel';
import { SecurityStatus } from '../SecurityStatus';
import { SecurityLayersPanel } from '../SecurityLayersPanel';
import { TaskList } from '../TaskList';
import { HeartbeatConfig } from '../HeartbeatConfig';
interface SettingsLayoutProps {
onBack: () => void;
@@ -49,6 +51,7 @@ type SettingsPage =
| 'security'
| 'audit'
| 'tasks'
| 'heartbeat'
| 'feedback'
| 'about';
@@ -65,13 +68,14 @@ const menuItems: { id: SettingsPage; label: string; icon: React.ReactNode }[] =
{ id: 'security', label: '安全状态', icon: <Shield className="w-4 h-4" /> },
{ id: 'audit', label: '审计日志', icon: <ClipboardList className="w-4 h-4" /> },
{ id: 'tasks', label: '定时任务', icon: <Clock className="w-4 h-4" /> },
{ id: 'heartbeat', label: '心跳配置', icon: <Heart className="w-4 h-4" /> },
{ id: 'feedback', label: '提交反馈', icon: <HelpCircle className="w-4 h-4" /> },
{ id: 'about', label: '关于', icon: <Info className="w-4 h-4" /> },
];
export function SettingsLayout({ onBack }: SettingsLayoutProps) {
const [activePage, setActivePage] = useState<SettingsPage>('general');
const { securityStatus } = useGatewayStore();
const securityStatus = useSecurityStore((s) => s.securityStatus);
const renderPage = () => {
switch (activePage) {
@@ -112,6 +116,11 @@ export function SettingsLayout({ onBack }: SettingsLayoutProps) {
</div>
</div>
);
case 'heartbeat': return (
<div className="max-w-3xl h-full">
<HeartbeatConfig />
</div>
);
case 'feedback': return <Feedback />;
case 'about': return <About />;
default: return <General />;

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { silentErrorHandler } from '../../lib/error-utils';
import { Wrench, Zap, FileCode, Globe, Mail, Database, Search, MessageSquare } from 'lucide-react';
@@ -64,7 +65,11 @@ const SYSTEM_SKILLS = [
];
export function Skills() {
const { connectionState, quickConfig, skillsCatalog, loadSkillsCatalog, saveQuickConfig } = useGatewayStore();
const connectionState = useConnectionStore((s) => s.connectionState);
const quickConfig = useConfigStore((s) => s.quickConfig);
const skillsCatalog = useConfigStore((s) => s.skillsCatalog);
const loadSkillsCatalog = useConfigStore((s) => s.loadSkillsCatalog);
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
const connected = connectionState === 'connected';
const [extraDir, setExtraDir] = useState('');
const [activeFilter, setActiveFilter] = useState<'all' | 'system' | 'builtin' | 'extra'>('all');

View File

@@ -11,33 +11,30 @@
*/
import { useState, useEffect, useMemo, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Search,
Package,
Check,
Plus,
Minus,
Sparkles,
Tag,
Layers,
ChevronDown,
ChevronRight,
RefreshCw,
Info,
} from 'lucide-react';
import { useConfigStore, type SkillInfo } from '../store/configStore';
import {
SkillDiscoveryEngine,
type SkillInfo,
type SkillSuggestion,
} from '../lib/skill-discovery';
adaptSkillsCatalog,
type SkillDisplay,
} from '../lib/skill-adapter';
// === Types ===
interface SkillMarketProps {
className?: string;
onSkillInstall?: (skill: SkillInfo) => void;
onSkillUninstall?: (skill: SkillInfo) => void;
onSkillInstall?: (skill: SkillDisplay) => void;
onSkillUninstall?: (skill: SkillDisplay) => void;
}
type CategoryFilter = 'all' | 'development' | 'security' | 'analytics' | 'content' | 'ops' | 'management' | 'testing' | 'business' | 'marketing';
@@ -80,7 +77,7 @@ function SkillCard({
onInstall,
onUninstall,
}: {
skill: SkillInfo;
skill: SkillDisplay;
isExpanded: boolean;
onToggle: () => void;
onInstall: () => void;
@@ -240,35 +237,6 @@ function SkillCard({
);
}
function SuggestionCard({ suggestion }: { suggestion: SkillSuggestion }) {
const confidencePercent = Math.round(suggestion.confidence * 100);
return (
<div className="p-3 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{suggestion.skill.name}
</span>
<span className="text-xs text-blue-600 dark:text-blue-400 ml-auto">
{confidencePercent}%
</span>
</div>
<p className="text-xs text-gray-600 dark:text-gray-300 mb-2">{suggestion.reason}</p>
<div className="flex flex-wrap gap-1">
{suggestion.matchedPatterns.map((pattern) => (
<span
key={pattern}
className="px-1.5 py-0.5 text-xs bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded"
>
{pattern}
</span>
))}
</div>
</div>
);
}
// === Main Component ===
export function SkillMarket({
@@ -276,19 +244,23 @@ export function SkillMarket({
onSkillInstall,
onSkillUninstall,
}: SkillMarketProps) {
const [engine] = useState(() => new SkillDiscoveryEngine());
const [skills, setSkills] = useState<SkillInfo[]>([]);
// Use configStore instead of SkillDiscoveryEngine
const skillsCatalog = useConfigStore((s) => s.skillsCatalog);
const loadSkillsCatalog = useConfigStore((s) => s.loadSkillsCatalog);
const updateSkill = useConfigStore((s) => s.updateSkill);
const [searchQuery, setSearchQuery] = useState('');
const [categoryFilter, setCategoryFilter] = useState<CategoryFilter>('all');
const [expandedSkillId, setExpandedSkillId] = useState<string | null>(null);
const [suggestions, setSuggestions] = useState<SkillSuggestion[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
// Load skills
// Adapt skills to display format
const skills = useMemo(() => adaptSkillsCatalog(skillsCatalog), [skillsCatalog]);
// Load skills on mount
useEffect(() => {
const allSkills = engine.getAllSkills();
setSkills(allSkills);
}, [engine]);
loadSkillsCatalog();
}, [loadSkillsCatalog]);
// Filter skills
const filteredSkills = useMemo(() => {
@@ -301,13 +273,17 @@ export function SkillMarket({
// Search filter
if (searchQuery.trim()) {
const searchResult = engine.searchSkills(searchQuery);
const matchingIds = new Set(searchResult.results.map((s) => s.id));
result = result.filter((s) => matchingIds.has(s.id));
const queryLower = searchQuery.toLowerCase();
result = result.filter((s) =>
s.name.toLowerCase().includes(queryLower) ||
s.description.toLowerCase().includes(queryLower) ||
s.triggers.some((t) => t.toLowerCase().includes(queryLower)) ||
s.capabilities.some((c) => c.toLowerCase().includes(queryLower))
);
}
return result;
}, [skills, categoryFilter, searchQuery, engine]);
}, [skills, categoryFilter, searchQuery]);
// Get categories from skills
const categories = useMemo(() => {
@@ -323,44 +299,31 @@ export function SkillMarket({
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
await new Promise((resolve) => setTimeout(resolve, 500));
// engine.refreshIndex doesn't exist - skip
setSkills(engine.getAllSkills());
await loadSkillsCatalog();
setIsRefreshing(false);
}, [engine]);
}, [loadSkillsCatalog]);
const handleInstall = useCallback(
(skill: SkillInfo) => {
// Install skill - update local state
setSkills((prev) => prev.map(s => ({ ...s, installed: true })));
onSkillInstall?.(skill);
},
[onSkillInstall]
async (skill: SkillDisplay) => {
// Update skill via configStore (persists to backend)
await updateSkill(skill.id, { enabled: true });
onSkillInstall?.(skill);
},
[updateSkill, onSkillInstall]
);
const handleUninstall = useCallback(
(skill: SkillInfo) => {
// Uninstall skill - update local state
setSkills((prev) => prev.map(s => ({ ...s, installed: false })));
onSkillUninstall?.(skill);
async (skill: SkillDisplay) => {
// Update skill via configStore (persists to backend)
await updateSkill(skill.id, { enabled: false });
onSkillUninstall?.(skill);
},
[onSkillUninstall]
[updateSkill, onSkillUninstall]
);
const handleSearch = useCallback(
async (query: string) => {
setSearchQuery(query);
if (query.trim()) {
// Get suggestions based on search
const mockConversation = [{ role: 'user' as const, content: query }];
const newSuggestions = await engine.suggestSkills(mockConversation, 'default', 3);
setSuggestions(newSuggestions.slice(0, 3));
} else {
setSuggestions([]);
}
},
[engine]
);
const handleSearch = useCallback((query: string) => {
setSearchQuery(query);
}, []);
return (
<div className={`flex flex-col h-full ${className}`}>
@@ -405,25 +368,8 @@ export function SkillMarket({
/>
</div>
{/* Suggestions */}
<AnimatePresence>
{suggestions.length > 0 && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mt-3 space-y-2"
>
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 flex items-center gap-1">
<Info className="w-3 h-3" />
</h4>
{suggestions.map((suggestion) => (
<SuggestionCard key={suggestion.skill.id} suggestion={suggestion} />
))}
</motion.div>
)}
</AnimatePresence>
{/* Suggestions - placeholder for future AI-powered recommendations */}
</div>
{/* Category Filter */}

View File

@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { useGatewayStore } from '../store/gatewayStore';
import { useConnectionStore } from '../store/connectionStore';
import { useConfigStore } from '../store/configStore';
import { Clock, RefreshCw, Play, Pause, AlertCircle, CheckCircle2 } from 'lucide-react';
const STATUS_CONFIG: Record<string, { icon: typeof Play; color: string; label: string }> = {
@@ -10,7 +11,9 @@ const STATUS_CONFIG: Record<string, { icon: typeof Play; color: string; label: s
};
export function TaskList() {
const { scheduledTasks, connectionState, loadScheduledTasks } = useGatewayStore();
const scheduledTasks = useConfigStore((s) => s.scheduledTasks);
const connectionState = useConnectionStore((s) => s.connectionState);
const loadScheduledTasks = useConfigStore((s) => s.loadScheduledTasks);
const connected = connectionState === 'connected';

View File

@@ -8,7 +8,7 @@
import { useEffect, useState } from 'react';
import { useTeamStore } from '../store/teamStore';
import { useGatewayStore } from '../store/gatewayStore';
import { useAgentStore } from '../store/agentStore';
import { useChatStore } from '../store/chatStore';
import { Users, Plus, Activity, CheckCircle, AlertTriangle, X, Bot } from 'lucide-react';
import type { TeamMemberRole } from '../types/team';
@@ -20,7 +20,7 @@ interface TeamListProps {
export function TeamList({ onSelectTeam, selectedTeamId }: TeamListProps) {
const { teams, loadTeams, setActiveTeam, createTeam, isLoading } = useTeamStore();
const { clones } = useGatewayStore();
const clones = useAgentStore((s) => s.clones);
const { agents } = useChatStore();
const [showCreateModal, setShowCreateModal] = useState(false);
const [teamName, setTeamName] = useState('');

View File

@@ -107,9 +107,9 @@ function TriggerCard({ trigger, onToggle, onDelete, isToggling, isDeleting }: Tr
export function TriggersPanel() {
const triggers = useHandStore((s) => s.triggers);
const loadTriggers = useHandStore((s) => s.loadTriggers);
const updateTrigger = useHandStore((s) => s.updateTrigger);
const deleteTrigger = useHandStore((s) => s.deleteTrigger);
const isLoading = useHandStore((s) => s.isLoading);
const client = useHandStore((s) => s.client);
const [togglingTrigger, setTogglingTrigger] = useState<string | null>(null);
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
@@ -122,14 +122,14 @@ export function TriggersPanel() {
const handleToggle = useCallback(async (id: string, enabled: boolean) => {
setTogglingTrigger(id);
try {
await client.request('triggers.toggle', { id, enabled });
await updateTrigger(id, { enabled });
await loadTriggers();
} catch (error) {
console.error('Failed to toggle trigger:', error);
} finally {
setTogglingTrigger(null);
}
}, [client, loadTriggers]);
}, [updateTrigger, loadTriggers]);
const handleDelete = useCallback(async (id: string) => {
setDeletingTrigger(id);

View File

@@ -8,7 +8,8 @@
*/
import { useState, useEffect, useCallback } from 'react';
import { useGatewayStore, type Hand, type Workflow } from '../store/gatewayStore';
import { useHandStore, type Hand } from '../store/handStore';
import type { Workflow } from '../store/workflowStore';
import {
X,
Plus,
@@ -199,7 +200,8 @@ function StepEditor({ step, hands, index, onUpdate, onRemove, onMoveUp, onMoveDo
// === Main WorkflowEditor Component ===
export function WorkflowEditor({ workflow, isOpen, onClose, onSave, isSaving }: WorkflowEditorProps) {
const { hands, loadHands } = useGatewayStore();
const hands = useHandStore((s) => s.hands);
const loadHands = useHandStore((s) => s.loadHands);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [steps, setSteps] = useState<WorkflowStep[]>([]);

View File

@@ -0,0 +1,193 @@
/**
* Skill Adapter - Converts between configStore and UI skill formats
*
* Bridges the gap between:
* - configStore.SkillInfo (backend/Gateway format)
* - SkillMarket UI format (based on skill-discovery types)
*
* Part of Phase 1: Skill Market Store Unification
*/
import type { SkillInfo as ConfigSkillInfo } from '../store/configStore';
// === UI Skill Types (aligned with SkillMarket expectations) ===
export interface UISkillInfo {
id: string;
name: string;
description: string;
triggers: string[];
capabilities: string[];
toolDeps: string[];
installed: boolean;
category?: string;
path?: string;
source?: 'builtin' | 'extra';
}
// Category mapping based on skill keywords
const CATEGORY_KEYWORDS: Record<string, string[]> = {
development: ['code', 'git', 'frontend', 'backend', 'react', 'vue', 'api', 'typescript', 'javascript'],
security: ['security', 'audit', 'vulnerability', 'pentest', 'auth'],
analytics: ['data', 'analysis', 'analytics', 'visualization', 'report'],
content: ['writing', 'content', 'article', 'copy', 'chinese'],
ops: ['devops', 'docker', 'k8s', 'deploy', 'ci', 'cd', 'automation'],
management: ['pm', 'project', 'requirement', 'planning', 'prd'],
testing: ['test', 'api test', 'e2e', 'unit'],
business: ['finance', 'budget', 'expense', 'accounting'],
marketing: ['social', 'media', 'marketing', 'campaign', 'operation'],
};
/**
* Infer category from skill name and description
*/
function inferCategory(skill: ConfigSkillInfo): string | undefined {
const text = `${skill.name} ${skill.description || ''}`.toLowerCase();
for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
if (keywords.some(keyword => text.includes(keyword))) {
return category;
}
}
return undefined;
}
/**
* Extract trigger patterns from config format
*/
function extractTriggers(triggers?: ConfigSkillInfo['triggers']): string[] {
if (!triggers) return [];
return triggers
.map(t => t.pattern || t.type)
.filter((p): p is string => Boolean(p));
}
/**
* Extract capabilities from actions
*/
function extractCapabilities(actions?: ConfigSkillInfo['actions']): string[] {
if (!actions) return [];
return actions
.map(a => a.type)
.filter((t): t is string => Boolean(t));
}
/**
* Extract tool dependencies from actions params
*/
function extractToolDeps(actions?: ConfigSkillInfo['actions']): string[] {
if (!actions) return [];
const deps = new Set<string>();
for (const action of actions) {
if (action.params?.tools && Array.isArray(action.params.tools)) {
for (const tool of action.params.tools) {
if (typeof tool === 'string') {
deps.add(tool);
}
}
}
if (action.params?.toolDeps && Array.isArray(action.params.toolDeps)) {
for (const dep of action.params.toolDeps) {
if (typeof dep === 'string') {
deps.add(dep);
}
}
}
}
return Array.from(deps);
}
/**
* Adapt a single skill from configStore format to UI format
*/
export function adaptSkillInfo(skill: ConfigSkillInfo): UISkillInfo {
return {
id: skill.id,
name: skill.name,
description: skill.description || '',
triggers: extractTriggers(skill.triggers),
capabilities: extractCapabilities(skill.actions),
toolDeps: extractToolDeps(skill.actions),
installed: skill.enabled ?? false,
category: inferCategory(skill),
path: skill.path,
source: skill.source,
};
}
/**
* Adapt an array of skills from configStore format to UI format
*/
export function adaptSkills(skills: ConfigSkillInfo[]): UISkillInfo[] {
return skills.map(adaptSkillInfo);
}
/**
* Search skills by query string
*/
export function searchSkills(skills: UISkillInfo[], query: string): UISkillInfo[] {
const q = query.toLowerCase().trim();
if (!q) return skills;
const tokens = q.split(/[\s,;.!?.,;!?]+/).filter(t => t.length > 0);
const scored = skills.map(skill => {
let score = 0;
// Name match (highest weight)
if (skill.name.toLowerCase().includes(q)) score += 10;
// Description match
if (skill.description.toLowerCase().includes(q)) score += 5;
// Trigger match
for (const trigger of skill.triggers) {
const tLower = trigger.toLowerCase();
if (tLower === q) { score += 15; break; }
if (tLower.includes(q) || q.includes(tLower)) score += 8;
}
// Capability match
for (const cap of skill.capabilities) {
if (cap.toLowerCase().includes(q)) score += 4;
}
// Token-level matching
for (const token of tokens) {
if (skill.name.toLowerCase().includes(token)) score += 2;
if (skill.description.toLowerCase().includes(token)) score += 1;
for (const trigger of skill.triggers) {
if (trigger.toLowerCase().includes(token)) score += 3;
}
}
// Category match
if (skill.category && skill.category.toLowerCase().includes(q)) score += 3;
return { skill, score };
});
return scored
.filter(s => s.score > 0)
.sort((a, b) => b.score - a.score)
.map(s => s.skill);
}
/**
* Get unique categories from skills
*/
export function getCategories(skills: UISkillInfo[]): string[] {
const categories = new Set<string>();
for (const skill of skills) {
if (skill.category) {
categories.add(skill.category);
}
}
return Array.from(categories);
}

View File

@@ -23,6 +23,12 @@ import {
getUnsupportedLocalGatewayStatus,
type LocalGatewayStatus,
} from '../lib/tauri-gateway';
import {
performHealthCheck,
createHealthCheckScheduler,
type HealthCheckResult,
type HealthStatus,
} from '../lib/health-check';
import { useConfigStore } from './configStore';
// === Types ===
@@ -114,6 +120,8 @@ export interface ConnectionStateSlice {
localGateway: LocalGatewayStatus;
localGatewayBusy: boolean;
isLoading: boolean;
healthStatus: HealthStatus;
healthCheckResult: HealthCheckResult | null;
}
export interface ConnectionActionsSlice {

View File

@@ -746,6 +746,17 @@ export async function mockAgentMessageResponse(page: Page, response: string): Pr
});
}
/**
* Create a mock agent message response object
*/
function createAgentMessageResponse(content: string): object {
return {
response: content,
input_tokens: 100,
output_tokens: content.length,
};
}
/**
* Mock 错误响应
*/

View File

@@ -1,7 +1,10 @@
{
"status": "failed",
"failedTests": [
"bdcac940a81c3235ce13-8b134df5feeb02852417",
"bdcac940a81c3235ce13-6df5d90e5b85ad4debff"
"4c7e6ccba74c38082eff-428751c1a27c810d25bc",
"4c7e6ccba74c38082eff-e8eb267dbe5d6b944c33",
"4c7e6ccba74c38082eff-1e63c4d4b91978536fc3",
"4c7e6ccba74c38082eff-5a47c30a876dee9d7f6a",
"4c7e6ccba74c38082eff-d595d4fb8c9beec7acb6"
]
}

View File

@@ -0,0 +1,71 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e6]:
- img [ref=e7]
- textbox "搜索..." [ref=e10]
- button "新对话" [ref=e12]:
- img [ref=e13]
- generic [ref=e16]: 新对话
- navigation [ref=e17]:
- button "分身" [ref=e18]:
- img [ref=e19]
- generic [ref=e22]: 分身
- img [ref=e23]
- button "自动化" [ref=e25]:
- img [ref=e26]
- generic [ref=e28]: 自动化
- button "技能" [ref=e29]:
- img [ref=e30]
- generic [ref=e34]: 技能
- button "团队" [ref=e35]:
- img [ref=e36]
- generic [ref=e41]: 团队
- button "协作" [ref=e42]:
- img [ref=e43]
- generic [ref=e47]: 协作
- generic [ref=e52]:
- generic [ref=e53] [cursor=pointer]:
- img [ref=e55]
- generic [ref=e58]:
- generic [ref=e60]: ZCLAW
- paragraph [ref=e61]: 默认助手
- generic [ref=e62]:
- img [ref=e64]
- generic [ref=e65]: 连接 Gateway 后创建
- button "用 用户7141" [ref=e67]:
- generic [ref=e68]:
- generic [ref=e69]: 用户7141
- img [ref=e70]
- generic [ref=e72]:
- banner [ref=e73]:
- generic [ref=e74]:
- generic [ref=e76]: Z
- generic [ref=e77]: ZCLAW
- button "详情" [ref=e79]:
- img [ref=e80]
- generic [ref=e83]: 详情
- main [ref=e84]:
- generic [ref=e85]:
- generic [ref=e87]:
- heading "ZCLAW" [level=2] [ref=e88]
- generic [ref=e89]: Gateway 未连接
- generic [ref=e94]:
- img [ref=e96]
- heading "欢迎使用 ZCLAW" [level=3] [ref=e98]
- paragraph [ref=e99]: 请先在设置中连接 Gateway
- generic [ref=e101]:
- generic [ref=e102]:
- button "添加附件" [ref=e103]:
- img [ref=e104]
- textbox "请先连接 Gateway" [disabled] [ref=e107]
- generic [ref=e108]:
- button "选择模型" [ref=e109]:
- generic [ref=e110]: claude-sonnet-4-20250514
- img [ref=e111]
- button "发送消息" [disabled] [ref=e113]:
- img [ref=e114]
- generic [ref=e116]: Agent 在本地运行,内容由 AI 生成
```

View File

@@ -0,0 +1,86 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e6]:
- img [ref=e7]
- textbox "搜索..." [ref=e10]
- button "新对话" [ref=e12]:
- img [ref=e13]
- generic [ref=e16]: 新对话
- navigation [ref=e17]:
- button "分身" [ref=e18]:
- img [ref=e19]
- generic [ref=e22]: 分身
- img [ref=e23]
- button "自动化" [ref=e25]:
- img [ref=e26]
- generic [ref=e28]: 自动化
- button "技能" [ref=e29]:
- img [ref=e30]
- generic [ref=e34]: 技能
- button "团队" [ref=e35]:
- img [ref=e36]
- generic [ref=e41]: 团队
- button "协作" [ref=e42]:
- img [ref=e43]
- generic [ref=e47]: 协作
- generic [ref=e52]:
- generic [ref=e53] [cursor=pointer]:
- img [ref=e55]
- generic [ref=e58]:
- generic [ref=e60]: ZCLAW
- paragraph [ref=e61]: 默认助手
- generic [ref=e62] [cursor=pointer]:
- img [ref=e64]
- generic [ref=e67]: 创建新 Agent
- button "用 用户7141" [ref=e69]:
- generic [ref=e70]:
- generic [ref=e71]: 用户7141
- img [ref=e72]
- generic [ref=e74]:
- banner [ref=e75]:
- generic [ref=e76]:
- generic [ref=e78]: Z
- generic [ref=e79]: ZCLAW
- button "详情" [ref=e81]:
- img [ref=e82]
- generic [ref=e85]: 详情
- main [ref=e86]:
- generic [ref=e87]:
- generic [ref=e88]:
- generic [ref=e89]:
- heading "ZCLAW" [level=2] [ref=e90]
- generic [ref=e91]: Gateway 已连接
- generic [ref=e93]:
- button "Search messages" [ref=e94]:
- img [ref=e95]
- generic [ref=e98]: Search
- button "开始新对话" [ref=e99]:
- img [ref=e100]
- text: 新对话
- generic [ref=e103]:
- generic [ref=e105]:
- generic [ref=e106]:
- generic [ref=e109]: Write a short poem
- generic [ref=e111]:
- generic [ref=e112]: Z
- generic [ref=e114]:
- generic [ref=e115]: ⚠️ WebSocket connection failed
- paragraph [ref=e116]: WebSocket connection failed
- button "下载为 Markdown" [ref=e117]:
- img [ref=e118]
- generic [ref=e122]:
- generic [ref=e123]:
- button "添加附件" [ref=e124]:
- img [ref=e125]
- textbox "发送给 ZCLAW" [ref=e128]
- generic [ref=e129]:
- button "选择模型" [ref=e130]:
- generic [ref=e131]: claude-sonnet-4-20250514
- img [ref=e132]
- button "发送消息" [disabled] [ref=e134]:
- img [ref=e135]
- generic [ref=e137]: Agent 在本地运行,内容由 AI 生成
```

View File

@@ -0,0 +1,86 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e6]:
- img [ref=e7]
- textbox "搜索..." [ref=e10]
- button "新对话" [ref=e12]:
- img [ref=e13]
- generic [ref=e16]: 新对话
- navigation [ref=e17]:
- button "分身" [ref=e18]:
- img [ref=e19]
- generic [ref=e22]: 分身
- img [ref=e23]
- button "自动化" [ref=e25]:
- img [ref=e26]
- generic [ref=e28]: 自动化
- button "技能" [ref=e29]:
- img [ref=e30]
- generic [ref=e34]: 技能
- button "团队" [ref=e35]:
- img [ref=e36]
- generic [ref=e41]: 团队
- button "协作" [ref=e42]:
- img [ref=e43]
- generic [ref=e47]: 协作
- generic [ref=e52]:
- generic [ref=e53] [cursor=pointer]:
- img [ref=e55]
- generic [ref=e58]:
- generic [ref=e60]: ZCLAW
- paragraph [ref=e61]: 默认助手
- generic [ref=e62] [cursor=pointer]:
- img [ref=e64]
- generic [ref=e67]: 创建新 Agent
- button "用 用户7141" [ref=e69]:
- generic [ref=e70]:
- generic [ref=e71]: 用户7141
- img [ref=e72]
- generic [ref=e74]:
- banner [ref=e75]:
- generic [ref=e76]:
- generic [ref=e78]: Z
- generic [ref=e79]: ZCLAW
- button "详情" [ref=e81]:
- img [ref=e82]
- generic [ref=e85]: 详情
- main [ref=e86]:
- generic [ref=e87]:
- generic [ref=e88]:
- generic [ref=e89]:
- heading "ZCLAW" [level=2] [ref=e90]
- generic [ref=e91]: Gateway 已连接
- generic [ref=e93]:
- button "Search messages" [ref=e94]:
- img [ref=e95]
- generic [ref=e98]: Search
- button "开始新对话" [ref=e99]:
- img [ref=e100]
- text: 新对话
- generic [ref=e103]:
- generic [ref=e105]:
- generic [ref=e106]:
- generic [ref=e109]: Store state test
- generic [ref=e111]:
- generic [ref=e112]: Z
- generic [ref=e114]:
- generic [ref=e115]: ⚠️ WebSocket connection failed
- paragraph [ref=e116]: WebSocket connection failed
- button "下载为 Markdown" [ref=e117]:
- img [ref=e118]
- generic [ref=e122]:
- generic [ref=e123]:
- button "添加附件" [ref=e124]:
- img [ref=e125]
- textbox "发送给 ZCLAW" [ref=e128]
- generic [ref=e129]:
- button "选择模型" [ref=e130]:
- generic [ref=e131]: claude-sonnet-4-20250514
- img [ref=e132]
- button "发送消息" [disabled] [ref=e134]:
- img [ref=e135]
- generic [ref=e137]: Agent 在本地运行,内容由 AI 生成
```

View File

@@ -0,0 +1,106 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e6]:
- img [ref=e7]
- textbox "搜索..." [ref=e10]
- button "新对话" [ref=e12]:
- img [ref=e13]
- generic [ref=e16]: 新对话
- navigation [ref=e17]:
- button "分身" [ref=e18]:
- img [ref=e19]
- generic [ref=e22]: 分身
- img [ref=e23]
- button "自动化" [ref=e25]:
- img [ref=e26]
- generic [ref=e28]: 自动化
- button "技能" [ref=e29]:
- img [ref=e30]
- generic [ref=e34]: 技能
- button "团队" [ref=e35]:
- img [ref=e36]
- generic [ref=e41]: 团队
- button "协作" [ref=e42]:
- img [ref=e43]
- generic [ref=e47]: 协作
- generic [ref=e52]:
- generic [ref=e53] [cursor=pointer]:
- img [ref=e55]
- generic [ref=e58]:
- generic [ref=e60]: ZCLAW
- paragraph [ref=e61]: 默认助手
- generic [ref=e62] [cursor=pointer]:
- img [ref=e64]
- generic [ref=e67]: 创建新 Agent
- button "用 用户7141" [ref=e69]:
- generic [ref=e70]:
- generic [ref=e71]: 用户7141
- img [ref=e72]
- generic [ref=e74]:
- banner [ref=e75]:
- generic [ref=e76]:
- generic [ref=e78]: Z
- generic [ref=e79]: ZCLAW
- button "详情" [ref=e81]:
- img [ref=e82]
- generic [ref=e85]: 详情
- main [ref=e86]:
- generic [ref=e87]:
- generic [ref=e88]:
- generic [ref=e89]:
- heading "ZCLAW" [level=2] [ref=e90]
- generic [ref=e91]: Gateway 已连接
- generic [ref=e93]:
- button "Search messages" [ref=e94]:
- img [ref=e95]
- generic [ref=e98]: Search
- button "开始新对话" [ref=e99]:
- img [ref=e100]
- text: 新对话
- generic [ref=e103]:
- generic [ref=e105]:
- generic [ref=e106]:
- generic [ref=e109]: First message
- generic [ref=e111]:
- generic [ref=e112]: Z
- generic [ref=e114]:
- generic [ref=e115]: ⚠️ WebSocket connection failed
- paragraph [ref=e116]: WebSocket connection failed
- button "下载为 Markdown" [ref=e117]:
- img [ref=e118]
- generic [ref=e122]:
- generic [ref=e123]:
- generic [ref=e126]: Second message
- generic [ref=e128]:
- generic [ref=e129]: Z
- generic [ref=e131]:
- generic [ref=e132]: ⚠️ WebSocket connection failed
- paragraph [ref=e133]: WebSocket connection failed
- button "下载为 Markdown" [ref=e134]:
- img [ref=e135]
- generic [ref=e139]:
- generic [ref=e140]:
- generic [ref=e143]: Third message
- generic [ref=e145]:
- generic [ref=e146]: Z
- generic [ref=e148]:
- generic [ref=e149]: ⚠️ WebSocket connection failed
- paragraph [ref=e150]: WebSocket connection failed
- button "下载为 Markdown" [ref=e151]:
- img [ref=e152]
- generic [ref=e156]:
- generic [ref=e157]:
- button "添加附件" [ref=e158]:
- img [ref=e159]
- textbox "发送给 ZCLAW" [ref=e162]
- generic [ref=e163]:
- button "选择模型" [ref=e164]:
- generic [ref=e165]: claude-sonnet-4-20250514
- img [ref=e166]
- button "发送消息" [disabled] [ref=e168]:
- img [ref=e169]
- generic [ref=e171]: Agent 在本地运行,内容由 AI 生成
```

View File

@@ -0,0 +1,71 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e6]:
- img [ref=e7]
- textbox "搜索..." [ref=e10]
- button "新对话" [ref=e12]:
- img [ref=e13]
- generic [ref=e16]: 新对话
- navigation [ref=e17]:
- button "分身" [ref=e18]:
- img [ref=e19]
- generic [ref=e22]: 分身
- img [ref=e23]
- button "自动化" [ref=e25]:
- img [ref=e26]
- generic [ref=e28]: 自动化
- button "技能" [ref=e29]:
- img [ref=e30]
- generic [ref=e34]: 技能
- button "团队" [ref=e35]:
- img [ref=e36]
- generic [ref=e41]: 团队
- button "协作" [ref=e42]:
- img [ref=e43]
- generic [ref=e47]: 协作
- generic [ref=e52]:
- generic [ref=e53] [cursor=pointer]:
- img [ref=e55]
- generic [ref=e58]:
- generic [ref=e60]: ZCLAW
- paragraph [ref=e61]: 默认助手
- generic [ref=e62] [cursor=pointer]:
- img [ref=e64]
- generic [ref=e67]: 创建新 Agent
- button "用 用户7141" [ref=e69]:
- generic [ref=e70]:
- generic [ref=e71]: 用户7141
- img [ref=e72]
- generic [ref=e74]:
- banner [ref=e75]:
- generic [ref=e76]:
- generic [ref=e78]: Z
- generic [ref=e79]: ZCLAW
- button "详情" [ref=e81]:
- img [ref=e82]
- generic [ref=e85]: 详情
- main [ref=e86]:
- generic [ref=e87]:
- generic [ref=e89]:
- heading "ZCLAW" [level=2] [ref=e90]
- generic [ref=e91]: Gateway 已连接
- generic [ref=e96]:
- img [ref=e98]
- heading "欢迎使用 ZCLAW" [level=3] [ref=e100]
- paragraph [ref=e101]: 发送消息开始对话
- generic [ref=e103]:
- generic [ref=e104]:
- button "添加附件" [ref=e105]:
- img [ref=e106]
- textbox "发送给 ZCLAW" [ref=e109]
- generic [ref=e110]:
- button "选择模型" [ref=e111]:
- generic [ref=e112]: claude-sonnet-4-20250514
- img [ref=e113]
- button "发送消息" [disabled] [ref=e115]:
- img [ref=e116]
- generic [ref=e118]: Agent 在本地运行,内容由 AI 生成
```

View File

@@ -1,41 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- generic [ref=e4]:
- generic [ref=e5]:
- img [ref=e7]
- generic [ref=e10]:
- heading "创建新 Agent" [level=2] [ref=e11]
- paragraph [ref=e12]: "步骤 1/5: 认识用户"
- button [ref=e13]:
- img [ref=e14]
- generic [ref=e18]:
- button [disabled] [ref=e20]:
- img [ref=e21]
- button [disabled] [ref=e26]:
- img [ref=e27]
- button [disabled] [ref=e32]:
- img [ref=e33]
- button [disabled] [ref=e38]:
- img [ref=e39]
- button [disabled] [ref=e44]:
- img [ref=e45]
- generic [ref=e48]:
- generic [ref=e49]:
- heading "让我们认识一下" [level=3] [ref=e50]
- paragraph [ref=e51]: 请告诉我们您的名字,让助手更好地为您服务
- generic [ref=e52]:
- generic [ref=e53]: 您的名字 *
- textbox "例如:张三" [ref=e54]
- generic [ref=e55]:
- generic [ref=e56]: 您的角色(可选)
- textbox "例如:产品经理、开发工程师" [ref=e57]
- generic [ref=e58]:
- button "上一步" [disabled] [ref=e59]:
- img [ref=e60]
- text: 上一步
- button "下一步" [ref=e63]:
- text: 下一步
- img [ref=e64]
```

View File

@@ -1,41 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- generic [ref=e4]:
- generic [ref=e5]:
- img [ref=e7]
- generic [ref=e10]:
- heading "创建新 Agent" [level=2] [ref=e11]
- paragraph [ref=e12]: "步骤 1/5: 认识用户"
- button [ref=e13]:
- img [ref=e14]
- generic [ref=e18]:
- button [disabled] [ref=e20]:
- img [ref=e21]
- button [disabled] [ref=e26]:
- img [ref=e27]
- button [disabled] [ref=e32]:
- img [ref=e33]
- button [disabled] [ref=e38]:
- img [ref=e39]
- button [disabled] [ref=e44]:
- img [ref=e45]
- generic [ref=e48]:
- generic [ref=e49]:
- heading "让我们认识一下" [level=3] [ref=e50]
- paragraph [ref=e51]: 请告诉我们您的名字,让助手更好地为您服务
- generic [ref=e52]:
- generic [ref=e53]: 您的名字 *
- textbox "例如:张三" [ref=e54]
- generic [ref=e55]:
- generic [ref=e56]: 您的角色(可选)
- textbox "例如:产品经理、开发工程师" [ref=e57]
- generic [ref=e58]:
- button "上一步" [disabled] [ref=e59]:
- img [ref=e60]
- text: 上一步
- button "下一步" [ref=e63]:
- text: 下一步
- img [ref=e64]
```