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>
193
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
193
desktop/src/lib/skill-adapter.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 错误响应
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"bdcac940a81c3235ce13-8b134df5feeb02852417",
|
||||
"bdcac940a81c3235ce13-6df5d90e5b85ad4debff"
|
||||
"4c7e6ccba74c38082eff-428751c1a27c810d25bc",
|
||||
"4c7e6ccba74c38082eff-e8eb267dbe5d6b944c33",
|
||||
"4c7e6ccba74c38082eff-1e63c4d4b91978536fc3",
|
||||
"4c7e6ccba74c38082eff-5a47c30a876dee9d7f6a",
|
||||
"4c7e6ccba74c38082eff-d595d4fb8c9beec7acb6"
|
||||
]
|
||||
}
|
||||
@@ -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 生成
|
||||
```
|
||||
|
After Width: | Height: | Size: 50 KiB |
@@ -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 生成
|
||||
```
|
||||
|
After Width: | Height: | Size: 52 KiB |
@@ -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 生成
|
||||
```
|
||||
|
After Width: | Height: | Size: 52 KiB |
@@ -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 生成
|
||||
```
|
||||
|
After Width: | Height: | Size: 63 KiB |
@@ -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 生成
|
||||
```
|
||||
|
After Width: | Height: | Size: 50 KiB |
@@ -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]
|
||||
```
|
||||
|
Before Width: | Height: | Size: 37 KiB |
@@ -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]
|
||||
```
|
||||
|
Before Width: | Height: | Size: 37 KiB |
381
docs/plans/skill-market-mvp.md
Normal file
@@ -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<string, unknown> }>;
|
||||
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` - 完整模板示例
|
||||
@@ -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<typeof vi.fn>;
|
||||
let disconnectMock: ReturnType<typeof vi.fn>;
|
||||
let saveQuickConfigMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
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'),
|
||||
|
||||
48
tests/e2e/README.md
Normal file
@@ -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 | - | - | 🚧 |
|
||||
49
tests/e2e/app.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
32
tests/e2e/playwright.config.ts
Normal file
@@ -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,
|
||||
},
|
||||
});
|
||||