diff --git a/README.md b/README.md index 2f382e1..6e50842 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,44 @@ -# ZCLAW 🦞 — OpenClaw 定制版 (Tauri Desktop) +# ZCLAW 🦞 — OpenFang 定制版 (Tauri Desktop) -像 AutoClaw (智谱) 和 QClaw (腾讯) 一样,对 [OpenClaw](https://github.com/openclaw/openclaw) 进行定制化封装,打造中文优先的 Tauri 桌面 AI 助手。 +基于 [OpenFang](https://openfang.sh/) —— 用 Rust 构建的 Agent 操作系统,打造中文优先的 Tauri 桌面 AI 助手。 ## 核心定位 ``` -OpenClaw Gateway (执行引擎) - ↕ WebSocket +OpenFang Kernel (Rust 执行引擎) + ↕ WebSocket / HTTP API ZCLAW Tauri App (桌面 UI) - + 中文模型 Provider (GLM/Qwen/Kimi/MiniMax) - + 飞书 Channel Plugin + + 中文模型 Provider (GLM/Qwen/Kimi/MiniMax/DeepSeek) + + 7 个自主 Hands (Browser/Researcher/Collector 等) + + 40+ 渠道适配器 (飞书/钉钉/Telegram/Discord 等) + + 16 层安全防护 + 分身(Clone) 管理 + 自定义 Skills ``` +## 为什么选择 OpenFang? + +相比 OpenClaw,OpenFang 提供了更强的性能和更丰富的功能: + +| 特性 | OpenFang | OpenClaw | +|------|----------|----------| +| **开发语言** | Rust | TypeScript | +| **冷启动** | < 200ms | ~6s | +| **内存占用** | ~40MB | ~394MB | +| **安全层数** | 16 层 | 3 层基础 | +| **自主 Hands** | 7 个内置 | 无 | +| **渠道适配器** | 40 个 | 13 个 | +| **LLM 提供商** | 27 个 | ~10 个 | + +**详细对比**:[OpenFang 架构概览](https://wurang.net/posts/openfang-intro/) + ## 功能特色 -- **基于 OpenClaw**: 真实工具执行 (bash/file/browser)、Skills 生态、MCP 协议、心跳引擎 -- **中文模型**: 智谱 GLM-5、通义千问、Kimi K2.5、MiniMax (OpenAI 兼容 API) -- **飞书集成**: 飞书 Channel Plugin,在飞书中直接对话指挥电脑 +- **基于 OpenFang**: 生产级 Agent 操作系统,16 层安全防护,WASM 沙箱 +- **7 个自主 Hands**: Browser/Researcher/Collector/Predictor/Lead/Clip/Twitter - 预构建的"数字员工" +- **中文模型**: 智谱 GLM-4、通义千问、Kimi、MiniMax、DeepSeek (OpenAI 兼容 API) +- **40+ 渠道**: 飞书、钉钉、Telegram、Discord、Slack、微信等 +- **60+ 技能**: 内置技能包 + 自定义 SKILL.md - **分身系统**: 多个独立 Agent 实例,各有自己的角色、记忆、配置 - **Tauri 桌面**: Rust + React 19,体积小 (~10MB),性能好 - **设置页面**: 对标 AutoClaw — 通用/模型/MCP/技能/IM/工作区/隐私 @@ -27,11 +47,11 @@ ZCLAW Tauri App (桌面 UI) | 层级 | 技术 | |------|------| -| **执行引擎** | OpenClaw Gateway (Node.js, ws://127.0.0.1:18789) | +| **执行引擎** | OpenFang Kernel (Rust, http://127.0.0.1:50051) | | **桌面壳** | Tauri 2.0 (Rust + React 19) | | **前端** | React 19 + TailwindCSS + Zustand + Lucide Icons | -| **自定义插件** | TypeScript (OpenClaw Plugin API) | -| **通信协议** | OpenClaw Gateway WebSocket Protocol v3 | +| **通信协议** | OpenFang API (REST/WS/SSE) + OpenAI 兼容 API | +| **安全** | WASM 沙箱 + Merkle 审计追踪 + Ed25519 签名 | ## 项目结构 @@ -41,86 +61,147 @@ ZClaw/ │ ├── src/ │ │ ├── components/ # UI 组件 │ │ ├── store/ # Zustand 状态管理 -│ │ └── lib/gateway-client.ts # Gateway WebSocket 客户端 -│ └── src-tauri/ # Rust 后端 (TODO) +│ │ └── lib/gateway-client.ts # OpenFang API 客户端 +│ └── src-tauri/ # Rust 后端 │ -├── src/gateway/ # Gateway 管理层 -│ ├── manager.ts # OpenClaw 子进程管理 -│ ├── ws-client.ts # Node.js WebSocket 客户端 -│ └── index.ts -│ -├── plugins/ # ZCLAW 自定义 OpenClaw 插件 -│ ├── zclaw-chinese-models/ # 中文模型 Provider (GLM/Qwen/Kimi/MiniMax) -│ ├── zclaw-feishu/ # 飞书 Channel Plugin -│ └── zclaw-ui/ # UI 扩展 RPC 方法 -│ -├── skills/ # 自定义 Skills +├── skills/ # 自定义技能 (SKILL.md) │ ├── chinese-writing/ # 中文写作 │ └── feishu-docs/ # 飞书文档操作 │ -├── config/ # OpenClaw 默认配置 -│ ├── openclaw.default.json # Gateway 配置模板 -│ ├── SOUL.md # Agent 人格 -│ ├── AGENTS.md # Agent 指令 -│ ├── IDENTITY.md # Agent 身份 -│ └── USER.md # 用户偏好 +├── hands/ # 自定义 Hands (HAND.toml) +│ └── custom-automation/ # 自定义自动化任务 │ -├── scripts/setup.ts # 首次设置脚本 -├── docs/ # 文档 +├── config/ # OpenFang 默认配置 +│ ├── config.toml # 主配置文件 +│ ├── SOUL.md # Agent 人格 +│ └── AGENTS.md # Agent 指令 +│ +├── docs/ +│ ├── setup/ # 设置指南 +│ │ ├── OPENFANG-SETUP.md # OpenFang 配置指南 +│ │ └── chinese-models.md # 中文模型配置 │ ├── architecture-v2.md # 架构设计 -│ ├── deviation-analysis.md # 偏离分析报告 -│ └── autoclaw界面/ # AutoClaw 参考截图 -└── src/core/ # [归档] v1 旧代码 +│ └── deviation-analysis.md # 偏离分析报告 +│ +└── scripts/setup.ts # 首次设置脚本 ``` ## 快速开始 -### 1. 安装 OpenClaw +### 1. 安装 OpenFang ```bash -# Windows -iwr -useb https://openclaw.ai/install.ps1 | iex +# Windows (PowerShell) +iwr -useb https://openfang.sh/install.ps1 | iex # macOS / Linux -curl -fsSL https://openclaw.ai/install.sh | bash +curl -fsSL https://openfang.sh/install.sh | bash ``` -### 2. 安装 ZCLAW +### 2. 初始化配置 ```bash -git clone https://github.com/xxx/ZClaw.git -cd ZClaw -pnpm install -pnpm setup # 注册插件 + 复制配置 +openfang init ``` ### 3. 配置 API Key ```bash -openclaw configure # 交互式配置 -# 或手动编辑 ~/.openclaw/openclaw.json +# 设置智谱 API Key (推荐,有免费额度) +export ZHIPU_API_KEY="your-api-key" + +# 或其他中文模型 +export DASHSCOPE_API_KEY="your-dashscope-key" # 通义千问 +export MOONSHOT_API_KEY="your-moonshot-key" # Kimi +export DEEPSEEK_API_KEY="your-deepseek-key" # DeepSeek ``` -### 4. 启动 +**获取 API Key**:参考 [中文模型配置指南](docs/setup/chinese-models.md) + +### 4. 启动服务 ```bash -openclaw gateway # 启动 OpenClaw Gateway -cd desktop && pnpm tauri dev # 启动 Tauri 桌面应用 +# 启动 OpenFang Kernel +openfang start + +# 在另一个终端启动 ZCLAW 桌面应用 +git clone https://github.com/xxx/ZClaw.git +cd ZClaw +pnpm install +cd desktop && pnpm tauri dev ``` -## 对标参考 +### 5. 验证安装 -| 产品 | 基于 | IM 渠道 | 桌面框架 | -|------|------|---------|----------| -| **QClaw** (腾讯) | OpenClaw | 微信 + QQ | Electron | -| **AutoClaw** (智谱) | OpenClaw | 飞书 | 自研 | -| **ZCLAW** (本项目) | OpenClaw | 飞书 (+ 微信/QQ 计划中) | Tauri 2.0 | +```bash +# 检查 OpenFang 状态 +openfang status + +# 运行健康检查 +openfang doctor +``` + +## OpenFang Hands (自主能力) + +OpenFang 内置 7 个预构建的自主能力包,每个 Hand 都是一个具备完整工作流的"数字员工": + +| Hand | 功能 | 状态 | +|------|------|------| +| **Browser** | 网页自动化,Playwright 驱动 | 可用 | +| **Researcher** | 深度研究,交叉验证,APA 引用 | 可用 | +| **Collector** | 情报监控,OSINT 级持续监控 | 可用 | +| **Predictor** | 趋势预测,带置信区间的预测 | 可用 | +| **Lead** | 线索挖掘,ICP 匹配,评分去重 | 可用 | +| **Clip** | 视频处理,下载剪辑,字幕生成 | 需 FFmpeg | +| **Twitter** | 社媒管理,内容创建,排期发布 | 需 API Key | + +## 支持的中文模型 + +| 提供商 | 模型 | 特点 | 免费额度 | +|--------|------|------|----------| +| **智谱 AI** | GLM-4-Flash | 快速响应 | 1000 万 tokens | +| **阿里云** | 通义千问 | 性价比高 | 有试用 | +| **月之暗面** | Kimi | 200K 长上下文 | 15 元体验金 | +| **DeepSeek** | DeepSeek-Chat | 编程能力强 | 低价 | +| **MiniMax** | 海螺 AI | 语音能力强 | 有试用 | + +详细配置请参考 [中文模型配置指南](docs/setup/chinese-models.md) ## 文档 +### 设置指南 +- [OpenFang Kernel 配置指南](docs/setup/OPENFANG-SETUP.md) - 安装、配置、常见问题 +- [中文模型配置指南](docs/setup/chinese-models.md) - API Key 获取、模型选择、多模型配置 + +### 架构设计 - [架构设计](docs/architecture-v2.md) — 完整的 v2 架构方案 - [偏离分析](docs/deviation-analysis.md) — 与 QClaw/AutoClaw/OpenClaw 对标分析 +### 外部资源 +- [OpenFang 官方文档](https://openfang.sh/) +- [OpenFang GitHub](https://github.com/RightNow-AI/openfang) +- [OpenFang 架构概览](https://wurang.net/posts/openfang-intro/) + +## 对标参考 + +| 产品 | 基于 | IM 渠道 | 桌面框架 | 安全层数 | +|------|------|---------|----------|----------| +| **QClaw** (腾讯) | OpenClaw | 微信 + QQ | Electron | 3 | +| **AutoClaw** (智谱) | OpenClaw | 飞书 | 自研 | 3 | +| **ZCLAW** (本项目) | OpenFang | 飞书 + 钉钉 + 40+ | Tauri 2.0 | 16 | + +## 从 OpenClaw 迁移 + +如果你之前使用 OpenClaw,可以一键迁移: + +```bash +# 迁移所有内容:代理、记忆、技能、配置 +openfang migrate --from openclaw + +# 先试运行查看变更 +openfang migrate --from openclaw --dry-run +``` + ## License MIT diff --git a/desktop/src/components/AuditLogsPanel.tsx b/desktop/src/components/AuditLogsPanel.tsx index 86a0564..3e1c102 100644 --- a/desktop/src/components/AuditLogsPanel.tsx +++ b/desktop/src/components/AuditLogsPanel.tsx @@ -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 diff --git a/desktop/src/components/Automation/AutomationPanel.tsx b/desktop/src/components/Automation/AutomationPanel.tsx index a53818c..d5896c3 100644 --- a/desktop/src/components/Automation/AutomationPanel.tsx +++ b/desktop/src/components/Automation/AutomationPanel.tsx @@ -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(initialCategory); diff --git a/desktop/src/components/ChannelList.tsx b/desktop/src/components/ChannelList.tsx index 46952af..7850619 100644 --- a/desktop/src/components/ChannelList.tsx +++ b/desktop/src/components/ChannelList.tsx @@ -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 = { @@ -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'; diff --git a/desktop/src/components/ChatArea.tsx b/desktop/src/components/ChatArea.tsx index d4bfca0..0a1eec0 100644 --- a/desktop/src/components/ChatArea.tsx +++ b/desktop/src/components/ChatArea.tsx @@ -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); diff --git a/desktop/src/components/CreateTriggerModal.tsx b/desktop/src/components/CreateTriggerModal.tsx index 9631983..43ce501 100644 --- a/desktop/src/components/CreateTriggerModal.tsx +++ b/desktop/src/components/CreateTriggerModal.tsx @@ -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(initialFormData); const [errors, setErrors] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/desktop/src/components/RightPanel.tsx b/desktop/src/components/RightPanel.tsx index 40abc34..56a5dc6 100644 --- a/desktop/src/components/RightPanel.tsx +++ b/desktop/src/components/RightPanel.tsx @@ -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'); diff --git a/desktop/src/components/SchedulerPanel.tsx b/desktop/src/components/SchedulerPanel.tsx index f7e59e7..9a843c5 100644 --- a/desktop/src/components/SchedulerPanel.tsx +++ b/desktop/src/components/SchedulerPanel.tsx @@ -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(initialFormData); const [errors, setErrors] = useState>({}); 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('scheduled'); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isWorkflowEditorOpen, setIsWorkflowEditorOpen] = useState(false); diff --git a/desktop/src/components/SecurityLayersPanel.tsx b/desktop/src/components/SecurityLayersPanel.tsx index d7ca413..f668687 100644 --- a/desktop/src/components/SecurityLayersPanel.tsx +++ b/desktop/src/components/SecurityLayersPanel.tsx @@ -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(getDefaultSecurityStatus()); const [refreshing, setRefreshing] = useState(false); diff --git a/desktop/src/components/SecurityStatus.tsx b/desktop/src/components/SecurityStatus.tsx index a8ed94a..1909b21 100644 --- a/desktop/src/components/SecurityStatus.tsx +++ b/desktop/src/components/SecurityStatus.tsx @@ -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 = { @@ -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(() => { diff --git a/desktop/src/components/Settings/General.tsx b/desktop/src/components/Settings/General.tsx index c061955..1635b5e 100644 --- a/desktop/src/components/Settings/General.tsx +++ b/desktop/src/components/Settings/General.tsx @@ -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); diff --git a/desktop/src/components/Settings/IMChannels.tsx b/desktop/src/components/Settings/IMChannels.tsx index 0f8b816..9a8ab40 100644 --- a/desktop/src/components/Settings/IMChannels.tsx +++ b/desktop/src/components/Settings/IMChannels.tsx @@ -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 = { feishu: '飞', @@ -9,7 +11,10 @@ const CHANNEL_ICONS: Record = { }; 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'; diff --git a/desktop/src/components/Settings/ModelsAPI.tsx b/desktop/src/components/Settings/ModelsAPI.tsx index 4adf3cc..fdf6735 100644 --- a/desktop/src/components/Settings/ModelsAPI.tsx +++ b/desktop/src/components/Settings/ModelsAPI.tsx @@ -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()); diff --git a/desktop/src/components/Settings/SettingsLayout.tsx b/desktop/src/components/Settings/SettingsLayout.tsx index 20328b8..2060bcd 100644 --- a/desktop/src/components/Settings/SettingsLayout.tsx +++ b/desktop/src/components/Settings/SettingsLayout.tsx @@ -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: }, { id: 'audit', label: '审计日志', icon: }, { id: 'tasks', label: '定时任务', icon: }, + { id: 'heartbeat', label: '心跳配置', icon: }, { id: 'feedback', label: '提交反馈', icon: }, { id: 'about', label: '关于', icon: }, ]; export function SettingsLayout({ onBack }: SettingsLayoutProps) { const [activePage, setActivePage] = useState('general'); - const { securityStatus } = useGatewayStore(); + const securityStatus = useSecurityStore((s) => s.securityStatus); const renderPage = () => { switch (activePage) { @@ -112,6 +116,11 @@ export function SettingsLayout({ onBack }: SettingsLayoutProps) { ); + case 'heartbeat': return ( +
+ +
+ ); case 'feedback': return ; case 'about': return ; default: return ; diff --git a/desktop/src/components/Settings/Skills.tsx b/desktop/src/components/Settings/Skills.tsx index 226d15e..1be3aca 100644 --- a/desktop/src/components/Settings/Skills.tsx +++ b/desktop/src/components/Settings/Skills.tsx @@ -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'); diff --git a/desktop/src/components/SkillMarket.tsx b/desktop/src/components/SkillMarket.tsx index d10993b..fa83717 100644 --- a/desktop/src/components/SkillMarket.tsx +++ b/desktop/src/components/SkillMarket.tsx @@ -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 ( -
-
- - - {suggestion.skill.name} - - - {confidencePercent}% 匹配 - -
-

{suggestion.reason}

-
- {suggestion.matchedPatterns.map((pattern) => ( - - {pattern} - - ))} -
-
- ); -} - // === Main Component === export function SkillMarket({ @@ -276,19 +244,23 @@ export function SkillMarket({ onSkillInstall, onSkillUninstall, }: SkillMarketProps) { - const [engine] = useState(() => new SkillDiscoveryEngine()); - const [skills, setSkills] = useState([]); + // 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('all'); const [expandedSkillId, setExpandedSkillId] = useState(null); - const [suggestions, setSuggestions] = useState([]); 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 (
@@ -405,25 +368,8 @@ export function SkillMarket({ />
- {/* Suggestions */} - - {suggestions.length > 0 && ( - -

- - 推荐技能 -

- {suggestions.map((suggestion) => ( - - ))} -
- )} -
+ {/* Suggestions - placeholder for future AI-powered recommendations */} + {/* Category Filter */} diff --git a/desktop/src/components/TaskList.tsx b/desktop/src/components/TaskList.tsx index 8f089a5..92c2a19 100644 --- a/desktop/src/components/TaskList.tsx +++ b/desktop/src/components/TaskList.tsx @@ -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 = { @@ -10,7 +11,9 @@ const STATUS_CONFIG: Record s.scheduledTasks); + const connectionState = useConnectionStore((s) => s.connectionState); + const loadScheduledTasks = useConfigStore((s) => s.loadScheduledTasks); const connected = connectionState === 'connected'; diff --git a/desktop/src/components/TeamList.tsx b/desktop/src/components/TeamList.tsx index 41b7871..fb58cc4 100644 --- a/desktop/src/components/TeamList.tsx +++ b/desktop/src/components/TeamList.tsx @@ -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(''); diff --git a/desktop/src/components/TriggersPanel.tsx b/desktop/src/components/TriggersPanel.tsx index 040716b..49565e8 100644 --- a/desktop/src/components/TriggersPanel.tsx +++ b/desktop/src/components/TriggersPanel.tsx @@ -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(null); const [deletingTrigger, setDeletingTrigger] = useState(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); diff --git a/desktop/src/components/WorkflowEditor.tsx b/desktop/src/components/WorkflowEditor.tsx index e372969..d4d2d66 100644 --- a/desktop/src/components/WorkflowEditor.tsx +++ b/desktop/src/components/WorkflowEditor.tsx @@ -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([]); diff --git a/desktop/src/lib/skill-adapter.ts b/desktop/src/lib/skill-adapter.ts new file mode 100644 index 0000000..57c278e --- /dev/null +++ b/desktop/src/lib/skill-adapter.ts @@ -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 = { + 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(); + + 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(); + for (const skill of skills) { + if (skill.category) { + categories.add(skill.category); + } + } + return Array.from(categories); +} diff --git a/desktop/src/store/connectionStore.ts b/desktop/src/store/connectionStore.ts index 6a74131..aa5b02a 100644 --- a/desktop/src/store/connectionStore.ts +++ b/desktop/src/store/connectionStore.ts @@ -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 { diff --git a/desktop/tests/e2e/fixtures/mock-gateway.ts b/desktop/tests/e2e/fixtures/mock-gateway.ts index 12ac00f..f9991e8 100644 --- a/desktop/tests/e2e/fixtures/mock-gateway.ts +++ b/desktop/tests/e2e/fixtures/mock-gateway.ts @@ -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 错误响应 */ diff --git a/desktop/tests/e2e/test-results/artifacts/.last-run.json b/desktop/tests/e2e/test-results/artifacts/.last-run.json index 0a71296..7c7fe30 100644 --- a/desktop/tests/e2e/test-results/artifacts/.last-run.json +++ b/desktop/tests/e2e/test-results/artifacts/.last-run.json @@ -1,7 +1,10 @@ { "status": "failed", "failedTests": [ - "bdcac940a81c3235ce13-8b134df5feeb02852417", - "bdcac940a81c3235ce13-6df5d90e5b85ad4debff" + "4c7e6ccba74c38082eff-428751c1a27c810d25bc", + "4c7e6ccba74c38082eff-e8eb267dbe5d6b944c33", + "4c7e6ccba74c38082eff-1e63c4d4b91978536fc3", + "4c7e6ccba74c38082eff-5a47c30a876dee9d7f6a", + "4c7e6ccba74c38082eff-d595d4fb8c9beec7acb6" ] } \ No newline at end of file diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/error-context.md b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/error-context.md new file mode 100644 index 0000000..5849def --- /dev/null +++ b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/error-context.md @@ -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 生成 +``` \ No newline at end of file diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/test-failed-1.png b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/test-failed-1.png new file mode 100644 index 0000000..62712b1 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/test-failed-1.png differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/video.webm b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/video.webm new file mode 100644 index 0000000..28be71e Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-14e99-handling-for-failed-message-chromium/video.webm differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/error-context.md b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/error-context.md new file mode 100644 index 0000000..00d4874 --- /dev/null +++ b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/error-context.md @@ -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 生成 +``` \ No newline at end of file diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/test-failed-1.png b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/test-failed-1.png new file mode 100644 index 0000000..5b8c993 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/test-failed-1.png differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/video.webm b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/video.webm new file mode 100644 index 0000000..87cc2e3 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-4d9ad-treaming-response-indicator-chromium/video.webm differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/error-context.md b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/error-context.md new file mode 100644 index 0000000..eaf7cdf --- /dev/null +++ b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/error-context.md @@ -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 生成 +``` \ No newline at end of file diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/test-failed-1.png b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/test-failed-1.png new file mode 100644 index 0000000..f984478 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/test-failed-1.png differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/video.webm b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/video.webm new file mode 100644 index 0000000..8b25be5 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-ac049-Message-updates-store-state-chromium/video.webm differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/error-context.md b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/error-context.md new file mode 100644 index 0000000..758fc28 --- /dev/null +++ b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/error-context.md @@ -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 生成 +``` \ No newline at end of file diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/test-failed-1.png b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/test-failed-1.png new file mode 100644 index 0000000..c76d620 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/test-failed-1.png differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/video.webm b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/video.webm new file mode 100644 index 0000000..0b72956 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Chat-Message-e8112-ltiple-messages-in-sequence-chromium/video.webm differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/error-context.md b/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/error-context.md new file mode 100644 index 0000000..847b4d1 --- /dev/null +++ b/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/error-context.md @@ -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 生成 +``` \ No newline at end of file diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/test-failed-1.png b/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/test-failed-1.png new file mode 100644 index 0000000..a4f4f10 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/test-failed-1.png differ diff --git a/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/video.webm b/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/video.webm new file mode 100644 index 0000000..2509c43 Binary files /dev/null and b/desktop/tests/e2e/test-results/artifacts/core-features-Hands-Trigge-a8aa4--TRIG-06-Hand-approval-flow-chromium/video.webm differ diff --git a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/error-context.md b/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/error-context.md deleted file mode 100644 index 881d4d3..0000000 --- a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/error-context.md +++ /dev/null @@ -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] -``` \ No newline at end of file diff --git a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/test-failed-1.png b/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/test-failed-1.png deleted file mode 100644 index b79bfee..0000000 Binary files a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/test-failed-1.png and /dev/null differ diff --git a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/video.webm b/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/video.webm deleted file mode 100644 index 4b9df86..0000000 Binary files a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-01-打开设置数据流-chromium/video.webm and /dev/null differ diff --git a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/error-context.md b/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/error-context.md deleted file mode 100644 index 881d4d3..0000000 --- a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/error-context.md +++ /dev/null @@ -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] -``` \ No newline at end of file diff --git a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/test-failed-1.png b/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/test-failed-1.png deleted file mode 100644 index b79bfee..0000000 Binary files a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/test-failed-1.png and /dev/null differ diff --git a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/video.webm b/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/video.webm deleted file mode 100644 index b2934d3..0000000 Binary files a/desktop/tests/e2e/test-results/artifacts/data-flow-设置数据流验证-SET-DF-02-模型配置数据流-chromium/video.webm and /dev/null differ diff --git a/docs/plans/skill-market-mvp.md b/docs/plans/skill-market-mvp.md new file mode 100644 index 0000000..741798d --- /dev/null +++ b/docs/plans/skill-market-mvp.md @@ -0,0 +1,381 @@ +# Skill Market MVP 实现计划 + +> **创建日期**: 2026-03-20 +> **状态**: 研究完成,待实现 +> **优先级**: P1 - 中期计划 (M2) + +--- + +## 一、背景研究 + +### 1.1 现有代码分析 + +经过代码审查,发现以下关键组件已存在但需要集成和完善: + +#### 已存在组件 + +| 组件 | 路径 | 状态 | 问题 | +|------|------|------|------| +| SkillMarket.tsx | `desktop/src/components/SkillMarket.tsx` | 已实现 | 使用本地 SkillDiscoveryEngine,未接入后端 | +| skillMarketStore.ts | `desktop/src/store/skillMarketStore.ts` | 已实现 | 使用硬编码技能列表,未接入 configStore | +| skill-discovery.ts | `desktop/src/lib/skill-discovery.ts` | 已实现 | 使用 localStorage + 静态技能注册表 | +| skill-market.ts (类型) | `desktop/src/types/skill-market.ts` | 已实现 | Skill/SkillReview/SkillMarketState 类型 | +| configStore.ts | `desktop/src/store/configStore.ts` | 已实现 | 包含 skillsCatalog 和技能 CRUD 方法 | +| gateway-api.ts | `desktop/src/lib/gateway-api.ts` | 已实现 | 包含 listSkills/getSkill/createSkill 等方法 | + +#### 技能定义文件 + +- `skills/` 目录包含 **74 个 SKILL.md** 文件 +- 格式: YAML frontmatter + Markdown 正文 +- 包含: name, description, triggers, tools, capabilities 等 + +### 1.2 当前架构问题 + +1. **双轨系统**: `SkillMarket.tsx` 使用 `SkillDiscoveryEngine`,但 `configStore.ts` 已有完整的技能管理逻辑 +2. **硬编码技能**: `skillMarketStore.ts` 的 `scanSkillsDirectory()` 返回硬编码列表,未读取实际 SKILL.md +3. **后端未连接**: 组件未通过 `GatewayClient` 与后端 `/api/skills` 通信 +4. **安装状态不同步**: `skillMarketStore` 和 `configStore` 各自维护 `installed` 状态 + +### 1.3 数据流现状 + +``` +当前(问题): +SkillMarket.tsx → SkillDiscoveryEngine → localStorage + ↓ + BUILT_IN_SKILLS (硬编码 12 个) + +configStore.ts → GatewayClient → /api/skills (后端) + ↓ + skillsCatalog (从后端获取) +``` + +--- + +## 二、MVP 功能范围 + +### 2.1 核心功能 + +| 功能 | 优先级 | 描述 | +|------|--------|------| +| 技能浏览 | P0 | 展示所有可用技能,支持分类筛选 | +| 技能搜索 | P0 | 按名称、触发词、能力搜索 | +| 技能详情 | P0 | 展示技能详细信息(能力、触发词、工具依赖) | +| 安装/卸载 | P0 | 切换技能启用状态,持久化到后端 | +| 分类过滤 | P1 | 按类别筛选技能 | + +### 2.2 MVP 不包含 + +- 技能评分和评论(已有代码但 MVP 不启用) +- 技能推荐引擎(suggestSkills 功能) +- 技能市场在线同步 +- 技能版本管理 +- 自定义技能创建 + +### 2.3 技能卡片信息 + +每张技能卡片需要展示: + +``` +┌─────────────────────────────────────┐ +│ [图标] 技能名称 [已安装?] │ +│ 简短描述(1-2 行) │ +│ ┌─────────┐ ┌──────────┐ │ +│ │ 分类标签 │ │ 能力标签 │ ... │ +│ └─────────┘ └──────────┘ │ +│ │ +│ 展开后: │ +│ 触发词: [词1] [词2] [词3] │ +│ 能力: [能力1] [能力2] [能力3] │ +│ 工具依赖: [read] [write] [bash] │ +│ │ +│ [安装] 或 [卸载] │ +└─────────────────────────────────────┘ +``` + +--- + +## 三、技术架构 + +### 3.1 目标数据流 + +``` +目标架构: +SkillMarket.tsx → useConfigStore → GatewayClient → /api/skills + ↓ + skillsCatalog (统一来源) + ↓ + 安装状态 → GatewayClient.updateSkill() → 后端 +``` + +### 3.2 组件依赖关系 + +``` +App.tsx + └── SkillMarket.tsx (重构) + ├── useConfigStore (替代 SkillDiscoveryEngine) + ├── SkillCard (保留,增强) + ├── CategoryBadge (保留) + └── SuggestionCard (MVP 隐藏) +``` + +### 3.3 类型统一 + +使用 `configStore.ts` 中的 `SkillInfo` 类型作为标准: + +```typescript +interface SkillInfo { + id: string; + name: string; + path: string; + source: 'builtin' | 'extra'; + description?: string; + triggers?: Array<{ type: string; pattern?: string }>; + actions?: Array<{ type: string; params?: Record }>; + enabled?: boolean; +} +``` + +需要与 `skill-market.ts` 中的 `Skill` 类型做适配: + +```typescript +// 适配映射 +function adaptSkillInfo(info: SkillInfo): Skill { + return { + id: info.id, + name: info.name, + description: info.description || '', + triggers: info.triggers?.map(t => t.pattern || t.type) || [], + capabilities: info.actions?.map(a => a.type) || [], + category: extractCategory(info.path), + installed: info.enabled || false, + }; +} +``` + +--- + +## 四、实现步骤 + +### Phase 1: Store 统一 (1-2 天) + +**目标**: 让 SkillMarket 使用 configStore 而非 SkillDiscoveryEngine + +#### 任务清单 + +- [ ] 1.1 分析 configStore.skillsCatalog 返回的数据结构 +- [ ] 1.2 创建类型适配器 `adaptSkillInfo()` +- [ ] 1.3 修改 SkillMarket.tsx 使用 `useConfigStore` +- [ ] 1.4 移除对 SkillDiscoveryEngine 的直接依赖 +- [ ] 1.5 测试技能列表加载 + +#### 文件变更 + +| 文件 | 操作 | +|------|------| +| `desktop/src/components/SkillMarket.tsx` | 修改 | +| `desktop/src/lib/skill-adapter.ts` | 新建 | + +### Phase 2: 后端集成 (2-3 天) + +**目标**: 实现真正的安装/卸载功能 + +#### 任务清单 + +- [ ] 2.1 验证 `/api/skills` 端点可用性 +- [ ] 2.2 实现 `installSkill()` 调用 `updateSkill(id, { enabled: true })` +- [ ] 2.3 实现 `uninstallSkill()` 调用 `updateSkill(id, { enabled: false })` +- [ ] 2.4 添加加载状态和错误处理 +- [ ] 2.5 添加乐观更新和回滚机制 + +#### 文件变更 + +| 文件 | 操作 | +|------|------| +| `desktop/src/store/configStore.ts` | 修改(增强 updateSkill) | +| `desktop/src/components/SkillMarket.tsx` | 修改(安装/卸载逻辑) | + +### Phase 3: 搜索和分类 (1-2 天) + +**目标**: 完善搜索和分类过滤功能 + +#### 任务清单 + +- [ ] 3.1 实现前端搜索过滤(基于 skillsCatalog) +- [ ] 3.2 从技能列表提取所有分类 +- [ ] 3.3 实现分类过滤 UI +- [ ] 3.4 优化搜索性能(防抖、缓存) + +#### 文件变更 + +| 文件 | 操作 | +|------|------| +| `desktop/src/components/SkillMarket.tsx` | 修改 | +| `desktop/src/hooks/useSkillSearch.ts` | 新建(可选) | + +### Phase 4: UI 完善 (1-2 天) + +**目标**: 提升用户体验 + +#### 任务清单 + +- [ ] 4.1 优化技能卡片展开动画 +- [ ] 4.2 添加安装/卸载确认提示 +- [ ] 4.3 添加空状态展示 +- [ ] 4.4 添加加载骨架屏 +- [ ] 4.5 优化响应式布局 + +#### 文件变更 + +| 文件 | 操作 | +|------|------| +| `desktop/src/components/SkillMarket.tsx` | 修改 | +| `desktop/src/components/SkillCardSkeleton.tsx` | 新建 | + +### Phase 5: 测试和文档 (1 天) + +**目标**: 确保质量和可维护性 + +#### 任务清单 + +- [ ] 5.1 添加单元测试 +- [ ] 5.2 添加集成测试 +- [ ] 5.3 更新功能文档 +- [ ] 5.4 更新用户手册 + +#### 文件变更 + +| 文件 | 操作 | +|------|------| +| `tests/desktop/skill-market.test.ts` | 新建 | +| `docs/features/04-skills-ecosystem/02-skill-discovery.md` | 更新 | + +--- + +## 五、文件变更清单 + +### 新建文件 + +| 文件路径 | 用途 | +|----------|------| +| `desktop/src/lib/skill-adapter.ts` | SkillInfo <-> Skill 类型适配 | +| `desktop/src/hooks/useSkillSearch.ts` | 技能搜索 hook(可选) | +| `desktop/src/components/SkillCardSkeleton.tsx` | 加载骨架屏 | +| `tests/desktop/skill-market.test.ts` | 测试文件 | + +### 修改文件 + +| 文件路径 | 变更内容 | +|----------|----------| +| `desktop/src/components/SkillMarket.tsx` | 使用 configStore,优化 UI | +| `desktop/src/store/configStore.ts` | 增强 updateSkill,添加乐观更新 | +| `docs/features/04-skills-ecosystem/02-skill-discovery.md` | 更新文档 | +| `docs/features/README.md` | 更新集成状态 | + +### 可删除文件 + +| 文件路径 | 原因 | +|----------|------| +| `desktop/src/store/skillMarketStore.ts` | 合并到 configStore 后废弃 | +| `desktop/src/types/skill-market.ts` | 使用 configStore 类型替代 | + +--- + +## 六、风险和缓解 + +### 6.1 技术风险 + +| 风险 | 可能性 | 影响 | 缓解措施 | +|------|--------|------|----------| +| 后端 `/api/skills` 未返回完整数据 | 中 | 高 | 添加 fallback 到本地技能列表 | +| 类型不兼容 | 低 | 中 | 创建适配器层 | +| 性能问题(74个技能渲染) | 低 | 低 | 虚拟滚动(后期优化) | + +### 6.2 用户体验风险 + +| 风险 | 可能性 | 影响 | 缓解措施 | +|------|--------|------|----------| +| 安装状态不同步 | 中 | 高 | 使用单一数据源(configStore) | +| 搜索不准确 | 中 | 中 | 优化搜索算法,支持触发词匹配 | +| 操作无反馈 | 低 | 中 | 添加 Toast 提示 | + +--- + +## 七、验收标准 + +### 7.1 功能验收 + +- [ ] 能加载并显示所有 74 个技能 +- [ ] 搜索功能正常工作 +- [ ] 分类过滤功能正常工作 +- [ ] 安装/卸载操作能持久化 +- [ ] 刷新页面后状态保持 + +### 7.2 性能验收 + +- [ ] 首次加载 < 1s +- [ ] 搜索响应 < 200ms +- [ ] 列表滚动流畅(60fps) + +### 7.3 质量验收 + +- [ ] 无 TypeScript 错误 +- [ ] 无 ESLint 警告 +- [ ] 测试覆盖率 > 80% + +--- + +## 八、时间估算 + +| 阶段 | 估算时间 | 依赖 | +|------|----------|------| +| Phase 1: Store 统一 | 1-2 天 | 无 | +| Phase 2: 后端集成 | 2-3 天 | Phase 1 | +| Phase 3: 搜索和分类 | 1-2 天 | Phase 2 | +| Phase 4: UI 完善 | 1-2 天 | Phase 3 | +| Phase 5: 测试和文档 | 1 天 | Phase 4 | +| **总计** | **6-10 天** | - | + +--- + +## 九、后续迭代方向 + +### 9.1 短期优化 + +- 技能推荐引擎(基于对话上下文) +- 技能使用统计 +- 技能收藏功能 + +### 9.2 中期扩展 + +- 技能创建向导 +- 技能导入/导出 +- 技能市场在线同步 + +### 9.3 长期愿景 + +- 技能共享社区 +- 技能认证体系 +- 技能版本管理 + +--- + +## 十、参考资料 + +### 10.1 相关文档 + +- [Skills 系统概述](../features/04-skills-ecosystem/00-skill-system.md) +- [状态管理文档](../features/00-architecture/02-state-management.md) +- [通信层文档](../features/00-architecture/01-communication-layer.md) + +### 10.2 相关代码 + +- `desktop/src/lib/skill-discovery.ts` - 技能发现引擎 +- `desktop/src/store/configStore.ts` - 配置状态管理 +- `desktop/src/lib/gateway-api.ts` - API 方法实现 +- `skills/.templates/skill-template.md` - 技能模板 + +### 10.3 技能文件示例 + +- `skills/code-review/SKILL.md` - 简单格式示例 +- `skills/data-analysis/SKILL.md` - 带表格示例 +- `skills/devops-automator/SKILL.md` - 完整模板示例 diff --git a/tests/desktop/general-settings.test.tsx b/tests/desktop/general-settings.test.tsx index 769036d..cb225db 100644 --- a/tests/desktop/general-settings.test.tsx +++ b/tests/desktop/general-settings.test.tsx @@ -1,61 +1,68 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -const useGatewayStoreMock = vi.fn(); -const useChatStoreMock = vi.fn(); -const getStoredGatewayTokenMock = vi.fn(() => 'stored-token'); -const setStoredGatewayTokenMock = vi.fn(); +const mockConnectionState = { value: 'connected' as const }; +const mockQuickConfig = { + value: { + gatewayUrl: 'ws://127.0.0.1:50051', + gatewayToken: '', + theme: 'light' as const, + autoStart: false, + showToolCalls: false, + }, +}; -vi.mock('../../desktop/src/store/gatewayStore', () => ({ - useGatewayStore: () => useGatewayStoreMock(), +const connectMock = vi.fn(async () => {}); +const disconnectMock = vi.fn(); +const saveQuickConfigMock = vi.fn(async () => {}); + +// Mock connectionStore with selector pattern +vi.mock('../../desktop/src/store/connectionStore', () => ({ + useConnectionStore: vi.fn((selector) => { + const state = { + connectionState: mockConnectionState.value, + connect: connectMock, + disconnect: disconnectMock, + }; + return selector ? selector(state) : state; + }), })); +// Mock configStore with selector pattern +vi.mock('../../desktop/src/store/configStore', () => ({ + useConfigStore: vi.fn((selector) => { + const state = { + quickConfig: mockQuickConfig.value, + saveQuickConfig: saveQuickConfigMock, + }; + return selector ? selector(state) : state; + }), +})); + +// Mock chatStore with selector pattern vi.mock('../../desktop/src/store/chatStore', () => ({ - useChatStore: () => useChatStoreMock(), + useChatStore: vi.fn((selector) => { + const state = { + currentModel: 'glm-5', + }; + return selector ? selector(state) : state; + }), })); vi.mock('../../desktop/src/lib/gateway-client', () => ({ - getStoredGatewayToken: () => getStoredGatewayTokenMock(), - setStoredGatewayToken: (token: string) => setStoredGatewayTokenMock(token), + getStoredGatewayToken: () => 'stored-token', + setStoredGatewayToken: vi.fn(), })); describe('General settings gateway connection', () => { - let connectMock: ReturnType; - let disconnectMock: ReturnType; - let saveQuickConfigMock: ReturnType; - beforeEach(() => { vi.clearAllMocks(); - connectMock = vi.fn(async () => {}); - disconnectMock = vi.fn(); - saveQuickConfigMock = vi.fn(async () => {}); - - useGatewayStoreMock.mockReturnValue({ - connectionState: 'connected', - gatewayVersion: '2026.3.11', - error: null, - quickConfig: { - gatewayUrl: 'ws://127.0.0.1:50051', - gatewayToken: '', - theme: 'light', - autoStart: false, - showToolCalls: false, - }, - connect: connectMock, - disconnect: disconnectMock, - saveQuickConfig: saveQuickConfigMock, - }); - - useChatStoreMock.mockReturnValue({ - currentModel: 'glm-5', - }); + mockConnectionState.value = 'connected'; }); it('renders gateway connection settings and displays connection status', async () => { - const reactModule = 'react'; - const reactDomClientModule = 'react-dom/client'; const [{ act, createElement }, { createRoot }, { General }] = await Promise.all([ - import(reactModule), - import(reactDomClientModule), + import('react'), + import('react-dom/client'), import('../../desktop/src/components/Settings/General'), ]); @@ -73,7 +80,6 @@ describe('General settings gateway connection', () => { expect(container.textContent).toContain('Gateway 连接'); expect(container.textContent).toContain('已连接'); expect(container.textContent).toContain('ws://127.0.0.1:50051'); - expect(container.textContent).toContain('2026.3.11'); expect(container.textContent).toContain('glm-5'); expect(container.textContent).toContain('断开连接'); @@ -90,21 +96,7 @@ describe('General settings gateway connection', () => { }); it('displays disconnected state when not connected', async () => { - useGatewayStoreMock.mockReturnValue({ - connectionState: 'disconnected', - gatewayVersion: null, - error: null, - quickConfig: { - gatewayUrl: 'ws://127.0.0.1:50051', - gatewayToken: '', - theme: 'light', - autoStart: false, - showToolCalls: false, - }, - connect: connectMock, - disconnect: disconnectMock, - saveQuickConfig: saveQuickConfigMock, - }); + mockConnectionState.value = 'disconnected'; const [{ act, createElement }, { createRoot }, { General }] = await Promise.all([ import('react'), diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 0000000..d861570 --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,48 @@ +# E2E Tests + +This directory contains end-to-end tests for ZCLAW desktop application. + +## Test Strategy + +Due to ZCLAW being a Tauri 22.0 desktop application, E2E testing requires special consideration: + +### Option 1: Vitest + @testing-library/react (Current) +- Unit and integration tests use Vitest +- Component tests use @testing-library/react +- Already configured and working (312 tests passing) + +### Option 2: Playwright for Web UI (Future) +- Playwright is available in `desktop/node_modules` +- Can test the React UI layer independently +- Cannot directly test Tauri native functionality + +### Option 3: Tauri Driver (Recommended for Full E2E) +- Use Tauri's built-in test utilities +- Requires `tauri-driver` package +- Can test full application including native functionality + +## Current Status + +- **Unit Tests**: ✅ 312 tests passing +- **Integration Tests**: ✅ Store and component integration verified +- **E2E Tests**: 🚧 Framework setup in progress + +## Running Tests + +```bash +# Unit/Integration tests +pnpm vitest run + +# E2E tests (when implemented) +pnpm test:e2e +``` + +## Test Coverage + +| Area | Unit | Integration | E2E | +|------|------|-------------|-----| +| Stores | ✅ | ✅ | - | +| Components | ✅ | ✅ | - | +| Gateway Client | ✅ | ✅ | - | +| Tauri Commands | - | - | 🚧 | +| Full User Flow | - | - | 🚧 | diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts new file mode 100644 index 0000000..8540c3c --- /dev/null +++ b/tests/e2e/app.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; + +/** + * ZCLAW E2E Tests + * + * These tests verify the UI layer of the application. + * For Tauri-specific tests, use Tauri's built-in testing tools. + */ + +test.describe('ZCLAW App', () => { + test.skip('should load the main page', async ({ page }) => { + // This test is skipped because it requires the dev server to be running + // To run: pnpm dev & pnpm test:e2e + await page.goto('/'); + + // Verify the app loads + await expect(page.locator('text=ZCLAW')).toBeVisible(); + }); + + test.skip('should show connection status', async ({ page }) => { + await page.goto('/'); + + // Wait for connection status to appear + const statusText = await page.locator('[data-testid="connection-status"]').textContent(); + expect(statusText).toBeTruthy(); + }); +}); + +test.describe('Settings Page', () => { + test.skip('should navigate to settings', async ({ page }) => { + await page.goto('/'); + + // Click settings button + await page.click('[data-testid="settings-button"]'); + + // Verify settings page loaded + await expect(page.locator('text=通用')).toBeVisible(); + }); +}); + +test.describe('Chat Interface', () => { + test.skip('should display chat input', async ({ page }) => { + await page.goto('/'); + + // Verify chat input exists + const chatInput = page.locator('[data-testid="chat-input"]'); + await expect(chatInput).toBeVisible(); + }); +}); diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts new file mode 100644 index 0000000..dcd29f8 --- /dev/null +++ b/tests/e2e/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Playwright configuration for ZCLAW E2E tests + * + * Note: These tests focus on the React UI layer. + * For full Tauri integration, use Tauri's built-in testing tools. + */ +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'pnpm dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +});