From bcea35f86f5caf42000e1b68447c724e97f10401 Mon Sep 17 00:00:00 2001 From: deepinsect Date: Thu, 8 Jan 2026 21:37:53 +0800 Subject: [PATCH 01/16] feat: implement skills management system with UI and backend integration - Add SkillTools class for handling skill list and control actions. - Extend ConversationsTable to include active_skills field in the database. - Create SkillsSettings.vue component for managing skills in the UI. - Update router to include skills settings route. - Add translations for skills management in English and Chinese. - Implement skills store using Pinia for state management. - Define skill-related types in shared types directory. - Integrate skill installation and uninstallation functionalities. --- CLAUDE.md | 377 +++------ docs/specs/skills-system/design.md | 644 ++++++++++++++ docs/specs/skills-system/research.md | 474 +++++++++++ docs/specs/skills-system/tasks.md | 253 ++++++ docs/specs/skills-system/ui-design.md | 545 ++++++++++++ package.json | 2 + src/main/events.ts | 10 + .../agentPresenter/acp/agentToolManager.ts | 130 ++- .../agentPresenter/message/messageBuilder.ts | 61 +- .../message/skillsPromptBuilder.ts | 96 +++ src/main/presenter/configPresenter/index.ts | 30 + src/main/presenter/index.ts | 26 +- src/main/presenter/sessionPresenter/const.ts | 3 +- src/main/presenter/sessionPresenter/types.ts | 1 + src/main/presenter/skillPresenter/index.ts | 791 ++++++++++++++++++ .../presenter/skillPresenter/skillTools.ts | 79 ++ .../sqlitePresenter/tables/conversations.ts | 26 +- src/main/presenter/toolPresenter/index.ts | 1 + .../settings/components/SkillsSettings.vue | 306 +++++++ src/renderer/settings/main.ts | 20 +- src/renderer/src/i18n/en-US/routes.json | 3 +- src/renderer/src/i18n/en-US/settings.json | 31 + src/renderer/src/i18n/zh-CN/routes.json | 3 +- src/renderer/src/i18n/zh-CN/settings.json | 31 + src/renderer/src/stores/skillsStore.ts | 100 +++ src/shared/types/index.d.ts | 1 + .../types/presenters/legacy.presenters.d.ts | 9 + .../types/presenters/thread.presenter.d.ts | 1 + src/shared/types/skill.ts | 124 +++ 29 files changed, 3891 insertions(+), 287 deletions(-) create mode 100644 docs/specs/skills-system/design.md create mode 100644 docs/specs/skills-system/research.md create mode 100644 docs/specs/skills-system/tasks.md create mode 100644 docs/specs/skills-system/ui-design.md create mode 100644 src/main/presenter/agentPresenter/message/skillsPromptBuilder.ts create mode 100644 src/main/presenter/skillPresenter/index.ts create mode 100644 src/main/presenter/skillPresenter/skillTools.ts create mode 100644 src/renderer/settings/components/SkillsSettings.vue create mode 100644 src/renderer/src/stores/skillsStore.ts create mode 100644 src/shared/types/skill.ts diff --git a/CLAUDE.md b/CLAUDE.md index 704df6d33..3743b2c9a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,348 +4,185 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -DeepChat is a feature-rich open-source AI chat platform built with Electron + Vue 3 + TypeScript. It supports multiple cloud and local LLM providers, advanced MCP (Model Context Protocol) tool calling, and multi-window/multi-tab architecture. +DeepChat is an open-source AI agent platform built with Electron + Vue 3 + TypeScript. It supports multiple cloud and local LLM providers, MCP (Model Context Protocol) tool calling, ACP (Agent Client Protocol) agent integration, and multi-window/multi-tab architecture. ## Development Commands -### Package Management - -Use `pnpm` as the package manager (required Node.js >= 20.19.0, pnpm >= 10.11.0): +### Setup ```bash -# Install dependencies -pnpm install - -# Install runtime dependencies for MCP and Python execution -pnpm run installRuntime - -# Note: If you encounter "No module named 'distutils'" error on Windows: -pip install setuptools +pnpm install # Install dependencies (Node.js >= 20.19.0, pnpm >= 10.11.0) +pnpm run installRuntime # Install runtime binaries (uv, node, ripgrep) ``` ### Development ```bash -# Start development server -pnpm run dev - -# Start development with inspector for debugging -pnpm run dev:inspect - -# Linux development (disable sandbox) -pnpm run dev:linux +pnpm run dev # Start development server with HMR +pnpm run dev:inspect # Start with debug inspector (port 9229) +pnpm run dev:linux # Start on Linux (no sandbox) ``` ### Code Quality ```bash -# Lint with OxLint -pnpm run lint - -# Format code with Prettier -pnpm run format +pnpm run lint # Lint with OxLint +pnpm run format # Format with Prettier +pnpm run typecheck # Type check all code +pnpm run typecheck:node # Type check main process only +pnpm run typecheck:web # Type check renderer process only +``` -# Type checking -pnpm run typecheck -# or separately: -pnpm run typecheck:node # Main process -pnpm run typecheck:web # Renderer process +**After completing a feature, always run:** +```bash +pnpm run format && pnpm run lint ``` ### Testing ```bash -# Run all tests -pnpm run test - -# Run tests with coverage -pnpm run test:coverage - -# Run tests in watch mode -pnpm run test:watch - -# Run tests with UI -pnpm run test:ui - -# Run specific test suites -pnpm run test:main # Main process tests -pnpm run test:renderer # Renderer process tests +pnpm test # Run all tests +pnpm test path/to/file.test.ts # Run a single test file +pnpm test:main # Main process tests only +pnpm test:renderer # Renderer process tests only +pnpm test:coverage # Generate coverage report +pnpm test:watch # Watch mode ``` ### Building ```bash -# Build for development preview -pnpm run build - -# Build for production (platform-specific) -pnpm run build:win # Windows -pnpm run build:mac # macOS -pnpm run build:linux # Linux - -# Build for specific architectures -pnpm run build:win:x64 -pnpm run build:win:arm64 -pnpm run build:mac:x64 -pnpm run build:mac:arm64 -pnpm run build:linux:x64 -pnpm run build:linux:arm64 +pnpm run build # Build for production (includes typecheck) +pnpm run build:win # Windows +pnpm run build:mac # macOS +pnpm run build:linux # Linux +# Architecture-specific: build:win:x64, build:win:arm64, build:mac:x64, build:mac:arm64, etc. ``` ### Internationalization ```bash -# Check i18n completeness (Chinese as source) -pnpm run i18n - -# Check i18n completeness (English as source) -pnpm run i18n:en +pnpm run i18n # Check i18n completeness (source: zh-CN) +pnpm run i18n:en # Check i18n completeness (source: en-US) +pnpm run i18n:types # Generate TypeScript types for i18n keys ``` ## Architecture Overview -### Multi-Process Architecture +``` +┌─────────────────────────────────────────────────────────────┐ +│ Electron Main (TS) │ +│ Presenters: window/tab/thread/config/llm/mcp/knowledge/ │ +│ sync/oauth/deeplink/floating button │ +│ Storage: SQLite chat.db, ElectronStore settings, backups │ +└───────────────┬─────────────────────────────────────────────┘ + │ IPC (contextBridge + EventBus) +┌───────────────▼─────────────────────────────────────────────┐ +│ Preload (strict API) │ +└───────────────┬─────────────────────────────────────────────┘ + │ Typed presenters via `usePresenter` +┌───────────────▼─────────────────────────────────────────────┐ +│ Renderer (Vue 3 + Pinia + Tailwind + shadcn/ui) │ +│ Shell UI, chat flow, ACP workspace, MCP console, settings │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Core Patterns -- **Main Process**: Core business logic, system integration, window management -- **Renderer Process**: UI components, user interactions, frontend state management -- **Preload Scripts**: Secure IPC bridge between main and renderer processes +**Presenter Pattern**: All system capabilities are in main-process presenters (`src/main/presenter/`). The renderer calls them via the typed `usePresenter` hook through the preload bridge. -### Key Architectural Patterns +**Multi-Window Multi-Tab**: WindowPresenter and TabPresenter manage Electron windows/BrowserViews with detach/move support. EventBus fans out cross-process events. -#### Presenter Pattern +**Data Boundaries**: Chat data in SQLite (`app_db/chat.db`), settings in Electron Store, knowledge bases in DuckDB. Renderer never touches filesystem directly. -Each functional domain has a dedicated Presenter class in `src/main/presenter/`: +### Key Presenters -- **WindowPresenter**: BrowserWindow lifecycle management -- **TabPresenter**: WebContentsView management with cross-window tab dragging +- **LLMProviderPresenter**: Streaming, rate limits, provider instances (cloud/local/ACP), model discovery, agent loop +- **McpPresenter**: MCP server lifecycle, tool/prompt/resource management, supports StreamableHTTP/SSE/Stdio - **ThreadPresenter**: Conversation session management and LLM coordination -- **McpPresenter**: MCP server connections and tool execution - **ConfigPresenter**: Unified configuration management -- **LLMProviderPresenter**: LLM provider abstraction with Agent Loop architecture - -#### Multi-Window Multi-Tab Architecture - -- **Window Shell** (`src/renderer/shell/`): Lightweight tab bar UI management -- **Tab Content** (`src/renderer/src/`): Complete application functionality -- **Independent Vue Instances**: Separation of concerns for better performance +- **WindowPresenter/TabPresenter**: Window and tab lifecycle -#### Event-Driven Communication +### LLM Provider Architecture (Two Layers) -- **EventBus** (`src/main/eventbus.ts`): Decoupled inter-process communication -- **Standard Event Patterns**: Consistent naming and responsibility separation -- **IPC Integration**: EventBus bridges main process events to renderer via IPC - -### LLM Provider Architecture - -The LLM system follows a two-layer architecture: - -1. **Agent Loop Layer** (`llmProviderPresenter/index.ts`): - - Manages conversation flow with multi-turn tool calling - - Handles tool execution via McpPresenter - - Standardizes events sent to frontend - -2. **Provider Layer** (`llmProviderPresenter/providers/*.ts`): - - Each provider handles specific LLM API interactions - - Converts MCP tools to provider-specific formats - - Normalizes streaming responses to standard events - - Supports both native and prompt-wrapped tool calling - -### MCP Integration - -- **Server Management**: Lifecycle management of MCP servers -- **Tool Execution**: Seamless integration with LLM providers -- **Format Conversion**: Bridges MCP tools with various LLM provider formats -- **Built-in Services**: In-memory servers for code execution, web access, file operations -- **Data Source Decoupling**: Custom prompts work independently of MCP through config data source +1. **Agent Loop Layer** (`llmProviderPresenter/index.ts`): Multi-turn tool calling, tool execution via McpPresenter, standardized frontend events +2. **Provider Layer** (`llmProviderPresenter/providers/*.ts`): Provider-specific API interactions, MCP tool conversion, streaming normalization ## Code Structure -### Main Process (`src/main/`) - -- `presenter/`: Core business logic organized by functional domain -- `eventbus.ts`: Central event coordination system -- `index.ts`: Application entry point and lifecycle management - -### Renderer Process (`src/renderer/`) - -- `src/`: Main application UI (Vue 3 + Composition API) -- `shell/`: Tab management UI shell -- `floating/`: Floating button interface - -### Shared Code (`src/shared/`) - -- Type definitions shared between main and renderer processes -- Common utilities and constants -- IPC contract definitions +``` +src/main/ # Main process + presenter/ # Core business logic by domain + eventbus.ts # Central event coordination +src/preload/ # Context-isolated IPC bridge +src/renderer/ + src/ # Main app UI (Vue 3 + Composition API) + shell/ # Tab management shell UI + floating/ # Floating button interface +src/shared/ # Shared types/utilities and presenter contracts +test/ # Vitest suites (main/, renderer/) +docs/ # Design docs and guides +``` ## Development Guidelines ### Code Standards -- **Language**: Use English for logs and comments (Chinese text exists in legacy code) -- **TypeScript**: Strict type checking enabled -- **Vue 3**: Use Composition API for all components -- **State Management**: Pinia for frontend state -- **Styling**: Tailwind CSS with scoped styles -- **Internationalization**: All user-facing strings must use i18n keys via vue-i18n - -### Specification-Driven Development - -Use SDD methodology for all feature implementations. See [docs/spec-driven-dev.md](docs/spec-driven-dev.md) for details. - -Prefer lightweight spec artifacts under `docs/specs//` (spec/plan/tasks) and resolve `[NEEDS CLARIFICATION]` markers before coding. - -Key principles: specification-first, test-when-useful, Presenter architecture, UI consistency, anti-over-engineering, compatibility/migration awareness. +- **Language**: English for all logs and comments +- **TypeScript**: Strict type checking +- **Vue 3**: Composition API with ` diff --git a/src/renderer/settings/main.ts b/src/renderer/settings/main.ts index 05717b17b..7c6730032 100644 --- a/src/renderer/settings/main.ts +++ b/src/renderer/settings/main.ts @@ -82,6 +82,16 @@ const router = createRouter({ position: 6 } }, + { + path: '/skills', + name: 'settings-skills', + component: () => import('./components/SkillsSettings.vue'), + meta: { + titleKey: 'routes.settings-skills', + icon: 'lucide:wand-sparkles', + position: 7 + } + }, { path: '/prompt', name: 'settings-prompt', @@ -89,7 +99,7 @@ const router = createRouter({ meta: { titleKey: 'routes.settings-prompt', icon: 'lucide:book-open-text', - position: 7 + position: 8 } }, { @@ -99,7 +109,7 @@ const router = createRouter({ meta: { titleKey: 'routes.settings-knowledge-base', icon: 'lucide:book-marked', - position: 8 + position: 9 } }, { @@ -109,7 +119,7 @@ const router = createRouter({ meta: { titleKey: 'routes.settings-database', icon: 'lucide:database', - position: 9 + position: 10 } }, { @@ -119,7 +129,7 @@ const router = createRouter({ meta: { titleKey: 'routes.settings-shortcut', icon: 'lucide:keyboard', - position: 10 + position: 11 } }, { @@ -129,7 +139,7 @@ const router = createRouter({ meta: { titleKey: 'routes.settings-about', icon: 'lucide:info', - position: 11 + position: 12 } }, { diff --git a/src/renderer/src/i18n/en-US/routes.json b/src/renderer/src/i18n/en-US/routes.json index f162971d3..0b9c13798 100644 --- a/src/renderer/src/i18n/en-US/routes.json +++ b/src/renderer/src/i18n/en-US/routes.json @@ -13,5 +13,6 @@ "settings-knowledge-base": "Knowledge Base", "settings-prompt": "Prompts", "settings-mcp-market": "MCP Market", - "settings-acp": "ACP Agents" + "settings-acp": "ACP Agents", + "settings-skills": "Skills" } diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index 0c8ab5acc..e4b218d03 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -1039,5 +1039,36 @@ "resetToDefault": "Reset to default prompt", "resetToDefaultSuccess": "Reset to default system prompt successfully", "resetToDefaultFailed": "Failed to reset, please try again" + }, + "skills": { + "title": "Skills", + "description": "Manage and configure AI assistant skills", + "openFolder": "Open Folder", + "addSkill": "Add Skill", + "empty": "No skills yet", + "emptyHint": "Click \"Add Skill\" to install a new skill", + "install": { + "title": "Install Skill", + "description": "Choose skill installation method", + "fromFolder": "Install from Folder", + "selectFolder": "Select Skill Folder", + "success": "Installation Successful", + "successMessage": "Skill {name} has been installed successfully", + "failed": "Installation Failed" + }, + "delete": { + "title": "Delete Skill", + "description": "Are you sure you want to delete skill {name}? This action cannot be undone.", + "success": "Deletion Successful", + "successMessage": "Skill {name} has been deleted successfully", + "failed": "Deletion Failed" + }, + "edit": { + "title": "Edit Skill", + "placeholder": "Edit skill content here...", + "readFailed": "Failed to read", + "success": "Saved Successfully", + "failed": "Failed to save" + } } } diff --git a/src/renderer/src/i18n/zh-CN/routes.json b/src/renderer/src/i18n/zh-CN/routes.json index d0970da30..a4e259397 100644 --- a/src/renderer/src/i18n/zh-CN/routes.json +++ b/src/renderer/src/i18n/zh-CN/routes.json @@ -13,5 +13,6 @@ "settings-knowledge-base": "知识库", "settings-prompt": "Prompt管理", "settings-mcp-market": "MCP市场", - "settings-acp": "ACP Agent" + "settings-acp": "ACP Agent", + "settings-skills": "技能管理" } diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index a1c1d0ea1..bfb4d96a9 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -1039,5 +1039,36 @@ "resetToDefault": "重置为默认提示词", "resetToDefaultSuccess": "已重置为默认系统提示词", "resetToDefaultFailed": "重置失败,请重试" + }, + "skills": { + "title": "技能管理", + "description": "管理和配置 AI 助手的技能模块", + "openFolder": "打开文件夹", + "addSkill": "添加技能", + "empty": "暂无技能", + "emptyHint": "点击\"添加技能\"按钮安装新技能", + "install": { + "title": "安装技能", + "description": "选择技能安装方式", + "fromFolder": "从文件夹安装", + "selectFolder": "选择技能文件夹", + "success": "安装成功", + "successMessage": "技能 {name} 已成功安装", + "failed": "安装失败" + }, + "delete": { + "title": "删除技能", + "description": "确定要删除技能 {name} 吗?此操作无法撤销。", + "success": "删除成功", + "successMessage": "技能 {name} 已成功删除", + "failed": "删除失败" + }, + "edit": { + "title": "编辑技能", + "placeholder": "在此编辑技能内容...", + "readFailed": "读取失败", + "success": "保存成功", + "failed": "保存失败" + } } } diff --git a/src/renderer/src/stores/skillsStore.ts b/src/renderer/src/stores/skillsStore.ts new file mode 100644 index 000000000..57866a99c --- /dev/null +++ b/src/renderer/src/stores/skillsStore.ts @@ -0,0 +1,100 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { usePresenter } from '@/composables/usePresenter' +import type { SkillMetadata, SkillInstallResult } from '@shared/types/skill' + +export const useSkillsStore = defineStore('skills', () => { + const skillPresenter = usePresenter('skillPresenter') + + // State + const skills = ref([]) + const loading = ref(false) + const error = ref(null) + + // Computed + const skillCount = computed(() => skills.value.length) + + // Actions + const loadSkills = async () => { + loading.value = true + error.value = null + try { + skills.value = await skillPresenter.getMetadataList() + } catch (e) { + error.value = e instanceof Error ? e.message : String(e) + console.error('[SkillsStore] Failed to load skills:', e) + } finally { + loading.value = false + } + } + + const installFromFolder = async (folderPath: string): Promise => { + try { + const result = await skillPresenter.installFromFolder(folderPath) + if (result.success) { + await loadSkills() + } + return result + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e) + return { success: false, error: errorMsg } + } + } + + const uninstallSkill = async (name: string): Promise => { + try { + const result = await skillPresenter.uninstallSkill(name) + if (result.success) { + await loadSkills() + } + return result + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e) + return { success: false, error: errorMsg } + } + } + + const getSkillsDir = async (): Promise => { + return await skillPresenter.getSkillsDir() + } + + const openSkillsFolder = async (): Promise => { + await skillPresenter.openSkillsFolder() + } + + const updateSkillFile = async (name: string, content: string): Promise => { + try { + const result = await skillPresenter.updateSkillFile(name, content) + if (result.success) { + await loadSkills() + } + return result + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e) + return { success: false, error: errorMsg } + } + } + + const getSkillFolderTree = async (name: string) => { + return await skillPresenter.getSkillFolderTree(name) + } + + return { + // State + skills, + loading, + error, + + // Computed + skillCount, + + // Actions + loadSkills, + installFromFolder, + uninstallSkill, + getSkillsDir, + openSkillsFolder, + updateSkillFile, + getSkillFolderTree + } +}) diff --git a/src/shared/types/index.d.ts b/src/shared/types/index.d.ts index 9023a69e1..1119eeaaa 100644 --- a/src/shared/types/index.d.ts +++ b/src/shared/types/index.d.ts @@ -5,3 +5,4 @@ export type * from './presenters/agent-provider' export type * from './presenters/workspace' export type * from './presenters/tool.presenter' export * from './browser' +export * from './skill' diff --git a/src/shared/types/presenters/legacy.presenters.d.ts b/src/shared/types/presenters/legacy.presenters.d.ts index feb57263b..0a2b63f13 100644 --- a/src/shared/types/presenters/legacy.presenters.d.ts +++ b/src/shared/types/presenters/legacy.presenters.d.ts @@ -13,6 +13,7 @@ import type { ISearchPresenter } from './search.presenter' import type { IConversationExporter } from './exporter.presenter' import type { IWorkspacePresenter } from './workspace' import type { IToolPresenter } from './tool.presenter' +import type { ISkillPresenter } from '../skill' import type { BrowserTabInfo, BrowserContextSnapshot, @@ -447,6 +448,7 @@ export interface IPresenter { knowledgePresenter: IKnowledgePresenter workspacePresenter: IWorkspacePresenter toolPresenter: IToolPresenter + skillPresenter: ISkillPresenter init(): void destroy(): void } @@ -548,6 +550,12 @@ export interface IConfigPresenter { setSyncFolderPath(folderPath: string): void getLastSyncTime(): number setLastSyncTime(time: number): void + // Skills settings + getSkillsEnabled(): boolean + setSkillsEnabled(enabled: boolean): void + getSkillsPath(): string + setSkillsPath(skillsPath: string): void + getSkillSettings(): { skillsPath: string; enableSkills: boolean } // MCP configuration related methods getMcpServers(): Promise> setMcpServers(servers: Record): Promise @@ -1060,6 +1068,7 @@ export type CONVERSATION_SETTINGS = { acpWorkdirMap?: Record chatMode?: 'chat' | 'agent' | 'acp agent' agentWorkspacePath?: string | null + activeSkills?: string[] // Activated skills for this conversation } export type ParentSelection = { diff --git a/src/shared/types/presenters/thread.presenter.d.ts b/src/shared/types/presenters/thread.presenter.d.ts index 594cb80a2..776f4a87a 100644 --- a/src/shared/types/presenters/thread.presenter.d.ts +++ b/src/shared/types/presenters/thread.presenter.d.ts @@ -24,6 +24,7 @@ export type CONVERSATION_SETTINGS = { chatMode?: 'chat' | 'agent' | 'acp agent' agentWorkspacePath?: string | null selectedVariantsMap?: Record + activeSkills?: string[] } export type ParentSelection = { diff --git a/src/shared/types/skill.ts b/src/shared/types/skill.ts new file mode 100644 index 000000000..4223a72a8 --- /dev/null +++ b/src/shared/types/skill.ts @@ -0,0 +1,124 @@ +/** + * Skills System Type Definitions + * + * Skills are file-based knowledge modules that provide specialized expertise + * and behavioral guidance to AI agents. They support progressive loading + * (metadata first, full content on activation) and hot-reloading. + */ + +/** + * Skill metadata extracted from SKILL.md frontmatter. + * Always kept in memory for quick access and semantic matching. + */ +export interface SkillMetadata { + /** Unique identifier (must match directory name) */ + name: string + /** Short description for semantic matching */ + description: string + /** Full path to SKILL.md file */ + path: string + /** Skill root directory path */ + skillRoot: string + /** Optional additional tools required by this skill */ + allowedTools?: string[] +} + +/** + * Full skill content loaded when activated. + * Injected into system prompt. + */ +export interface SkillContent { + /** Skill name */ + name: string + /** Full SKILL.md content (body after frontmatter) */ + content: string +} + +/** + * Skill installation result + */ +export interface SkillInstallResult { + success: boolean + error?: string + skillName?: string +} + +/** + * Skill installation options + */ +export interface SkillInstallOptions { + overwrite?: boolean +} + +/** + * Folder tree node for displaying skill directory structure + */ +export interface SkillFolderNode { + name: string + type: 'file' | 'directory' + path: string + children?: SkillFolderNode[] +} + +/** + * Skill state associated with a conversation session. + * Persisted in the database. + */ +export interface SkillState { + /** Associated conversation ID */ + conversationId: string + /** Set of activated skill names */ + activeSkills: string[] +} + +/** + * Skill list tool response item + */ +export interface SkillListItem { + name: string + description: string + active: boolean +} + +/** + * Skill control action type + */ +export type SkillControlAction = 'activate' | 'deactivate' + +/** + * Skill Presenter interface for main process + */ +export interface ISkillPresenter { + // Discovery and listing + getSkillsDir(): Promise + discoverSkills(): Promise + getMetadataList(): Promise + getMetadataPrompt(): Promise + + // Content loading + loadSkillContent(name: string): Promise + + // Installation and uninstallation + installBuiltinSkills(): Promise + installFromFolder(folderPath: string, options?: SkillInstallOptions): Promise + installFromZip(zipPath: string, options?: SkillInstallOptions): Promise + installFromUrl(url: string, options?: SkillInstallOptions): Promise + uninstallSkill(name: string): Promise + + // File operations + updateSkillFile(name: string, content: string): Promise + getSkillFolderTree(name: string): Promise + openSkillsFolder(): Promise + + // Session state management + getActiveSkills(conversationId: string): Promise + setActiveSkills(conversationId: string, skills: string[]): Promise + validateSkillNames(names: string[]): Promise + + // Tool integration + getActiveSkillsAllowedTools(conversationId: string): Promise + + // Hot reload + watchSkillFiles(): void + stopWatching(): void +} From c50217c5a096e83f18849cf169db4f4ebde1da5d Mon Sep 17 00:00:00 2001 From: deepinsect Date: Fri, 9 Jan 2026 14:39:04 +0800 Subject: [PATCH 02/16] feat: add file selection functionality and enhance skill presenter tests - Added `selectFiles` method to `IDevicePresenter` interface for file selection with options for filters and multiple selections. - Created comprehensive tests for `SkillPresenter`, covering skill discovery, installation, activation, and deactivation. - Introduced tests for `SkillTools` to validate skill handling, including edge cases and input validation. - Mocked necessary dependencies and ensured proper integration with the event bus for skill events. --- docs/specs/skills-system/code-review.md | 451 ++++++++++ docs/specs/skills-system/tasks.md | 74 +- electron-builder.yml | 3 + resources/skills/code-review/SKILL.md | 54 ++ resources/skills/git-commit/SKILL.md | 59 ++ src/main/presenter/devicePresenter/index.ts | 19 + .../settings/components/SkillsSettings.vue | 306 ------- .../settings/components/skills/SkillCard.vue | 68 ++ .../components/skills/SkillEditorSheet.vue | 234 +++++ .../components/skills/SkillFolderTree.vue | 56 ++ .../components/skills/SkillFolderTreeNode.vue | 70 ++ .../components/skills/SkillInstallDialog.vue | 327 +++++++ .../components/skills/SkillsHeader.vue | 52 ++ .../components/skills/SkillsSettings.vue | 208 +++++ src/renderer/settings/main.ts | 2 +- src/renderer/src/i18n/da-DK/routes.json | 3 +- src/renderer/src/i18n/da-DK/settings.json | 60 ++ src/renderer/src/i18n/en-US/settings.json | 33 +- src/renderer/src/i18n/fa-IR/routes.json | 3 +- src/renderer/src/i18n/fa-IR/settings.json | 60 ++ src/renderer/src/i18n/fr-FR/routes.json | 3 +- src/renderer/src/i18n/fr-FR/settings.json | 60 ++ src/renderer/src/i18n/he-IL/routes.json | 3 +- src/renderer/src/i18n/he-IL/settings.json | 60 ++ src/renderer/src/i18n/ja-JP/routes.json | 3 +- src/renderer/src/i18n/ja-JP/settings.json | 60 ++ src/renderer/src/i18n/ko-KR/routes.json | 3 +- src/renderer/src/i18n/ko-KR/settings.json | 60 ++ src/renderer/src/i18n/pt-BR/routes.json | 3 +- src/renderer/src/i18n/pt-BR/settings.json | 60 ++ src/renderer/src/i18n/ru-RU/routes.json | 3 +- src/renderer/src/i18n/ru-RU/settings.json | 60 ++ src/renderer/src/i18n/zh-CN/settings.json | 33 +- src/renderer/src/i18n/zh-HK/routes.json | 3 +- src/renderer/src/i18n/zh-HK/settings.json | 60 ++ src/renderer/src/i18n/zh-TW/routes.json | 3 +- src/renderer/src/i18n/zh-TW/settings.json | 60 ++ src/renderer/src/stores/skillsStore.ts | 41 +- .../types/presenters/legacy.presenters.d.ts | 4 + .../skillPresenter/skillPresenter.test.ts | 828 ++++++++++++++++++ .../skillPresenter/skillTools.test.ts | 373 ++++++++ 41 files changed, 3566 insertions(+), 359 deletions(-) create mode 100644 docs/specs/skills-system/code-review.md create mode 100644 resources/skills/code-review/SKILL.md create mode 100644 resources/skills/git-commit/SKILL.md delete mode 100644 src/renderer/settings/components/SkillsSettings.vue create mode 100644 src/renderer/settings/components/skills/SkillCard.vue create mode 100644 src/renderer/settings/components/skills/SkillEditorSheet.vue create mode 100644 src/renderer/settings/components/skills/SkillFolderTree.vue create mode 100644 src/renderer/settings/components/skills/SkillFolderTreeNode.vue create mode 100644 src/renderer/settings/components/skills/SkillInstallDialog.vue create mode 100644 src/renderer/settings/components/skills/SkillsHeader.vue create mode 100644 src/renderer/settings/components/skills/SkillsSettings.vue create mode 100644 test/main/presenter/skillPresenter/skillPresenter.test.ts create mode 100644 test/main/presenter/skillPresenter/skillTools.test.ts diff --git a/docs/specs/skills-system/code-review.md b/docs/specs/skills-system/code-review.md new file mode 100644 index 000000000..d46fd5b98 --- /dev/null +++ b/docs/specs/skills-system/code-review.md @@ -0,0 +1,451 @@ +# Skills System Code Review + +## Overview + +This document records issues and suggestions found during the code review of the Skills system implementation. + +**Review Date**: 2026-01-09 +**Reviewed Files**: +- `src/main/presenter/skillPresenter/index.ts` +- `src/main/presenter/skillPresenter/skillTools.ts` +- `src/shared/types/skill.ts` +- `src/renderer/settings/components/skills/SkillsSettings.vue` +- `src/renderer/settings/components/skills/SkillEditorSheet.vue` +- `src/renderer/settings/components/skills/SkillInstallDialog.vue` +- `src/renderer/settings/components/skills/SkillCard.vue` +- `src/renderer/settings/components/skills/SkillsHeader.vue` +- `src/renderer/settings/components/skills/SkillFolderTree.vue` +- `src/renderer/settings/components/skills/SkillFolderTreeNode.vue` +- `src/renderer/src/stores/skillsStore.ts` +- `src/main/presenter/agentPresenter/message/skillsPromptBuilder.ts` +- `src/main/presenter/agentPresenter/acp/agentToolManager.ts` +- `src/main/presenter/configPresenter/index.ts` (skills config) +- `src/main/presenter/sessionPresenter/types.ts` (activeSkills type) + +--- + +## Issues + +### Issue 1: Potential race condition in `getMetadataList()` + +**Location**: `src/main/presenter/skillPresenter/index.ts:155-160` + +**Severity**: Medium + +**Description**: Multiple concurrent calls to `getMetadataList()` when cache is empty could trigger multiple `discoverSkills()` calls simultaneously. + +```typescript +async getMetadataList(): Promise { + if (this.metadataCache.size === 0) { + await this.discoverSkills() // Race condition here + } + return Array.from(this.metadataCache.values()) +} +``` + +**Recommendation**: Add a discovery lock or pending promise pattern: +```typescript +private discoveryPromise: Promise | null = null + +async getMetadataList(): Promise { + if (this.metadataCache.size === 0) { + if (!this.discoveryPromise) { + this.discoveryPromise = this.discoverSkills().finally(() => { + this.discoveryPromise = null + }) + } + await this.discoveryPromise + } + return Array.from(this.metadataCache.values()) +} +``` + +**Status**: [ ] Not Fixed + +--- + +### Issue 2: Duplicate code in prompt generation + +**Location**: +- `src/main/presenter/skillPresenter/index.ts:165-176` +- `src/main/presenter/agentPresenter/message/skillsPromptBuilder.ts:52-74` + +**Severity**: Low + +**Description**: The same prompt generation logic exists in two places, violating DRY principle. + +**Recommendation**: Have `buildSkillsMetadataPrompt()` delegate to `skillPresenter.getMetadataPrompt()`: +```typescript +export async function buildSkillsMetadataPrompt(): Promise { + if (!isSkillsEnabled()) return '' + const skillPresenter = presenter.skillPresenter as SkillPresenter + return skillPresenter.getMetadataPrompt() +} +``` + +**Status**: [ ] Not Fixed + +--- + +### Issue 3: Recursive folder tree has no depth limit + +**Location**: `src/main/presenter/skillPresenter/index.ts:564-587` + +**Severity**: Medium + +**Description**: The `buildFolderTree()` method recurses without depth protection. This could cause stack overflow with deep directory structures or symlink loops. + +```typescript +private buildFolderTree(dirPath: string): SkillFolderNode[] { + // No depth limit - could recurse infinitely + for (const entry of entries) { + if (entry.isDirectory()) { + nodes.push({ + children: this.buildFolderTree(fullPath) // Unlimited recursion + }) + } + } +} +``` + +**Recommendation**: Add depth limit parameter: +```typescript +private buildFolderTree(dirPath: string, depth: number = 0, maxDepth: number = 5): SkillFolderNode[] { + if (depth >= maxDepth) return [] + // ... rest of implementation with depth + 1 in recursive call +} +``` + +**Status**: [ ] Not Fixed + +--- + +### Issue 4: URL download lacks timeout and size limit + +**Location**: `src/main/presenter/skillPresenter/index.ts:487-494` + +**Severity**: Medium + +**Description**: The `downloadSkillZip()` function uses `fetch` without timeout or maximum file size check. A malicious or slow URL could hang the application or exhaust memory. + +```typescript +private async downloadSkillZip(url: string, destPath: string): Promise { + const response = await fetch(url) // No timeout + const buffer = new Uint8Array(await response.arrayBuffer()) // No size limit + fs.writeFileSync(destPath, Buffer.from(buffer)) +} +``` + +**Recommendation**: Add AbortController with timeout and Content-Length validation: +```typescript +private async downloadSkillZip(url: string, destPath: string): Promise { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 30000) // 30s timeout + + try { + const response = await fetch(url, { signal: controller.signal }) + if (!response.ok) { + throw new Error(`Download failed: ${response.status}`) + } + + const contentLength = response.headers.get('content-length') + const maxSize = 50 * 1024 * 1024 // 50MB limit + if (contentLength && parseInt(contentLength) > maxSize) { + throw new Error('File too large') + } + + const buffer = new Uint8Array(await response.arrayBuffer()) + fs.writeFileSync(destPath, Buffer.from(buffer)) + } finally { + clearTimeout(timeoutId) + } +} +``` + +**Status**: [ ] Not Fixed + +--- + +### Issue 5: Missing validation for `allowedTools` array elements + +**Location**: `src/main/presenter/skillPresenter/index.ts:144` + +**Severity**: Low + +**Description**: The code checks if `allowedTools` is an array but doesn't validate that elements are strings. + +```typescript +allowedTools: Array.isArray(data.allowedTools) ? data.allowedTools : undefined +``` + +**Recommendation**: Filter to ensure only strings: +```typescript +allowedTools: Array.isArray(data.allowedTools) + ? data.allowedTools.filter((t): t is string => typeof t === 'string') + : undefined +``` + +**Status**: [ ] Not Fixed + +--- + +### Issue 6: Error state not exposed in store + +**Location**: `src/renderer/src/stores/skillsStore.ts:20-29` + +**Severity**: Low + +**Description**: The `error` ref is set on failure but the UI doesn't display it. Users won't know why skill loading failed. + +**Recommendation**: Either: +1. Show error state in SkillsSettings.vue +2. Use toast notifications for load errors +3. Add retry mechanism + +**Status**: [ ] Not Fixed + +--- + +### Issue 7: Event listener cleanup pattern + +**Location**: `src/renderer/settings/components/skills/SkillsSettings.vue:148-162` + +**Severity**: Low + +**Description**: Event listeners are added with inline function reference. If component remounts rapidly, listeners could accumulate. + +**Recommendation**: Consider using a composable or ensuring the cleanup ref is properly nullified. + +**Status**: [ ] Not Fixed + +--- + +### Issue 8: YAML injection in skill editor + +**Location**: `src/renderer/settings/components/skills/SkillEditorSheet.vue:179-201` + +**Severity**: Medium + +**Description**: The `buildSkillContent()` function directly interpolates user input into YAML frontmatter without escaping. If name or description contains special YAML characters (quotes, colons, newlines), the resulting file could be malformed or inject unintended fields. + +```typescript +const buildSkillContent = (): string => { + const frontmatter = ['---'] + frontmatter.push(`name: "${editName.value}"`) // No escaping + frontmatter.push(`description: "${editDescription.value}"`) // No escaping + // ... +} +``` + +**Recommendation**: Use a proper YAML serializer like `yaml` or `js-yaml`: +```typescript +import yaml from 'js-yaml' + +const buildSkillContent = (): string => { + const frontmatter = { + name: editName.value, + description: editDescription.value, + ...(tools.length > 0 && { allowedTools: tools }) + } + return `---\n${yaml.dump(frontmatter)}---\n\n${editContent.value}` +} +``` + +**Status**: [ ] Not Fixed + +--- + +### Issue 9: Skill name change not handled + +**Location**: `src/renderer/settings/components/skills/SkillEditorSheet.vue:203-233` + +**Severity**: Medium + +**Description**: The editor allows changing the skill `name` field, but the save operation uses the original `props.skill.name`. If a user changes the name, the skill file is updated but the directory name remains unchanged, causing a mismatch between directory name and skill name in frontmatter. + +```typescript +const handleSave = async () => { + if (!props.skill) return + // ... + const result = await skillsStore.updateSkillFile(props.skill.name, content) // Uses old name + // ... +} +``` + +**Recommendation**: Either: +1. Make the name field read-only in the editor +2. Implement rename logic that renames the directory when name changes +3. Add validation to prevent name changes + +**Status**: [ ] Not Fixed + +--- + +### Issue 10: Drag-and-drop handlers show error but don't work + +**Location**: `src/renderer/settings/components/skills/SkillInstallDialog.vue:200-211, 238-247` + +**Severity**: Low + +**Description**: The UI has drag-and-drop visual feedback (border highlighting) but the actual handlers just show an error toast saying "drag not supported". This is confusing UX - the UI suggests drag is supported when it isn't. + +```typescript +const handleFolderDrop = async (event: DragEvent) => { + folderDragOver.value = false + // ... + toast({ + title: t('settings.skills.install.dragNotSupported'), + variant: 'destructive' + }) +} +``` + +**Recommendation**: Either: +1. Remove the drag-over visual feedback if drag is not supported +2. Implement proper drag-and-drop via IPC (Electron can get file paths from drag events) + +**Status**: [ ] Not Fixed + +--- + +### Issue 11: Unused `filePresenter` import + +**Location**: `src/renderer/settings/components/skills/SkillEditorSheet.vue:121` + +**Severity**: Low + +**Description**: The `filePresenter` is used to read skill file content, but the store already has methods to handle this. This creates an inconsistent pattern where some operations go through the store and others directly through presenters. + +```typescript +const filePresenter = usePresenter('filePresenter') +// ... +const content = await filePresenter.readFile(skill.path) +``` + +**Recommendation**: Add a `getSkillContent(name)` method to the store or use the existing `skillPresenter.loadSkillContent()` consistently. + +**Status**: [ ] Not Fixed + +--- + +### Issue 12: Missing URL validation + +**Location**: `src/renderer/settings/components/skills/SkillInstallDialog.vue:260-276` + +**Severity**: Low + +**Description**: The URL input accepts any string without validation. Invalid URLs will fail at the fetch stage, but early validation would provide better UX. + +```typescript +const installFromUrl = async () => { + if (!installUrl.value || installing.value) return // No URL format validation + await tryInstallFromUrl(installUrl.value) +} +``` + +**Recommendation**: Add URL format validation: +```typescript +const isValidUrl = (url: string): boolean => { + try { + const parsed = new URL(url) + return ['http:', 'https:'].includes(parsed.protocol) + } catch { + return false + } +} +``` + +**Status**: [ ] Not Fixed + +--- + +### Issue 13: SkillFolderTreeNode always starts expanded + +**Location**: `src/renderer/settings/components/skills/SkillFolderTreeNode.vue:42` + +**Severity**: Low + +**Description**: All directory nodes default to `expanded = true`. For skills with many nested directories, this could create a very long tree that's hard to navigate. + +```typescript +const expanded = ref(true) // Always expanded by default +``` + +**Recommendation**: Consider: +1. Only expand the first level by default +2. Accept an `initialExpanded` prop based on depth +3. Collapse all by default and let users expand as needed + +**Status**: [ ] Not Fixed + +--- + +## Suggestions (Non-Critical) + +### Suggestion 1: Type assertion in skillsPromptBuilder + +**Location**: `src/main/presenter/agentPresenter/message/skillsPromptBuilder.ts:21,58,90` + +**Description**: Repeated type assertion `presenter.skillPresenter as SkillPresenter` is needed because presenter type is `ISkillPresenter`. + +**Recommendation**: Update presenter definition to use concrete type or extend interface. + +--- + +### Suggestion 2: Backup cleanup strategy + +**Location**: `src/main/presenter/skillPresenter/index.ts:401-412` + +**Description**: `backupExistingSkill()` creates backups with timestamps but never cleans them up. Could accumulate over time. + +**Recommendation**: Consider: +- Limiting number of backups per skill (e.g., keep last 3) +- Adding a cleanup method +- Documenting backup location for users + +--- + +### Suggestion 3: Structured logging + +**Description**: Console logs use `[SkillPresenter]` prefix which is good, but consider using a structured logging utility for consistency with rest of codebase. + +--- + +### Suggestion 4: Initialize state tracking + +**Location**: `src/main/presenter/skillPresenter/index.ts:71-78` + +**Description**: The `initialized` flag is boolean. Consider tracking initialization state more granularly (pending/complete/error). + +--- + +## Test Coverage Gaps + +The following scenarios lack test coverage: + +1. URL download timeout/error scenarios +2. Symlink handling in folder tree +3. Concurrent `getMetadataList()` calls (race condition) +4. Very deep directory structures +5. Large skill file handling +6. Invalid frontmatter edge cases (e.g., `allowedTools: "string"` instead of array) + +--- + +## Summary + +| Severity | Count | +|----------|-------| +| High | 0 | +| Medium | 5 | +| Low | 8 | + +**Total Issues**: 13 + +The Skills system implementation is solid overall with good security practices (ZIP path traversal protection) and proper separation of concerns. The most critical issues to address are: + +1. **Issue 8 (YAML injection)** - Could cause malformed skill files +2. **Issue 9 (Name change not handled)** - Could cause mismatch between directory and skill name +3. **Issue 4 (URL download safety)** - Could hang application or exhaust memory + +The remaining issues are primarily defensive programming improvements and UX enhancements rather than critical bugs. diff --git a/docs/specs/skills-system/tasks.md b/docs/specs/skills-system/tasks.md index fb23a37a2..d254e3e04 100644 --- a/docs/specs/skills-system/tasks.md +++ b/docs/specs/skills-system/tasks.md @@ -121,55 +121,55 @@ ### 3.2 Pinia Store -- [~] **3.2.1** 创建 `src/renderer/src/stores/skills.ts`(已存在 `skillsStore.ts`) +- [x] **3.2.1** 创建 `src/renderer/src/stores/skills.ts`(已存在 `skillsStore.ts`) - [x] **3.2.2** 实现 state: `skills`, `loading`, `error` -- [~] **3.2.3** 实现 actions: `loadSkills`, `installFromFolder`, `installFromZip`, `installFromUrl`, `uninstall`, `updateSkill`(缺少 zip/url) +- [x] **3.2.3** 实现 actions: `loadSkills`, `installFromFolder`, `installFromZip`, `installFromUrl`, `uninstall`, `updateSkill` ### 3.3 主页面组件 -- [~] **3.3.1** 创建 `src/renderer/settings/components/skills/SkillsSettings.vue`(已存在简版页面) -- [~] **3.3.2** 实现页面整体布局(Header + ScrollArea + Footer)(无 Footer) +- [x] **3.3.1** 创建 `src/renderer/settings/components/skills/SkillsSettings.vue` +- [x] **3.3.2** 实现页面整体布局(Header + ScrollArea + Footer) - [x] **3.3.3** 实现空状态展示 -- [~] **3.3.4** 实现卡片网格布局(目前为列表) -- [ ] **3.3.5** 监听 SKILL_EVENTS 实时更新 +- [x] **3.3.4** 实现卡片网格布局(`grid grid-cols-1 md:grid-cols-2`) +- [x] **3.3.5** 监听 SKILL_EVENTS 实时更新 ### 3.4 Header 组件 -- [ ] **3.4.1** 创建 `SkillsHeader.vue` -- [ ] **3.4.2** 实现搜索输入框 -- [ ] **3.4.3** 实现导入下拉菜单(文件夹/ZIP/URL) -- [ ] **3.4.4** 实现安装按钮 +- [x] **3.4.1** 创建 `SkillsHeader.vue` +- [x] **3.4.2** 实现搜索输入框 +- [~] **3.4.3** 实现导入下拉菜单(文件夹/ZIP/URL)(通过 Dialog Tab 实现) +- [x] **3.4.4** 实现安装按钮 ### 3.5 Skill 卡片组件 -- [ ] **3.5.1** 创建 `SkillCard.vue` -- [ ] **3.5.2** 实现卡片展示(名称、描述、allowedTools) -- [ ] **3.5.3** 实现编辑/删除操作按钮 -- [ ] **3.5.4** 实现 hover 效果 +- [x] **3.5.1** 创建 `SkillCard.vue` +- [x] **3.5.2** 实现卡片展示(名称、描述、allowedTools) +- [x] **3.5.3** 实现编辑/删除操作按钮 +- [x] **3.5.4** 实现 hover 效果 ### 3.6 编辑侧边栏 -- [~] **3.6.1** 创建 `SkillEditorSheet.vue`(已内嵌在 SkillsSettings) -- [ ] **3.6.2** 实现 frontmatter 字段编辑(name, description, allowedTools) +- [x] **3.6.1** 创建 `SkillEditorSheet.vue`(独立组件) +- [x] **3.6.2** 实现 frontmatter 字段编辑(name, description, allowedTools) - [x] **3.6.3** 实现 Markdown 内容编辑 -- [ ] **3.6.4** 实现文件夹结构展示(只读) +- [x] **3.6.4** 实现文件夹结构展示(只读) - [x] **3.6.5** 实现保存逻辑(写回 SKILL.md) ### 3.7 安装对话框 -- [~] **3.7.1** 创建 `SkillInstallDialog.vue`(已内嵌 Dialog) -- [ ] **3.7.2** 实现 Tab 切换(文件夹/ZIP/URL) -- [~] **3.7.3** 实现文件夹选择(支持拖拽)(仅选择) -- [ ] **3.7.4** 实现 ZIP 文件选择(支持拖拽) -- [ ] **3.7.5** 实现 URL 输入 -- [ ] **3.7.6** 实现安装流程与进度提示 -- [ ] **3.7.7** 实现冲突确认对话框 +- [x] **3.7.1** 创建 `SkillInstallDialog.vue`(独立组件) +- [x] **3.7.2** 实现 Tab 切换(文件夹/ZIP/URL) +- [~] **3.7.3** 实现文件夹选择(支持拖拽)(仅选择,拖拽待实现) +- [~] **3.7.4** 实现 ZIP 文件选择(支持拖拽)(仅选择,拖拽待实现) +- [x] **3.7.5** 实现 URL 输入 +- [x] **3.7.6** 实现安装流程与进度提示 +- [x] **3.7.7** 实现冲突确认对话框 ### 3.8 文件夹树组件 -- [ ] **3.8.1** 创建 `SkillFolderTree.vue` +- [x] **3.8.1** 创建 `SkillFolderTree.vue` 和 `SkillFolderTreeNode.vue` - [x] **3.8.2** 实现 `getSkillFolderTree(name)` Presenter 方法 -- [ ] **3.8.3** 实现树形结构展示 +- [x] **3.8.3** 实现树形结构展示 ### 3.9 删除确认 @@ -182,28 +182,30 @@ ### 4.1 i18n -- [~] **4.1.1** 添加中文 i18n keys (`zh-CN`)(已部分添加) -- [~] **4.1.2** 添加英文 i18n keys (`en-US`)(已部分添加) -- [ ] **4.1.3** 运行 `pnpm run i18n` 检查完整性 +- [x] **4.1.1** 添加中文 i18n keys (`zh-CN`) +- [x] **4.1.2** 添加英文 i18n keys (`en-US`) +- [x] **4.1.3** 运行 `pnpm run i18n` 检查完整性 ### 4.2 内置 Skills -- [ ] **4.2.1** 设计并编写 1-2 个内置 Skill 示例 -- [ ] **4.2.2** 打包内置 Skills 到应用资源 -- [ ] **4.2.3** 首次启动时自动安装 +- [x] **4.2.1** 设计并编写 1-2 个内置 Skill 示例 + - `code-review`: 代码审查助手 + - `git-commit`: Git 提交信息生成助手 +- [x] **4.2.2** 打包内置 Skills 到应用资源(electron-builder.yml extraResources) +- [x] **4.2.3** 首次启动时自动安装(已在 SkillPresenter.initialize() 中实现) ### 4.3 测试 -- [ ] **4.3.1** SkillPresenter 单元测试 -- [ ] **4.3.2** skill_list / skill_control 工具测试 -- [ ] **4.3.3** 安装/卸载流程测试 +- [x] **4.3.1** SkillPresenter 单元测试 +- [x] **4.3.2** skill_list / skill_control 工具测试 +- [x] **4.3.3** 安装/卸载流程测试 - [ ] **4.3.4** UI 组件测试(可选) ### 4.4 文档与清理 - [ ] **4.4.1** 更新 README 或用户文档 - [ ] **4.4.2** 代码审查与清理 -- [ ] **4.4.3** 运行 `pnpm run format && pnpm run lint && pnpm run typecheck` +- [x] **4.4.3** 运行 `pnpm run format && pnpm run lint && pnpm run typecheck` --- diff --git a/electron-builder.yml b/electron-builder.yml index 3b2ace778..0fbe9ad91 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -35,6 +35,9 @@ extraResources: - from: ./resources/cdn/ to: app.asar.unpacked/resources/cdn filter: ['**/*'] + - from: ./resources/skills/ + to: app.asar.unpacked/resources/skills + filter: ['**/*'] electronLanguages: - zh-CN - zh-TW diff --git a/resources/skills/code-review/SKILL.md b/resources/skills/code-review/SKILL.md new file mode 100644 index 000000000..75691d8b5 --- /dev/null +++ b/resources/skills/code-review/SKILL.md @@ -0,0 +1,54 @@ +--- +name: code-review +description: Comprehensive code review assistant that analyzes code quality, security, and best practices +allowedTools: + - read_file + - list_files + - search_files +--- + +# Code Review Skill + +You are an expert code reviewer. When this skill is activated, you should: + +## Review Focus Areas + +1. **Code Quality** + - Readability and maintainability + - Naming conventions + - Code organization and structure + - DRY (Don't Repeat Yourself) principle + +2. **Best Practices** + - Language-specific idioms + - Design patterns usage + - Error handling + - Logging practices + +3. **Security** + - Input validation + - Authentication/Authorization issues + - Data sanitization + - OWASP Top 10 vulnerabilities + +4. **Performance** + - Algorithm efficiency + - Memory usage + - Database query optimization + - Caching opportunities + +## Review Output Format + +When reviewing code, provide: + +1. **Summary**: Brief overview of the code's purpose and quality +2. **Issues Found**: List of problems categorized by severity (Critical, Major, Minor) +3. **Suggestions**: Specific improvements with code examples +4. **Positive Aspects**: Highlight what's done well + +## Usage + +Activate this skill when: +- User asks for code review +- User wants feedback on their implementation +- User requests security audit of code diff --git a/resources/skills/git-commit/SKILL.md b/resources/skills/git-commit/SKILL.md new file mode 100644 index 000000000..a82c2337b --- /dev/null +++ b/resources/skills/git-commit/SKILL.md @@ -0,0 +1,59 @@ +--- +name: git-commit +description: Generate well-formatted git commit messages following conventional commit standards +allowedTools: + - run_terminal_cmd +--- + +# Git Commit Message Skill + +You are a git commit message expert. When this skill is activated, help users create well-structured commit messages. + +## Commit Message Format + +Follow the Conventional Commits specification: + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +## Types + +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **perf**: A code change that improves performance +- **test**: Adding missing tests or correcting existing tests +- **build**: Changes that affect the build system or external dependencies +- **ci**: Changes to CI configuration files and scripts +- **chore**: Other changes that don't modify src or test files + +## Guidelines + +1. **Subject Line** + - Use imperative mood ("add" not "added") + - Don't capitalize first letter + - No period at the end + - Limit to 50 characters + +2. **Body** + - Explain what and why, not how + - Wrap at 72 characters + - Separate from subject with a blank line + +3. **Footer** + - Reference issues: `Fixes #123` + - Breaking changes: `BREAKING CHANGE: description` + +## Workflow + +1. Run `git diff --staged` or `git status` to see changes +2. Analyze the changes to understand what was modified +3. Generate an appropriate commit message +4. Optionally run `git commit -m "message"` if user confirms diff --git a/src/main/presenter/devicePresenter/index.ts b/src/main/presenter/devicePresenter/index.ts index ca9ce9d11..463d46efa 100644 --- a/src/main/presenter/devicePresenter/index.ts +++ b/src/main/presenter/devicePresenter/index.ts @@ -476,6 +476,25 @@ export class DevicePresenter implements IDevicePresenter { }) } + /** + * 选择文件 + * @param options 文件选择选项 + * @returns 返回所选文件的路径,如果用户取消则返回空数组 + */ + async selectFiles(options?: { + filters?: { name: string; extensions: string[] }[] + multiple?: boolean + }): Promise<{ canceled: boolean; filePaths: string[] }> { + const properties: ('openFile' | 'multiSelections')[] = ['openFile'] + if (options?.multiple) { + properties.push('multiSelections') + } + return dialog.showOpenDialog({ + properties, + filters: options?.filters + }) + } + /** * 重启应用程序 */ diff --git a/src/renderer/settings/components/SkillsSettings.vue b/src/renderer/settings/components/SkillsSettings.vue deleted file mode 100644 index d72c993b2..000000000 --- a/src/renderer/settings/components/SkillsSettings.vue +++ /dev/null @@ -1,306 +0,0 @@ -