diff --git a/locales/en-US/discover.json b/locales/en-US/discover.json
index 802247f6c7..c7ac629d4f 100644
--- a/locales/en-US/discover.json
+++ b/locales/en-US/discover.json
@@ -477,11 +477,129 @@
"search.placeholder": "Search by name, description, or keywords...",
"search.result": "{{count}} results about {{keyword}}",
"search.searching": "Searching...",
+ "skillEmpty.description": "Try adjusting filters or searching with different keywords.",
+ "skillEmpty.search": "No matching Skills found",
+ "skillEmpty.title": "No Skills found",
+ "skills.categories.agent-to-agent-protocols.description": "Inter-agent communication, orchestration, and protocol skills",
+ "skills.categories.agent-to-agent-protocols.name": "Agent-to-Agent Protocols",
+ "skills.categories.ai-llms.description": "AI model integrations, LLM tooling, and prompt engineering skills",
+ "skills.categories.ai-llms.name": "AI & LLMs",
+ "skills.categories.all.description": "All Skills",
+ "skills.categories.all.name": "All",
+ "skills.categories.apple-apps-services.description": "Apple ecosystem apps, services, and platform integrations",
+ "skills.categories.apple-apps-services.name": "Apple Apps & Services",
+ "skills.categories.browser-automation.description": "Browser control, web scraping, and UI automation skills",
+ "skills.categories.browser-automation.name": "Browser & Automation",
+ "skills.categories.calendar-scheduling.description": "Calendar management, meeting scheduling, and time coordination skills",
+ "skills.categories.calendar-scheduling.name": "Calendar & Scheduling",
+ "skills.categories.clawdbot-tools.description": "Skills and utilities built for the Clawdbot ecosystem",
+ "skills.categories.clawdbot-tools.name": "Clawdbot Tools",
+ "skills.categories.cli-utilities.description": "Command-line tools, shell scripting, and terminal utilities",
+ "skills.categories.cli-utilities.name": "CLI Utilities",
+ "skills.categories.coding-agents-ides.description": "Skills for coding agents, IDEs, and AI-assisted development",
+ "skills.categories.coding-agents-ides.name": "Coding Agents & IDEs",
+ "skills.categories.communication.description": "Messaging, email, chat platforms, and communication workflow skills",
+ "skills.categories.communication.name": "Communication",
+ "skills.categories.data-analytics.description": "Data analysis, visualization, and business intelligence skills",
+ "skills.categories.data-analytics.name": "Data & Analytics",
+ "skills.categories.devops-cloud.description": "DevOps pipelines, cloud infrastructure, and deployment skills",
+ "skills.categories.devops-cloud.name": "DevOps & Cloud",
+ "skills.categories.finance.description": "Finance, banking, payments, and financial data skills",
+ "skills.categories.finance.name": "Finance",
+ "skills.categories.gaming.description": "Game data, achievements, leaderboards, and gaming platform skills",
+ "skills.categories.gaming.name": "Gaming",
+ "skills.categories.git-github.description": "Git version control and GitHub platform integrations",
+ "skills.categories.git-github.name": "Git & GitHub",
+ "skills.categories.health-fitness.description": "Health tracking, fitness planning, and wellness skills",
+ "skills.categories.health-fitness.name": "Health & Fitness",
+ "skills.categories.image-video-generation.description": "AI image generation, video creation, and visual media skills",
+ "skills.categories.image-video-generation.name": "Image & Video Generation",
+ "skills.categories.ios-macos-development.description": "iOS and macOS app development, Xcode, and Swift tooling skills",
+ "skills.categories.ios-macos-development.name": "iOS & macOS Development",
+ "skills.categories.marketing-sales.description": "Marketing campaigns, sales workflows, and growth automation skills",
+ "skills.categories.marketing-sales.name": "Marketing & Sales",
+ "skills.categories.media-streaming.description": "Media playback, streaming platforms, and content delivery skills",
+ "skills.categories.media-streaming.name": "Media & Streaming",
+ "skills.categories.moltbook.description": "Moltbook platform integrations and notebook automation skills",
+ "skills.categories.moltbook.name": "Moltbook",
+ "skills.categories.notes-pkm.description": "Note-taking, personal knowledge management, and second-brain skills",
+ "skills.categories.notes-pkm.name": "Notes & PKM",
+ "skills.categories.pdf-documents.description": "PDF processing, document parsing, and file management skills",
+ "skills.categories.pdf-documents.name": "PDF & Documents",
+ "skills.categories.personal-development.description": "Personal growth, habit building, and self-improvement skills",
+ "skills.categories.personal-development.name": "Personal Development",
+ "skills.categories.productivity-tasks.description": "Task management, workflow automation, and productivity skills",
+ "skills.categories.productivity-tasks.name": "Productivity & Tasks",
+ "skills.categories.search-research.description": "Web search, data retrieval, and research automation skills",
+ "skills.categories.search-research.name": "Search & Research",
+ "skills.categories.security-passwords.description": "Security auditing, password management, and privacy protection skills",
+ "skills.categories.security-passwords.name": "Security & Passwords",
+ "skills.categories.self-hosted-automation.description": "Self-hosted services, home lab automation, and infrastructure skills",
+ "skills.categories.self-hosted-automation.name": "Self-Hosted & Automation",
+ "skills.categories.shopping-ecommerce.description": "E-commerce integrations, shopping automation, and retail skills",
+ "skills.categories.shopping-ecommerce.name": "Shopping & E-commerce",
+ "skills.categories.smart-home-iot.description": "Smart home automation, IoT device control, and home management skills",
+ "skills.categories.smart-home-iot.name": "Smart Home & IoT",
+ "skills.categories.speech-transcription.description": "Speech recognition, audio transcription, and voice interface skills",
+ "skills.categories.speech-transcription.name": "Speech & Transcription",
+ "skills.categories.transportation.description": "Transportation, logistics, routing, and mobility skills",
+ "skills.categories.transportation.name": "Transportation",
+ "skills.categories.web-frontend-development.description": "Web development, frontend frameworks, and UI tooling skills",
+ "skills.categories.web-frontend-development.name": "Web & Frontend Development",
+ "skills.details.nav.needHelp": "Need Help?",
+ "skills.details.nav.reportIssue": "Report Issue",
+ "skills.details.nav.viewSourceCode": "View Source Code",
+ "skills.details.overview.title": "Overview",
+ "skills.details.rating.title": "Ratings",
+ "skills.details.related.empty": "No related Skills yet",
+ "skills.details.related.listTitle": "Related Skills",
+ "skills.details.related.more": "View More",
+ "skills.details.related.title": "Related Skills",
+ "skills.details.resources.empty": "No resources available",
+ "skills.details.resources.table.name": "Name",
+ "skills.details.resources.table.size": "Size",
+ "skills.details.resources.title": "Resources",
+ "skills.details.review.title": "How to Submit a Review",
+ "skills.details.sidebar.agent.copied": "Prompt Copied",
+ "skills.details.sidebar.agent.copyPrompt": "Copy Prompt",
+ "skills.details.sidebar.agent.title": "Send this prompt to your Agent to install this Skill",
+ "skills.details.sidebar.agent.useOnLobeAI": "Use on LobeAI",
+ "skills.details.sidebar.description": "Description",
+ "skills.details.sidebar.details": "Details",
+ "skills.details.sidebar.directoryLayout": "Directory Layout",
+ "skills.details.sidebar.downloadSkill": "Download Skill",
+ "skills.details.sidebar.files": "File Tree",
+ "skills.details.sidebar.installCommand": "Install Command",
+ "skills.details.sidebar.installationConfig": "Installation",
+ "skills.details.sidebar.platform.layout.lobehub": "Skills are managed by LobeHub automatically",
+ "skills.details.sidebar.platform.layout.resourcesHint": "other resources",
+ "skills.details.sidebar.platform.steps.claude": "Run the install command in your terminal to download and configure this skill for Claude Code.",
+ "skills.details.sidebar.platform.steps.cline": "Run the install command in your terminal to download and configure this skill for Cline.",
+ "skills.details.sidebar.platform.steps.codex": "Run the install command in your terminal to download and configure this skill for Codex.",
+ "skills.details.sidebar.platform.steps.cursor": "Run the install command in your terminal to download and configure this skill for Cursor.",
+ "skills.details.sidebar.platform.steps.lobehub": "Install directly from the LobeHub marketplace with one click.",
+ "skills.details.sidebar.platform.steps.vscode": "Run the install command in your terminal to download and configure this skill for VS Code.",
+ "skills.details.sidebar.platform.title": "Install on {{platform}}",
+ "skills.details.sidebar.tags": "Tags",
+ "skills.details.summary.title": "Summary",
+ "skills.details.versions.empty": "No historical versions yet",
+ "skills.details.versions.table.isLatest": "Latest",
+ "skills.details.versions.table.publishAt": "Published",
+ "skills.details.versions.table.version": "Version",
+ "skills.details.versions.title": "Version History",
+ "skills.hero.guide.agent": "I am Agent",
+ "skills.hero.guide.human": "I am Human",
+ "skills.sorts.createdAt": "Recently Published",
+ "skills.sorts.installCount": "Downloads",
+ "skills.sorts.name": "Name",
+ "skills.sorts.stars": "GitHub Stars",
+ "skills.sorts.updatedAt": "Recently Updated",
"tab.assistant": "Agent",
"tab.home": "Home",
"tab.model": "Model",
"tab.plugin": "Skill",
"tab.provider": "Provider",
+ "tab.skill": "Skills",
"tab.user": "User",
"time.formatOtherYear": "MMM D, YYYY",
"time.formatThisYear": "MMM D",
diff --git a/locales/zh-CN/discover.json b/locales/zh-CN/discover.json
index eea326df5a..90b5e32cd3 100644
--- a/locales/zh-CN/discover.json
+++ b/locales/zh-CN/discover.json
@@ -477,11 +477,129 @@
"search.placeholder": "搜索名称介绍或关键词…",
"search.result": "{{count}} 个关于 {{keyword}} 的搜索结果",
"search.searching": "搜索中…",
+ "skillEmpty.description": "尝试调整筛选条件或搜索关键词",
+ "skillEmpty.search": "未找到匹配的技能",
+ "skillEmpty.title": "暂无技能",
+ "skills.categories.agent-to-agent-protocols.description": "代理间通信、协调和协议技能",
+ "skills.categories.agent-to-agent-protocols.name": "代理协议",
+ "skills.categories.ai-llms.description": "AI模型集成、大语言模型工具和提示词工程技能",
+ "skills.categories.ai-llms.name": "AI & LLMs",
+ "skills.categories.all.description": "全部技能",
+ "skills.categories.all.name": "全部",
+ "skills.categories.apple-apps-services.description": "苹果生态系统应用、服务和平台集成",
+ "skills.categories.apple-apps-services.name": "苹果应用与服务",
+ "skills.categories.browser-automation.description": "浏览器控制、网页抓取和UI自动化技能",
+ "skills.categories.browser-automation.name": "浏览器自动化",
+ "skills.categories.calendar-scheduling.description": "日历管理、会议安排和时间协调技能",
+ "skills.categories.calendar-scheduling.name": "日历与日程",
+ "skills.categories.clawdbot-tools.description": "为 Clawdbot 生态系统构建的技能和工具",
+ "skills.categories.clawdbot-tools.name": "Clawdbot 工具",
+ "skills.categories.cli-utilities.description": "命令行工具、Shell脚本和终端工具",
+ "skills.categories.cli-utilities.name": "CLI 工具",
+ "skills.categories.coding-agents-ides.description": "用于编程代理、IDE和AI辅助开发的技能",
+ "skills.categories.coding-agents-ides.name": "编程代理与IDE",
+ "skills.categories.communication.description": "消息传递、电子邮件、聊天平台和通信工作流技能",
+ "skills.categories.communication.name": "通信",
+ "skills.categories.data-analytics.description": "数据分析、可视化和商业智能技能",
+ "skills.categories.data-analytics.name": "数据分析",
+ "skills.categories.devops-cloud.description": "DevOps流水线、云基础设施和部署技能",
+ "skills.categories.devops-cloud.name": "DevOps与云",
+ "skills.categories.finance.description": "金融、银行、支付和财务数据技能",
+ "skills.categories.finance.name": "金融",
+ "skills.categories.gaming.description": "游戏数据、成就、排行榜和游戏平台技能",
+ "skills.categories.gaming.name": "游戏",
+ "skills.categories.git-github.description": "Git版本控制和GitHub平台集成",
+ "skills.categories.git-github.name": "Git与GitHub",
+ "skills.categories.health-fitness.description": "健康追踪、健身计划和健康技能",
+ "skills.categories.health-fitness.name": "健康健身",
+ "skills.categories.image-video-generation.description": "AI图像生成、视频创作和视觉媒体技能",
+ "skills.categories.image-video-generation.name": "图像与视频生成",
+ "skills.categories.ios-macos-development.description": "iOS和macOS应用开发、Xcode和Swift工具技能",
+ "skills.categories.ios-macos-development.name": "iOS与macOS开发",
+ "skills.categories.marketing-sales.description": "营销活动、销售工作流和增长自动化技能",
+ "skills.categories.marketing-sales.name": "营销与销售",
+ "skills.categories.media-streaming.description": "媒体播放、流媒体平台和内容分发技能",
+ "skills.categories.media-streaming.name": "媒体与流媒体",
+ "skills.categories.moltbook.description": "Moltbook平台集成和笔记本自动化技能",
+ "skills.categories.moltbook.name": "Moltbook",
+ "skills.categories.notes-pkm.description": "笔记、个人知识管理和第二大脑技能",
+ "skills.categories.notes-pkm.name": "笔记与知识管理",
+ "skills.categories.pdf-documents.description": "PDF处理、文档解析和文件管理技能",
+ "skills.categories.pdf-documents.name": "PDF与文档",
+ "skills.categories.personal-development.description": "个人成长、习惯养成和自我提升技能",
+ "skills.categories.personal-development.name": "个人发展",
+ "skills.categories.productivity-tasks.description": "任务管理、工作流自动化和生产力技能",
+ "skills.categories.productivity-tasks.name": "生产力与任务",
+ "skills.categories.search-research.description": "网页搜索、数据检索和研究自动化技能",
+ "skills.categories.search-research.name": "搜索与研究",
+ "skills.categories.security-passwords.description": "安全审计、密码管理和隐私保护技能",
+ "skills.categories.security-passwords.name": "安全与密码",
+ "skills.categories.self-hosted-automation.description": "自托管服务、家庭实验室自动化和基础设施技能",
+ "skills.categories.self-hosted-automation.name": "自托管与自动化",
+ "skills.categories.shopping-ecommerce.description": "电子商务集成、购物自动化和零售技能",
+ "skills.categories.shopping-ecommerce.name": "购物与电商",
+ "skills.categories.smart-home-iot.description": "智能家居自动化、物联网设备控制和家居管理技能",
+ "skills.categories.smart-home-iot.name": "智能家居与物联网",
+ "skills.categories.speech-transcription.description": "语音识别、音频转录和语音接口技能",
+ "skills.categories.speech-transcription.name": "语音与转录",
+ "skills.categories.transportation.description": "交通、物流、路线规划和移动技能",
+ "skills.categories.transportation.name": "交通运输",
+ "skills.categories.web-frontend-development.description": "Web开发、前端框架和UI工具技能",
+ "skills.categories.web-frontend-development.name": "Web与前端开发",
+ "skills.details.nav.needHelp": "需要帮助?",
+ "skills.details.nav.reportIssue": "报告问题",
+ "skills.details.nav.viewSourceCode": "查看源码",
+ "skills.details.overview.title": "概览",
+ "skills.details.rating.title": "评分",
+ "skills.details.related.empty": "暂无相关技能",
+ "skills.details.related.listTitle": "相关技能",
+ "skills.details.related.more": "查看更多",
+ "skills.details.related.title": "相关技能",
+ "skills.details.resources.empty": "暂无资源",
+ "skills.details.resources.table.name": "名称",
+ "skills.details.resources.table.size": "大小",
+ "skills.details.resources.title": "资源",
+ "skills.details.review.title": "如何提交评价",
+ "skills.details.sidebar.agent.copied": "已复制提示词",
+ "skills.details.sidebar.agent.copyPrompt": "复制提示词",
+ "skills.details.sidebar.agent.title": "发送此提示词给你的 Agent 以安装此技能",
+ "skills.details.sidebar.agent.useOnLobeAI": "在 LobeAI 上使用",
+ "skills.details.sidebar.description": "描述",
+ "skills.details.sidebar.details": "详情",
+ "skills.details.sidebar.directoryLayout": "目录布局",
+ "skills.details.sidebar.downloadSkill": "下载技能",
+ "skills.details.sidebar.files": "文件树",
+ "skills.details.sidebar.installCommand": "安装命令",
+ "skills.details.sidebar.installationConfig": "安装方式",
+ "skills.details.sidebar.platform.layout.lobehub": "技能由 LobeHub 自动管理",
+ "skills.details.sidebar.platform.layout.resourcesHint": "其他资源",
+ "skills.details.sidebar.platform.steps.claude": "在终端运行安装命令,为 Claude Code 下载并配置此技能。",
+ "skills.details.sidebar.platform.steps.cline": "在终端运行安装命令,为 Cline 下载并配置此技能。",
+ "skills.details.sidebar.platform.steps.codex": "在终端运行安装命令,为 Codex 下载并配置此技能。",
+ "skills.details.sidebar.platform.steps.cursor": "在终端运行安装命令,为 Cursor 下载并配置此技能。",
+ "skills.details.sidebar.platform.steps.lobehub": "直接从 LobeHub 市场一键安装。",
+ "skills.details.sidebar.platform.steps.vscode": "在终端运行安装命令,为 VS Code 下载并配置此技能。",
+ "skills.details.sidebar.platform.title": "在 {{platform}} 中安装",
+ "skills.details.sidebar.tags": "标签",
+ "skills.details.summary.title": "摘要",
+ "skills.details.versions.empty": "暂无历史版本",
+ "skills.details.versions.table.isLatest": "最新版本",
+ "skills.details.versions.table.publishAt": "发布于",
+ "skills.details.versions.table.version": "版本",
+ "skills.details.versions.title": "版本历史",
+ "skills.hero.guide.agent": "我是 Agent",
+ "skills.hero.guide.human": "我是人类",
+ "skills.sorts.createdAt": "最近发布",
+ "skills.sorts.installCount": "最多下载",
+ "skills.sorts.name": "名称",
+ "skills.sorts.stars": "GitHub 星标",
+ "skills.sorts.updatedAt": "最近更新",
"tab.assistant": "助理",
"tab.home": "首页",
"tab.model": "模型",
"tab.plugin": "技能",
"tab.provider": "模型服务商",
+ "tab.skill": "Skills",
"tab.user": "用户",
"time.formatOtherYear": "YYYY年M月D日",
"time.formatThisYear": "M月D日",
diff --git a/packages/types/src/discover/index.ts b/packages/types/src/discover/index.ts
index 23a5aeb7cc..6d67077b78 100644
--- a/packages/types/src/discover/index.ts
+++ b/packages/types/src/discover/index.ts
@@ -8,6 +8,7 @@ export * from './mcp';
export * from './models';
export * from './plugins';
export * from './providers';
+export * from './skills';
export enum DiscoverTab {
Assistants = 'agent',
@@ -17,6 +18,7 @@ export enum DiscoverTab {
Models = 'model',
Plugins = 'plugin',
Providers = 'provider',
+ Skills = 'skill',
User = 'user',
}
@@ -32,6 +34,7 @@ export enum CacheTag {
Models = 'models',
Plugins = 'plugins',
Providers = 'providers',
+ Skills = 'skills',
}
export enum CacheRevalidate {
diff --git a/packages/types/src/discover/skills.ts b/packages/types/src/discover/skills.ts
new file mode 100644
index 0000000000..6ff5d4e33a
--- /dev/null
+++ b/packages/types/src/discover/skills.ts
@@ -0,0 +1,96 @@
+import type {
+ MarketSkillCategory,
+ MarketSkillDetail,
+ MarketSkillListItem,
+ MarketSkillListResponse,
+ SkillCommentListResponse,
+ SkillRatingDistribution,
+} from '@lobehub/market-sdk';
+
+export enum SkillCategory {
+ AgentToAgentProtocols = 'agent-to-agent-protocols',
+ AILLMs = 'ai-llms',
+ All = 'all',
+ AppleAppsServices = 'apple-apps-services',
+ BrowserAutomation = 'browser-automation',
+ CalendarScheduling = 'calendar-scheduling',
+ ClawdbotTools = 'clawdbot-tools',
+ CLIUtilities = 'cli-utilities',
+ CodingAgentsIDEs = 'coding-agents-ides',
+ Communication = 'communication',
+ DataAnalytics = 'data-analytics',
+ DevOpsCloud = 'devops-cloud',
+ Finance = 'finance',
+ Gaming = 'gaming',
+ GitGitHub = 'git-github',
+ HealthFitness = 'health-fitness',
+ ImageVideoGeneration = 'image-video-generation',
+ IOSMacOSDevelopment = 'ios-macos-development',
+ MarketingSales = 'marketing-sales',
+ MediaStreaming = 'media-streaming',
+ Moltbook = 'moltbook',
+ NotesPKM = 'notes-pkm',
+ PDFDocuments = 'pdf-documents',
+ PersonalDevelopment = 'personal-development',
+ ProductivityTasks = 'productivity-tasks',
+ SearchResearch = 'search-research',
+ SecurityPasswords = 'security-passwords',
+ SelfHostedAutomation = 'self-hosted-automation',
+ ShoppingEcommerce = 'shopping-ecommerce',
+ SmartHomeIoT = 'smart-home-iot',
+ SpeechTranscription = 'speech-transcription',
+ Transportation = 'transportation',
+ WebFrontendDevelopment = 'web-frontend-development',
+}
+
+export enum SkillSorts {
+ CreatedAt = 'createdAt',
+ InstallCount = 'installCount',
+ Name = 'name',
+ Relevance = 'relevance',
+ Stars = 'stars',
+ UpdatedAt = 'updatedAt',
+}
+
+export enum SkillNavKey {
+ Installation = 'installation',
+ Overview = 'overview',
+ Related = 'related',
+ Resources = 'resources',
+ Skill = 'skill',
+ Version = 'version',
+}
+
+export interface DiscoverSkillItem extends Omit {
+ commentCount?: number;
+ homepage?: string;
+ ratingAvg?: number;
+}
+
+export interface SkillQueryParams {
+ category?: string;
+ locale?: string;
+ order?: 'asc' | 'desc';
+ page?: number;
+ pageSize?: number;
+ q?: string;
+ sort?: SkillSorts;
+}
+
+export interface SkillListResponse extends MarketSkillListResponse {
+ categories?: SkillCategoryItem[];
+}
+
+export interface DiscoverSkillDetail extends MarketSkillDetail {
+ comments?: SkillCommentListResponse;
+ downloadUrl?: string;
+ github?: {
+ stars?: number;
+ url?: string;
+ };
+ homepage?: string;
+ ratingDistribution?: SkillRatingDistribution;
+ related?: DiscoverSkillItem[];
+}
+
+export type SkillCategoryItem = MarketSkillCategory;
diff --git a/src/hooks/useSkillCategory.tsx b/src/hooks/useSkillCategory.tsx
new file mode 100644
index 0000000000..6adfa4d09e
--- /dev/null
+++ b/src/hooks/useSkillCategory.tsx
@@ -0,0 +1,253 @@
+import {
+ AppleIcon,
+ BarChart2Icon,
+ BookIcon,
+ BookOpenIcon,
+ BotIcon,
+ BrainIcon,
+ CalendarIcon,
+ CheckSquareIcon,
+ CloudIcon,
+ DollarSignIcon,
+ FileTextIcon,
+ GamepadIcon,
+ GitBranchIcon,
+ GlobeIcon,
+ HeartIcon,
+ HomeIcon,
+ ImageIcon,
+ LayoutPanelTopIcon,
+ MegaphoneIcon,
+ MessageCircleIcon,
+ MicIcon,
+ MonitorIcon,
+ NetworkIcon,
+ PlayIcon,
+ SearchIcon,
+ ServerIcon,
+ ShieldIcon,
+ ShoppingCartIcon,
+ SmartphoneIcon,
+ TerminalIcon,
+ TruckIcon,
+ UserIcon,
+ WrenchIcon,
+} from 'lucide-react';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { SkillCategory } from '@/types/discover';
+
+export const useSkillCategory = () => {
+ const { t } = useTranslation('discover');
+ return useMemo(
+ () => [
+ {
+ icon: LayoutPanelTopIcon,
+ key: SkillCategory.All,
+ label: t('skills.categories.all.name'),
+ title: t('skills.categories.all.description'),
+ },
+ // Sorted by category count (descending)
+ {
+ icon: BotIcon,
+ key: SkillCategory.CodingAgentsIDEs,
+ label: t('skills.categories.coding-agents-ides.name'),
+ title: t('skills.categories.coding-agents-ides.description'),
+ },
+ {
+ icon: MonitorIcon,
+ key: SkillCategory.WebFrontendDevelopment,
+ label: t('skills.categories.web-frontend-development.name'),
+ title: t('skills.categories.web-frontend-development.description'),
+ },
+ {
+ icon: CloudIcon,
+ key: SkillCategory.DevOpsCloud,
+ label: t('skills.categories.devops-cloud.name'),
+ title: t('skills.categories.devops-cloud.description'),
+ },
+ {
+ icon: SearchIcon,
+ key: SkillCategory.SearchResearch,
+ label: t('skills.categories.search-research.name'),
+ title: t('skills.categories.search-research.description'),
+ },
+ {
+ icon: GlobeIcon,
+ key: SkillCategory.BrowserAutomation,
+ label: t('skills.categories.browser-automation.name'),
+ title: t('skills.categories.browser-automation.description'),
+ },
+ {
+ icon: CheckSquareIcon,
+ key: SkillCategory.ProductivityTasks,
+ label: t('skills.categories.productivity-tasks.name'),
+ title: t('skills.categories.productivity-tasks.description'),
+ },
+ {
+ icon: BrainIcon,
+ key: SkillCategory.AILLMs,
+ label: t('skills.categories.ai-llms.name'),
+ title: t('skills.categories.ai-llms.description'),
+ },
+ {
+ icon: TerminalIcon,
+ key: SkillCategory.CLIUtilities,
+ label: t('skills.categories.cli-utilities.name'),
+ title: t('skills.categories.cli-utilities.description'),
+ },
+ {
+ icon: GitBranchIcon,
+ key: SkillCategory.GitGitHub,
+ label: t('skills.categories.git-github.name'),
+ title: t('skills.categories.git-github.description'),
+ },
+ {
+ icon: ImageIcon,
+ key: SkillCategory.ImageVideoGeneration,
+ label: t('skills.categories.image-video-generation.name'),
+ title: t('skills.categories.image-video-generation.description'),
+ },
+ {
+ icon: MessageCircleIcon,
+ key: SkillCategory.Communication,
+ label: t('skills.categories.communication.name'),
+ title: t('skills.categories.communication.description'),
+ },
+ {
+ icon: TruckIcon,
+ key: SkillCategory.Transportation,
+ label: t('skills.categories.transportation.name'),
+ title: t('skills.categories.transportation.description'),
+ },
+ {
+ icon: FileTextIcon,
+ key: SkillCategory.PDFDocuments,
+ label: t('skills.categories.pdf-documents.name'),
+ title: t('skills.categories.pdf-documents.description'),
+ },
+ {
+ icon: MegaphoneIcon,
+ key: SkillCategory.MarketingSales,
+ label: t('skills.categories.marketing-sales.name'),
+ title: t('skills.categories.marketing-sales.description'),
+ },
+ {
+ icon: HeartIcon,
+ key: SkillCategory.HealthFitness,
+ label: t('skills.categories.health-fitness.name'),
+ title: t('skills.categories.health-fitness.description'),
+ },
+ {
+ icon: PlayIcon,
+ key: SkillCategory.MediaStreaming,
+ label: t('skills.categories.media-streaming.name'),
+ title: t('skills.categories.media-streaming.description'),
+ },
+ {
+ icon: BookOpenIcon,
+ key: SkillCategory.NotesPKM,
+ label: t('skills.categories.notes-pkm.name'),
+ title: t('skills.categories.notes-pkm.description'),
+ },
+ {
+ icon: CalendarIcon,
+ key: SkillCategory.CalendarScheduling,
+ label: t('skills.categories.calendar-scheduling.name'),
+ title: t('skills.categories.calendar-scheduling.description'),
+ },
+ {
+ icon: ShoppingCartIcon,
+ key: SkillCategory.ShoppingEcommerce,
+ label: t('skills.categories.shopping-ecommerce.name'),
+ title: t('skills.categories.shopping-ecommerce.description'),
+ },
+ {
+ icon: ShieldIcon,
+ key: SkillCategory.SecurityPasswords,
+ label: t('skills.categories.security-passwords.name'),
+ title: t('skills.categories.security-passwords.description'),
+ },
+ {
+ icon: UserIcon,
+ key: SkillCategory.PersonalDevelopment,
+ label: t('skills.categories.personal-development.name'),
+ title: t('skills.categories.personal-development.description'),
+ },
+ {
+ icon: MicIcon,
+ key: SkillCategory.SpeechTranscription,
+ label: t('skills.categories.speech-transcription.name'),
+ title: t('skills.categories.speech-transcription.description'),
+ },
+ {
+ icon: AppleIcon,
+ key: SkillCategory.AppleAppsServices,
+ label: t('skills.categories.apple-apps-services.name'),
+ title: t('skills.categories.apple-apps-services.description'),
+ },
+ {
+ icon: HomeIcon,
+ key: SkillCategory.SmartHomeIoT,
+ label: t('skills.categories.smart-home-iot.name'),
+ title: t('skills.categories.smart-home-iot.description'),
+ },
+ {
+ icon: GamepadIcon,
+ key: SkillCategory.Gaming,
+ label: t('skills.categories.gaming.name'),
+ title: t('skills.categories.gaming.description'),
+ },
+ {
+ icon: WrenchIcon,
+ key: SkillCategory.ClawdbotTools,
+ label: t('skills.categories.clawdbot-tools.name'),
+ title: t('skills.categories.clawdbot-tools.description'),
+ },
+ {
+ icon: ServerIcon,
+ key: SkillCategory.SelfHostedAutomation,
+ label: t('skills.categories.self-hosted-automation.name'),
+ title: t('skills.categories.self-hosted-automation.description'),
+ },
+ {
+ icon: SmartphoneIcon,
+ key: SkillCategory.IOSMacOSDevelopment,
+ label: t('skills.categories.ios-macos-development.name'),
+ title: t('skills.categories.ios-macos-development.description'),
+ },
+ {
+ icon: BookIcon,
+ key: SkillCategory.Moltbook,
+ label: t('skills.categories.moltbook.name'),
+ title: t('skills.categories.moltbook.description'),
+ },
+ {
+ icon: BarChart2Icon,
+ key: SkillCategory.DataAnalytics,
+ label: t('skills.categories.data-analytics.name'),
+ title: t('skills.categories.data-analytics.description'),
+ },
+ {
+ icon: DollarSignIcon,
+ key: SkillCategory.Finance,
+ label: t('skills.categories.finance.name'),
+ title: t('skills.categories.finance.description'),
+ },
+ {
+ icon: NetworkIcon,
+ key: SkillCategory.AgentToAgentProtocols,
+ label: t('skills.categories.agent-to-agent-protocols.name'),
+ title: t('skills.categories.agent-to-agent-protocols.description'),
+ },
+ ],
+ [t],
+ );
+};
+
+export const useSkillCategoryItem = (key?: string) => {
+ const items = useSkillCategory();
+ if (!key) return;
+ return items.find((item) => item.key === key);
+};
diff --git a/src/locales/default/discover.ts b/src/locales/default/discover.ts
index d9c1e15e1d..5901ec224a 100644
--- a/src/locales/default/discover.ts
+++ b/src/locales/default/discover.ts
@@ -865,6 +865,244 @@ export default {
'search.searching': 'Searching...',
+ 'skillEmpty.description': 'Try adjusting filters or searching with different keywords.',
+
+ 'skillEmpty.search': 'No matching Skills found',
+
+ 'skillEmpty.title': 'No Skills found',
+
+ 'skills.categories.agent-to-agent-protocols.description':
+ 'Inter-agent communication, orchestration, and protocol skills',
+ 'skills.categories.agent-to-agent-protocols.name': 'Agent-to-Agent Protocols',
+
+ 'skills.categories.ai-llms.description':
+ 'AI model integrations, LLM tooling, and prompt engineering skills',
+ 'skills.categories.ai-llms.name': 'AI & LLMs',
+
+ 'skills.categories.all.description': 'All Skills',
+ 'skills.categories.all.name': 'All',
+
+ 'skills.categories.apple-apps-services.description':
+ 'Apple ecosystem apps, services, and platform integrations',
+ 'skills.categories.apple-apps-services.name': 'Apple Apps & Services',
+
+ 'skills.categories.browser-automation.description':
+ 'Browser control, web scraping, and UI automation skills',
+ 'skills.categories.browser-automation.name': 'Browser & Automation',
+
+ 'skills.categories.calendar-scheduling.description':
+ 'Calendar management, meeting scheduling, and time coordination skills',
+ 'skills.categories.calendar-scheduling.name': 'Calendar & Scheduling',
+
+ 'skills.categories.clawdbot-tools.description':
+ 'Skills and utilities built for the Clawdbot ecosystem',
+ 'skills.categories.clawdbot-tools.name': 'Clawdbot Tools',
+
+ 'skills.categories.cli-utilities.description':
+ 'Command-line tools, shell scripting, and terminal utilities',
+ 'skills.categories.cli-utilities.name': 'CLI Utilities',
+
+ 'skills.categories.coding-agents-ides.description':
+ 'Skills for coding agents, IDEs, and AI-assisted development',
+ 'skills.categories.coding-agents-ides.name': 'Coding Agents & IDEs',
+
+ 'skills.categories.communication.description':
+ 'Messaging, email, chat platforms, and communication workflow skills',
+ 'skills.categories.communication.name': 'Communication',
+
+ 'skills.categories.data-analytics.description':
+ 'Data analysis, visualization, and business intelligence skills',
+ 'skills.categories.data-analytics.name': 'Data & Analytics',
+
+ 'skills.categories.devops-cloud.description':
+ 'DevOps pipelines, cloud infrastructure, and deployment skills',
+ 'skills.categories.devops-cloud.name': 'DevOps & Cloud',
+
+ 'skills.categories.finance.description': 'Finance, banking, payments, and financial data skills',
+ 'skills.categories.finance.name': 'Finance',
+
+ 'skills.categories.gaming.description':
+ 'Game data, achievements, leaderboards, and gaming platform skills',
+ 'skills.categories.gaming.name': 'Gaming',
+
+ 'skills.categories.git-github.description':
+ 'Git version control and GitHub platform integrations',
+ 'skills.categories.git-github.name': 'Git & GitHub',
+
+ 'skills.categories.health-fitness.description':
+ 'Health tracking, fitness planning, and wellness skills',
+ 'skills.categories.health-fitness.name': 'Health & Fitness',
+
+ 'skills.categories.image-video-generation.description':
+ 'AI image generation, video creation, and visual media skills',
+ 'skills.categories.image-video-generation.name': 'Image & Video Generation',
+
+ 'skills.categories.ios-macos-development.description':
+ 'iOS and macOS app development, Xcode, and Swift tooling skills',
+ 'skills.categories.ios-macos-development.name': 'iOS & macOS Development',
+
+ 'skills.categories.marketing-sales.description':
+ 'Marketing campaigns, sales workflows, and growth automation skills',
+ 'skills.categories.marketing-sales.name': 'Marketing & Sales',
+
+ 'skills.categories.media-streaming.description':
+ 'Media playback, streaming platforms, and content delivery skills',
+ 'skills.categories.media-streaming.name': 'Media & Streaming',
+
+ 'skills.categories.moltbook.description':
+ 'Moltbook platform integrations and notebook automation skills',
+ 'skills.categories.moltbook.name': 'Moltbook',
+
+ 'skills.categories.notes-pkm.description':
+ 'Note-taking, personal knowledge management, and second-brain skills',
+ 'skills.categories.notes-pkm.name': 'Notes & PKM',
+
+ 'skills.categories.pdf-documents.description':
+ 'PDF processing, document parsing, and file management skills',
+ 'skills.categories.pdf-documents.name': 'PDF & Documents',
+
+ 'skills.categories.personal-development.description':
+ 'Personal growth, habit building, and self-improvement skills',
+ 'skills.categories.personal-development.name': 'Personal Development',
+
+ 'skills.categories.productivity-tasks.description':
+ 'Task management, workflow automation, and productivity skills',
+ 'skills.categories.productivity-tasks.name': 'Productivity & Tasks',
+
+ 'skills.categories.search-research.description':
+ 'Web search, data retrieval, and research automation skills',
+ 'skills.categories.search-research.name': 'Search & Research',
+
+ 'skills.categories.security-passwords.description':
+ 'Security auditing, password management, and privacy protection skills',
+ 'skills.categories.security-passwords.name': 'Security & Passwords',
+
+ 'skills.categories.self-hosted-automation.description':
+ 'Self-hosted services, home lab automation, and infrastructure skills',
+ 'skills.categories.self-hosted-automation.name': 'Self-Hosted & Automation',
+
+ 'skills.categories.shopping-ecommerce.description':
+ 'E-commerce integrations, shopping automation, and retail skills',
+ 'skills.categories.shopping-ecommerce.name': 'Shopping & E-commerce',
+
+ 'skills.categories.smart-home-iot.description':
+ 'Smart home automation, IoT device control, and home management skills',
+ 'skills.categories.smart-home-iot.name': 'Smart Home & IoT',
+
+ 'skills.categories.speech-transcription.description':
+ 'Speech recognition, audio transcription, and voice interface skills',
+ 'skills.categories.speech-transcription.name': 'Speech & Transcription',
+
+ 'skills.categories.transportation.description':
+ 'Transportation, logistics, routing, and mobility skills',
+ 'skills.categories.transportation.name': 'Transportation',
+
+ 'skills.categories.web-frontend-development.description':
+ 'Web development, frontend frameworks, and UI tooling skills',
+ 'skills.categories.web-frontend-development.name': 'Web & Frontend Development',
+
+ 'skills.details.nav.needHelp': 'Need Help?',
+
+ 'skills.details.nav.reportIssue': 'Report Issue',
+
+ 'skills.details.nav.viewSourceCode': 'View Source Code',
+
+ 'skills.details.overview.title': 'Overview',
+
+ 'skills.details.rating.title': 'Ratings',
+
+ 'skills.details.related.empty': 'No related Skills yet',
+
+ 'skills.details.related.listTitle': 'Related Skills',
+
+ 'skills.details.related.more': 'View More',
+
+ 'skills.details.related.title': 'Related Skills',
+
+ 'skills.details.resources.empty': 'No resources available',
+
+ 'skills.details.resources.table.name': 'Name',
+
+ 'skills.details.resources.table.size': 'Size',
+
+ 'skills.details.resources.title': 'Resources',
+
+ 'skills.details.review.title': 'How to Submit a Review',
+
+ 'skills.details.sidebar.agent.copied': 'Prompt Copied',
+
+ 'skills.details.sidebar.agent.copyPrompt': 'Copy Prompt',
+
+ 'skills.details.sidebar.agent.title': 'Send this prompt to your Agent to install this Skill',
+
+ 'skills.details.sidebar.agent.useOnLobeAI': 'Use on LobeAI',
+
+ 'skills.details.sidebar.description': 'Description',
+
+ 'skills.details.sidebar.details': 'Details',
+
+ 'skills.details.sidebar.directoryLayout': 'Directory Layout',
+
+ 'skills.details.sidebar.downloadSkill': 'Download Skill',
+
+ 'skills.details.sidebar.files': 'File Tree',
+
+ 'skills.details.sidebar.installationConfig': 'Installation',
+
+ 'skills.details.sidebar.installCommand': 'Install Command',
+
+ 'skills.details.sidebar.platform.layout.lobehub': 'Skills are managed by LobeHub automatically',
+
+ 'skills.details.sidebar.platform.layout.resourcesHint': 'other resources',
+
+ 'skills.details.sidebar.platform.steps.claude':
+ 'Run the install command in your terminal to download and configure this skill for Claude Code.',
+
+ 'skills.details.sidebar.platform.steps.cline':
+ 'Run the install command in your terminal to download and configure this skill for Cline.',
+
+ 'skills.details.sidebar.platform.steps.codex':
+ 'Run the install command in your terminal to download and configure this skill for Codex.',
+
+ 'skills.details.sidebar.platform.steps.cursor':
+ 'Run the install command in your terminal to download and configure this skill for Cursor.',
+
+ 'skills.details.sidebar.platform.steps.lobehub':
+ 'Install directly from the LobeHub marketplace with one click.',
+
+ 'skills.details.sidebar.platform.steps.vscode':
+ 'Run the install command in your terminal to download and configure this skill for VS Code.',
+
+ 'skills.details.sidebar.platform.title': 'Install on {{platform}}',
+
+ 'skills.details.sidebar.tags': 'Tags',
+
+ 'skills.details.summary.title': 'Summary',
+
+ 'skills.details.versions.empty': 'No historical versions yet',
+
+ 'skills.details.versions.table.isLatest': 'Latest',
+
+ 'skills.details.versions.table.publishAt': 'Published',
+
+ 'skills.details.versions.table.version': 'Version',
+
+ 'skills.details.versions.title': 'Version History',
+
+ 'skills.hero.guide.agent': 'I am Agent',
+
+ 'skills.hero.guide.human': 'I am Human',
+
+ 'skills.sorts.createdAt': 'Recently Published',
+
+ 'skills.sorts.installCount': 'Downloads',
+
+ 'skills.sorts.name': 'Name',
+
+ 'skills.sorts.stars': 'GitHub Stars',
+
+ 'skills.sorts.updatedAt': 'Recently Updated',
+
'tab.assistant': 'Agent',
'tab.home': 'Home',
@@ -875,6 +1113,8 @@ export default {
'tab.provider': 'Provider',
+ 'tab.skill': 'Skills',
+
'tab.user': 'User',
'user.agents': 'Agents',
diff --git a/src/routes/(main)/community/(detail)/skill/features/DetailProvider.tsx b/src/routes/(main)/community/(detail)/skill/features/DetailProvider.tsx
new file mode 100644
index 0000000000..3a39ef5bd8
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/DetailProvider.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { createContext, memo, type ReactNode, use } from 'react';
+
+import { type DiscoverSkillDetail } from '@/types/discover';
+
+export type DetailContextConfig = Partial;
+
+export const DetailContext = createContext({});
+
+export const DetailProvider = memo<{ children: ReactNode; config?: DetailContextConfig }>(
+ ({ children, config = {} }) => {
+ return {children};
+ },
+);
+
+export const useDetailContext = () => {
+ return use(DetailContext);
+};
diff --git a/src/routes/(main)/community/(detail)/skill/features/Details/Installation/index.tsx b/src/routes/(main)/community/(detail)/skill/features/Details/Installation/index.tsx
new file mode 100644
index 0000000000..56a2a71f72
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Details/Installation/index.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { memo } from 'react';
+
+import { useDetailContext } from '../../DetailProvider';
+import Platform from '../../Sidebar/Platform';
+
+const Installation = memo<{ mobile?: boolean }>(({ mobile }) => {
+ const { identifier, downloadUrl } = useDetailContext();
+
+ return (
+
+ );
+});
+
+export default Installation;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Details/Nav.tsx b/src/routes/(main)/community/(detail)/skill/features/Details/Nav.tsx
new file mode 100644
index 0000000000..cccfdf4564
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Details/Nav.tsx
@@ -0,0 +1,144 @@
+'use client';
+
+import { Flexbox, Icon, Tabs, Tag } from '@lobehub/ui';
+import { SkillsIcon } from '@lobehub/ui/icons';
+import { createStaticStyles } from 'antd-style';
+import { BookOpenIcon, DownloadIcon, FileTextIcon, HistoryIcon, ListIcon } from 'lucide-react';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import urlJoin from 'url-join';
+
+import { useDetailContext } from '../DetailProvider';
+import { SkillNavKey } from '../types';
+
+export const styles = createStaticStyles(({ css, cssVar }) => ({
+ link: css`
+ color: ${cssVar.colorTextDescription};
+
+ &:hover {
+ color: ${cssVar.colorInfo};
+ }
+ `,
+ nav: css`
+ border-block-end: 1px solid ${cssVar.colorBorder};
+ `,
+}));
+
+const Nav = memo<{
+ activeTab?: SkillNavKey;
+ mobile?: boolean;
+ setActiveTab?: (_tab: SkillNavKey) => void;
+}>(({ mobile, setActiveTab, activeTab = SkillNavKey.Overview }) => {
+ const { t } = useTranslation('discover');
+ const { versions, repository, homepage, github, resources } = useDetailContext();
+
+ const versionCount = versions?.length || 0;
+ const resourcesCount = Object.keys(resources || {}).length;
+ const source = homepage || repository;
+ const issueTarget = github?.url || repository;
+
+ const nav = (
+ ,
+ key: SkillNavKey.Overview,
+ label: t('skills.details.overview.title'),
+ },
+ {
+ icon: ,
+ key: SkillNavKey.Installation,
+ label: t('skills.details.sidebar.installationConfig'),
+ },
+ {
+ icon: ,
+ key: SkillNavKey.Skill,
+ label: 'SKILL.md',
+ },
+ {
+ icon: ,
+ key: SkillNavKey.Resources,
+ label:
+ resourcesCount > 1 ? (
+
+ {t('skills.details.resources.title')}
+ {resourcesCount}
+
+ ) : (
+ t('skills.details.resources.title')
+ ),
+ },
+ {
+ icon: ,
+ key: SkillNavKey.Related,
+ label: t('skills.details.related.title'),
+ },
+ {
+ icon: ,
+ key: SkillNavKey.Version,
+ label:
+ versionCount > 1 ? (
+
+ {t('skills.details.versions.title')}
+ {versionCount}
+
+ ) : (
+ t('skills.details.versions.title')
+ ),
+ },
+ ]}
+ onChange={(key) => setActiveTab?.(key as SkillNavKey)}
+ />
+ );
+
+ return mobile ? (
+ nav
+ ) : (
+
+ {nav}
+
+
+ {t('skills.details.nav.needHelp')}
+
+ {source && (
+
+ {t('skills.details.nav.viewSourceCode')}
+
+ )}
+ {issueTarget && (
+
+ {t('skills.details.nav.reportIssue')}
+
+ )}
+
+
+ );
+});
+
+export default Nav;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Details/Overview/index.tsx b/src/routes/(main)/community/(detail)/skill/features/Details/Overview/index.tsx
new file mode 100644
index 0000000000..71c8f24e8b
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Details/Overview/index.tsx
@@ -0,0 +1,68 @@
+'use client';
+
+import { Collapse, Flexbox, Markdown, ScrollShadow, Tag } from '@lobehub/ui';
+import qs from 'query-string';
+import { memo, type PropsWithChildren } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import List from '../../../../../(list)/skill/features/List';
+import Title from '../../../../../components/Title';
+import { useDetailContext } from '../../DetailProvider';
+
+const Overview = memo(({ children }) => {
+ const { t } = useTranslation('discover');
+ const { tags = [], description, overview, category, related } = useDetailContext();
+
+ return (
+
+
+ {t('skills.details.summary.title')}
+ {overview?.summary || description || ''}
+
+
+
+ {children}
+
+
+ ),
+ key: 'summary',
+ label: 'SKILL.md',
+ },
+ ]}
+ padding={{
+ body: 0,
+ }}
+ />
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+ {tag}
+ ))}
+
+ )}
+ {related && related.length > 0 && (
+
+
+ {t('skills.details.related.listTitle')}
+
+
+
+ )}
+
+ );
+});
+
+export default Overview;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Details/Related/index.tsx b/src/routes/(main)/community/(detail)/skill/features/Details/Related/index.tsx
new file mode 100644
index 0000000000..034009bb99
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Details/Related/index.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import { Flexbox } from '@lobehub/ui';
+import qs from 'query-string';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import List from '../../../../../(list)/skill/features/List';
+import Title from '../../../../../components/Title';
+import { useDetailContext } from '../../DetailProvider';
+
+const Related = memo(() => {
+ const { t } = useTranslation('discover');
+ const { related, category } = useDetailContext();
+
+ return (
+
+
+ {t('skills.details.related.listTitle')}
+
+
+
+ );
+});
+
+export default Related;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Details/Resources/index.tsx b/src/routes/(main)/community/(detail)/skill/features/Details/Resources/index.tsx
new file mode 100644
index 0000000000..789a58f390
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Details/Resources/index.tsx
@@ -0,0 +1,123 @@
+'use client';
+
+import { Block, Empty, Flexbox, MaterialFileTypeIcon, Text } from '@lobehub/ui';
+import { memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import urlJoin from 'url-join';
+
+import InlineTable from '@/components/InlineTable';
+
+import { useDetailContext } from '../../DetailProvider';
+
+type ResourceMeta = {
+ fileHash?: string;
+ size?: number;
+};
+
+type ResourceItem = ResourceMeta & {
+ name: string;
+};
+
+const Resources = memo(() => {
+ const { t } = useTranslation('discover');
+ const { resources, github, homepage, repository } = useDetailContext();
+ const repoUrl = homepage || github?.url || repository;
+
+ const dataSource = useMemo(() => {
+ return Object.entries((resources || {}) as Record)
+ .map(([name, meta]) => ({
+ fileHash: meta?.fileHash,
+ name,
+ size: meta?.size,
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ }, [resources]);
+
+ const getResourceLink = (filePath: string) => {
+ if (!repoUrl) return;
+ const encodedPath = filePath
+ .split('/')
+ .filter(Boolean)
+ .map((segment) => encodeURIComponent(segment))
+ .join('/');
+ return urlJoin(repoUrl, encodedPath);
+ };
+
+ if (dataSource.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {
+ const link = getResourceLink(text);
+ const node = (
+
+
+
+ {text}
+
+
+ );
+ if (!link) {
+ return node;
+ }
+
+ return (
+
+ {node}
+
+ );
+ },
+ title: t('skills.details.resources.table.name'),
+ },
+ {
+ align: 'end',
+ dataIndex: 'size',
+ key: 'size',
+ render: (value) => {
+ let size;
+
+ if (typeof value !== 'number') {
+ size = '--';
+ } else if (value < 1024) {
+ size = value + 'B';
+ } else if (value < 1024 * 1024) {
+ size = (value / 1024).toFixed(2) + 'KB';
+ } else {
+ size = (value / (1024 * 1024)).toFixed(2) + 'MB';
+ }
+
+ return (
+
+ {size}
+
+ );
+ },
+ title: t('skills.details.resources.table.size'),
+ },
+ ]}
+ />
+
+ );
+});
+
+export default Resources;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Details/Versions/index.tsx b/src/routes/(main)/community/(detail)/skill/features/Details/Versions/index.tsx
new file mode 100644
index 0000000000..c42a48e411
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Details/Versions/index.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import { Block, Flexbox, Tag } from '@lobehub/ui';
+import qs from 'query-string';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Link, useLocation } from 'react-router-dom';
+
+import InlineTable from '@/components/InlineTable';
+import PublishedTime from '@/components/PublishedTime';
+
+import Title from '../../../../../components/Title';
+import { useDetailContext } from '../../DetailProvider';
+
+const Versions = memo(() => {
+ const { t } = useTranslation('discover');
+ const { versions = [] } = useDetailContext();
+ const { pathname } = useLocation();
+
+ return (
+
+ {t('skills.details.versions.title')}
+
+ (
+
+
+ {record.version}
+ {record.isLatest && (
+ {t('skills.details.versions.table.isLatest')}
+ )}
+
+
+ ),
+ title: t('skills.details.versions.table.version'),
+ },
+ {
+ align: 'end',
+ dataIndex: 'createdAt',
+ render: (_, record) => ,
+ title: t('skills.details.versions.table.publishAt'),
+ },
+ ]}
+ dataSource={versions}
+ rowKey={'version'}
+ size={'middle'}
+ />
+
+
+ );
+});
+
+export default Versions;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Details/index.tsx b/src/routes/(main)/community/(detail)/skill/features/Details/index.tsx
new file mode 100644
index 0000000000..47fa1f43cd
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Details/index.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import { Flexbox, Markdown } from '@lobehub/ui';
+import { memo, useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+
+import { useDetailContext } from '../DetailProvider';
+import Sidebar from '../Sidebar';
+import { SkillNavKey } from '../types';
+import Installation from './Installation';
+import Nav from './Nav';
+import Overview from './Overview';
+import Related from './Related';
+import Resources from './Resources';
+import Versions from './Versions';
+
+const Details = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const activeTabParam = searchParams.get('activeTab') as SkillNavKey | null;
+ const [activeTab, setActiveTab] = useState(activeTabParam || SkillNavKey.Overview);
+ const { content } = useDetailContext();
+
+ const handleSetActiveTab = (tab: SkillNavKey) => {
+ setActiveTab(tab);
+ if (tab === SkillNavKey.Overview) {
+ searchParams.delete('activeTab');
+ } else {
+ searchParams.set('activeTab', tab);
+ }
+ setSearchParams(searchParams, { replace: true });
+ };
+
+ const skillContent = {content ?? ''};
+
+ return (
+
+
+
+
+ {activeTab === SkillNavKey.Overview && {skillContent}}
+ {activeTab === SkillNavKey.Installation && }
+ {activeTab === SkillNavKey.Skill && skillContent}
+ {activeTab === SkillNavKey.Resources && }
+ {activeTab === SkillNavKey.Related && }
+ {activeTab === SkillNavKey.Version && }
+
+
+
+
+ );
+});
+
+export default Details;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Header.tsx b/src/routes/(main)/community/(detail)/skill/features/Header.tsx
new file mode 100644
index 0000000000..ec32e929f0
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Header.tsx
@@ -0,0 +1,226 @@
+'use client';
+
+import { Github } from '@lobehub/icons';
+import { ActionIcon, Avatar, Button, Flexbox, Icon, stopPropagation, Text } from '@lobehub/ui';
+import { createStaticStyles, cssVar } from 'antd-style';
+import {
+ DotIcon,
+ DownloadIcon,
+ FileTextIcon,
+ MessageSquare,
+ ScaleIcon,
+ StarIcon,
+} from 'lucide-react';
+import qs from 'query-string';
+import { memo } from 'react';
+import { Link } from 'react-router-dom';
+
+import PublishedTime from '@/components/PublishedTime';
+import { useSkillCategoryItem } from '@/hooks/useSkillCategory';
+
+import { useDetailContext } from './DetailProvider';
+
+export const styles = createStaticStyles(({ css, cssVar }) => {
+ return {
+ desc: css`
+ color: ${cssVar.colorTextSecondary};
+ `,
+ extraTag: css`
+ padding-block: 4px;
+ padding-inline: 10px 12px;
+ border-radius: 16px;
+
+ color: ${cssVar.colorTextSecondary};
+
+ background: ${cssVar.colorFillTertiary};
+ `,
+ extraTagActive: css`
+ &:hover {
+ color: ${cssVar.colorText};
+ }
+ `,
+ time: css`
+ font-size: 12px;
+ color: ${cssVar.colorTextDescription};
+ `,
+ version: css`
+ font-family: ${cssVar.fontFamilyCode};
+ font-size: 13px;
+ `,
+ };
+});
+
+const formatCompactNumber = (num?: number): string => {
+ if (!num) return '0';
+ if (num < 1000) return num.toString();
+ if (num < 1000000) return `${(num / 1000).toFixed(1)}k`;
+ return `${(num / 1000000).toFixed(1)}M`;
+};
+
+const Header = memo<{ mobile?: boolean }>(({ mobile }) => {
+ const {
+ name,
+ author,
+ version,
+ identifier,
+ updatedAt,
+ createdAt,
+ ratingAverage,
+ category,
+ installCount,
+ github,
+ homepage,
+ resources,
+ comments,
+ license,
+ icon,
+ } = useDetailContext();
+
+ const cate = useSkillCategoryItem(category);
+ const resourcesCount = (Object.values(resources || {})?.length || 0) + 1;
+
+ const scores = (
+
+
+
+ {resourcesCount}
+
+
+ );
+
+ const cateButton = cate ? (
+
+ } size={'middle'} variant={'outlined'}>
+ {cate.label}
+
+
+ ) : null;
+
+ return (
+
+
+
+
+
+
+
+ {name}
+
+ {!mobile && scores}
+
+
+ {homepage && (
+
+
+
+ )}
+
+
+
+ {Boolean(ratingAverage) ? (
+
+
+ {ratingAverage?.toFixed(1)}
+
+ ) : (
+ {version}
+ )}
+
+ {author?.url ? (
+
+ {author.name}
+
+ ) : (
+ {author?.name}
+ )}
+
+
+
+
+
+
+ {mobile && scores}
+ {!mobile && cateButton}
+
+ {Boolean(license?.name) && (
+
+
+ {license?.name}
+
+ )}
+ {Boolean(installCount) && (
+
+
+ {formatCompactNumber(installCount)}
+
+ )}
+ {Boolean(github?.stars) && (
+
+
+ {formatCompactNumber(github?.stars)}
+
+ )}
+ {Boolean(comments?.totalCount) && (
+
+
+ {formatCompactNumber(comments?.totalCount)}
+
+ )}
+
+
+
+ );
+});
+
+export default Header;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Sidebar/FileTree.tsx b/src/routes/(main)/community/(detail)/skill/features/Sidebar/FileTree.tsx
new file mode 100644
index 0000000000..0df7f67b47
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Sidebar/FileTree.tsx
@@ -0,0 +1,162 @@
+'use client';
+
+import { Flexbox, Icon, MaterialFileTypeIcon, Text } from '@lobehub/ui';
+import { type GetProps, Tree } from 'antd';
+import type { DataNode } from 'antd/es/tree';
+import { createStaticStyles } from 'antd-style';
+import { ChevronDown } from 'lucide-react';
+import qs from 'query-string';
+import { type Key, memo, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useLocation } from 'react-router-dom';
+
+import Title from '../../../../components/Title';
+import { useDetailContext } from '../DetailProvider';
+
+type DirectoryTreeProps = GetProps;
+
+interface TreeNode {
+ children: Map;
+ key: string;
+ name: string;
+}
+
+const createNode = (name: string, key: string): TreeNode => ({
+ children: new Map(),
+ key: key || '/',
+ name,
+});
+
+export const styles = createStaticStyles(({ css, cssVar }) => ({
+ tree: css`
+ .ant-tree-node-content-wrapper {
+ overflow: hidden;
+ display: flex;
+ gap: 4px;
+ align-items: center;
+
+ color: ${cssVar.colorTextSecondary};
+ }
+
+ .ant-tree-title {
+ overflow: hidden;
+ display: block;
+ line-height: 1.2;
+ }
+
+ .ant-tree-switcher {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ margin-inline-end: 0;
+
+ color: ${cssVar.colorTextDescription};
+ }
+ `,
+}));
+
+const sortNodes = (nodes: TreeNode[]) =>
+ [...nodes].sort((a, b) => {
+ const aIsFolder = a.children.size > 0;
+ const bIsFolder = b.children.size > 0;
+ if (aIsFolder !== bIsFolder) return aIsFolder ? -1 : 1;
+ return a.name.localeCompare(b.name);
+ });
+
+const toTreeData = (node: TreeNode): DataNode => {
+ const children = sortNodes([...node.children.values()]).map(toTreeData);
+ const isFolder = children.length > 0;
+ return {
+ children: isFolder ? children : undefined,
+ icon: (
+
+ ),
+ key: node.key,
+ title: {node.name},
+ };
+};
+
+const FileTree = memo(() => {
+ const { t } = useTranslation('discover');
+ const { resources = {} } = useDetailContext();
+ const { pathname, search } = useLocation();
+ const [expand, setExpand] = useState([]);
+ const detailLink = useMemo(
+ () =>
+ qs.stringifyUrl({
+ query: {
+ ...Object.fromEntries(new URLSearchParams(search).entries()),
+ activeTab: 'resources',
+ },
+ url: pathname,
+ }),
+ [pathname, search],
+ );
+
+ const treeData = useMemo(() => {
+ const root = createNode('root', '/');
+ const entries = Object.entries((resources || {}) as Record);
+
+ // 至少包含主文件,避免出现空树
+ if (!entries.some(([path]) => path.toLowerCase() === 'skill.md')) {
+ entries.push(['SKILL.md', {}]);
+ }
+
+ for (const [filePath] of entries) {
+ const parts = filePath.split('/').filter(Boolean);
+ let current = root;
+ let currentPath = '';
+
+ for (const part of parts) {
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
+ if (!current.children.has(part)) {
+ current.children.set(part, createNode(part, currentPath));
+ }
+ current = current.children.get(part)!;
+ }
+ }
+
+ return sortNodes([...root.children.values()]).map(toTreeData);
+ }, [resources]);
+
+ const onSelect: DirectoryTreeProps['onSelect'] = (keys) => {
+ setExpand((prevState) => {
+ if (prevState.includes(keys[0])) {
+ return prevState.filter((key) => key !== keys[0]);
+ } else {
+ return [...prevState, keys[0]];
+ }
+ });
+ };
+
+ const onExpand: DirectoryTreeProps['onExpand'] = (keys) => {
+ setExpand(keys);
+ };
+
+ return (
+
+
+ {t('skills.details.sidebar.files')}
+
+ }
+ treeData={treeData}
+ onExpand={onExpand}
+ onSelect={onSelect}
+ />
+
+ );
+});
+
+export default FileTree;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Sidebar/InstallationConfig.tsx b/src/routes/(main)/community/(detail)/skill/features/Sidebar/InstallationConfig.tsx
new file mode 100644
index 0000000000..ebdfb5245f
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Sidebar/InstallationConfig.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import { Flexbox } from '@lobehub/ui';
+import qs from 'query-string';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useLocation } from 'react-router-dom';
+
+import Title from '../../../../components/Title';
+import { useDetailContext } from '../DetailProvider';
+import { SkillNavKey } from '../types';
+import Platform from './Platform';
+
+const InstallationConfig = memo(() => {
+ const { t } = useTranslation('discover');
+ const { pathname } = useLocation();
+ const installLink = qs.stringifyUrl({
+ query: {
+ activeTab: SkillNavKey.Installation,
+ },
+ url: pathname,
+ });
+ const { identifier, downloadUrl } = useDetailContext();
+
+ return (
+
+
+ {t('skills.details.sidebar.installationConfig')}
+
+
+
+ );
+});
+
+export default InstallationConfig;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Sidebar/Platform.tsx b/src/routes/(main)/community/(detail)/skill/features/Sidebar/Platform.tsx
new file mode 100644
index 0000000000..497c4752c2
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Sidebar/Platform.tsx
@@ -0,0 +1,344 @@
+'use client';
+
+import { DEFAULT_INBOX_AVATAR, SESSION_CHAT_URL } from '@lobechat/const';
+import { Claude, Cline, Cursor, OpenAI } from '@lobehub/icons';
+import {
+ Avatar,
+ Block,
+ Button,
+ Flexbox,
+ Highlighter,
+ Icon,
+ Markdown,
+ Segmented,
+ Select,
+ Text,
+} from '@lobehub/ui';
+import { Divider } from 'antd';
+import { createStaticStyles, cx } from 'antd-style';
+import { BotIcon, UserRoundIcon } from 'lucide-react';
+import { memo, useCallback, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+
+import { useAgentStore } from '@/store/agent';
+import { builtinAgentSelectors } from '@/store/agent/selectors';
+import { useChatStore } from '@/store/chat';
+
+import Title from '../../../../components/Title';
+import VsCodeIcon from './VsCodeIcon';
+
+type GuideMode = 'agent' | 'human';
+
+enum PlatformType {
+ Claude = 'claude',
+ Cline = 'cline',
+ Codex = 'codex',
+ Cursor = 'cursor',
+ LobeHub = 'lobehub',
+ VsCode = 'vscode',
+}
+
+export const styles = createStaticStyles(({ css }) => ({
+ lite: css`
+ pre {
+ padding: 12px !important;
+ }
+ `,
+}));
+
+interface PlatformProps {
+ downloadUrl?: string;
+ expandCodeByDefault?: boolean;
+ identifier?: string;
+ lite?: boolean;
+ mobile?: boolean;
+}
+
+const genInstallCommand = (identifier?: string, platform?: PlatformType) => {
+ const id = identifier || '';
+
+ const agentMap: Record = {
+ [PlatformType.Claude]: 'claude-code',
+ [PlatformType.Cline]: 'cline',
+ [PlatformType.Cursor]: 'cursor',
+ [PlatformType.LobeHub]: 'lobehub',
+ [PlatformType.Codex]: 'codex',
+ [PlatformType.VsCode]: 'vscode',
+ };
+
+ switch (platform) {
+ case PlatformType.Cursor:
+ case PlatformType.Claude:
+ case PlatformType.Cline:
+ case PlatformType.VsCode: {
+ return `npx -y @lobehub/market-cli skills install ${id} --agent ${agentMap[platform]}`;
+ }
+ case PlatformType.Codex: {
+ return `npx -y @lobehub/market-cli skills install ${id} --agent ${agentMap[platform]}`;
+ }
+ default: {
+ return `# Recommended for LobeHub users:
+# Open the marketplace page and install with one click:
+# https://lobechat.com/community/skills/${id}`;
+ }
+ }
+};
+
+const genLayout = (
+ identifier: string | undefined,
+ platform: PlatformType,
+ i18nText: {
+ lobehub: string;
+ resourcesHint: string;
+ },
+) => {
+ const id = identifier || '';
+ const basePathMap: Record = {
+ [PlatformType.Claude]: `~/.claude/skills/${id}`,
+ [PlatformType.Cline]: `~/.cline/skills/${id}`,
+ [PlatformType.Cursor]: `~/.cursor/skills/${id}`,
+ [PlatformType.LobeHub]: ``,
+ [PlatformType.Codex]: `~/.agents/skills/${id}`,
+ [PlatformType.VsCode]: `./.vscode/skills/${id}`,
+ };
+ const basePath = basePathMap[platform];
+
+ if (platform === PlatformType.LobeHub) {
+ return i18nText.lobehub;
+ }
+
+ return `${basePath}
+├── SKILL.md
+└── ... (${i18nText.resourcesHint})`;
+};
+
+const Platform = memo(
+ ({ lite, identifier, mobile, expandCodeByDefault, downloadUrl }) => {
+ const { t } = useTranslation('discover');
+ const navigate = useNavigate();
+ const inboxAgentId = useAgentStore(builtinAgentSelectors.inboxAgentId);
+ const sendMessage = useChatStore((s) => s.sendMessage);
+ const [active, setActive] = useState(PlatformType.Claude);
+ const [mode, setMode] = useState('agent');
+
+ const options = [
+ {
+ icon: ,
+ label: 'Claude Code',
+ value: PlatformType.Claude,
+ },
+ {
+ icon: ,
+ label: 'Codex',
+ value: PlatformType.Codex,
+ },
+ {
+ icon: ,
+ label: 'Cursor',
+ value: PlatformType.Cursor,
+ },
+ {
+ icon: ,
+ label: 'VsCode',
+ value: PlatformType.VsCode,
+ },
+ {
+ icon: ,
+ label: 'Cline',
+ value: PlatformType.Cline,
+ },
+ ];
+
+ const { platform, steps } = useMemo(() => {
+ switch (active) {
+ case PlatformType.LobeHub: {
+ return {
+ platform: 'LobeHub',
+ steps: t('skills.details.sidebar.platform.steps.lobehub'),
+ };
+ }
+ case PlatformType.Claude: {
+ return {
+ platform: 'Claude',
+ steps: t('skills.details.sidebar.platform.steps.claude'),
+ };
+ }
+ case PlatformType.Codex: {
+ return {
+ platform: 'Codex',
+ steps: t('skills.details.sidebar.platform.steps.codex'),
+ };
+ }
+ case PlatformType.Cursor: {
+ return {
+ platform: 'Cursor',
+ steps: t('skills.details.sidebar.platform.steps.cursor'),
+ };
+ }
+ case PlatformType.Cline: {
+ return {
+ platform: 'Cline',
+ steps: t('skills.details.sidebar.platform.steps.cline'),
+ };
+ }
+ case PlatformType.VsCode: {
+ return {
+ platform: 'VS Code',
+ steps: t('skills.details.sidebar.platform.steps.vscode'),
+ };
+ }
+ }
+ }, [active, t]);
+
+ const command = genInstallCommand(identifier, active);
+
+ const agentPrompt = `Curl https://lobehub.com/skills/${identifier}/skill.md, then follow the instructions to set up LobeHub Skills Marketplace and install the skill. Once installed, read the SKILL.md file in the installed directory and follow its instructions to complete the task.`;
+
+ const handleUseOnLobeAI = useCallback(() => {
+ if (!inboxAgentId) return;
+
+ // Send message to LobeAI
+ sendMessage({
+ context: { agentId: inboxAgentId },
+ message: agentPrompt,
+ });
+
+ // Navigate to LobeAI chat session
+ navigate(SESSION_CHAT_URL(inboxAgentId, mobile));
+ }, [agentPrompt, inboxAgentId, mobile, navigate, sendMessage]);
+
+ return (
+
+ ,
+ label: t('skills.hero.guide.agent'),
+ value: 'agent',
+ },
+ {
+ icon: ,
+ label: t('skills.hero.guide.human'),
+ value: 'human',
+ },
+ ]}
+ onChange={(value) => setMode(value as GuideMode)}
+ />
+
+ {mode === 'agent' ? (
+
+ {mobile || lite ? (
+
+ {t('skills.details.sidebar.agent.title')}
+
+ ) : (
+ {t('skills.details.sidebar.agent.title')}
+ )}
+
+ {agentPrompt}
+
+
+ }
+ size={'large'}
+ type={'primary'}
+ onClick={handleUseOnLobeAI}
+ >
+ {t('skills.details.sidebar.agent.useOnLobeAI')}
+
+
+
+ ) : (
+ <>
+ {mobile || lite ? (
+
+ );
+ },
+);
+
+export default Platform;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Sidebar/VsCodeIcon.tsx b/src/routes/(main)/community/(detail)/skill/features/Sidebar/VsCodeIcon.tsx
new file mode 100644
index 0000000000..0acfa178ff
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Sidebar/VsCodeIcon.tsx
@@ -0,0 +1,99 @@
+'use client';
+
+import { type IconType, useFillIds } from '@lobehub/icons';
+import { memo } from 'react';
+
+const VsCodeIcon: IconType = memo(({ size = '1em', style, ...rest }) => {
+ const [a, b, c, d] = useFillIds('vscode', 4);
+ return (
+
+ );
+});
+
+export default VsCodeIcon;
diff --git a/src/routes/(main)/community/(detail)/skill/features/Sidebar/index.tsx b/src/routes/(main)/community/(detail)/skill/features/Sidebar/index.tsx
new file mode 100644
index 0000000000..4e043a00bf
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/Sidebar/index.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+import { Flexbox, ScrollShadow } from '@lobehub/ui';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import urlJoin from 'url-join';
+
+import ShareButton from '../../../features/ShareButton';
+import { useDetailContext } from '../DetailProvider';
+import { SkillNavKey } from '../types';
+import FileTree from './FileTree';
+import InstallationConfig from './InstallationConfig';
+
+const Sidebar = memo<{ activeTab?: SkillNavKey; mobile?: boolean }>(
+ ({ mobile, activeTab = SkillNavKey.Overview }) => {
+ const { description, tags, name, identifier, icon } = useDetailContext();
+ const { t } = useTranslation('common');
+ const showInstallationConfig = activeTab !== SkillNavKey.Installation;
+ const showFileTree = activeTab !== SkillNavKey.Resources;
+
+ const shareButton = (
+
+ {t('share')}
+
+ );
+
+ if (mobile) {
+ if (activeTab !== SkillNavKey.Overview && activeTab !== SkillNavKey.Resources) return null;
+
+ return (
+
+ {showInstallationConfig && }
+ {shareButton}
+ {showFileTree && }
+
+ );
+ }
+
+ return (
+
+ {showInstallationConfig && }
+ {shareButton}
+ {showFileTree && }
+
+ );
+ },
+);
+
+export default Sidebar;
diff --git a/src/routes/(main)/community/(detail)/skill/features/types.ts b/src/routes/(main)/community/(detail)/skill/features/types.ts
new file mode 100644
index 0000000000..26f720cd7e
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/features/types.ts
@@ -0,0 +1,8 @@
+export enum SkillNavKey {
+ Installation = 'installation',
+ Overview = 'overview',
+ Related = 'related',
+ Resources = 'resources',
+ Skill = 'skill',
+ Version = 'version',
+}
diff --git a/src/routes/(main)/community/(detail)/skill/index.tsx b/src/routes/(main)/community/(detail)/skill/index.tsx
new file mode 100644
index 0000000000..eb2c032b6c
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/index.tsx
@@ -0,0 +1,48 @@
+'use client';
+
+import { Flexbox } from '@lobehub/ui';
+import { memo } from 'react';
+import { useParams } from 'react-router-dom';
+
+import { useQuery } from '@/hooks/useQuery';
+import { useDiscoverStore } from '@/store/discover';
+
+import NotFound from '../components/NotFound';
+import { TocProvider } from '../features/Toc/useToc';
+import { DetailProvider } from './features/DetailProvider';
+import Details from './features/Details';
+import Header from './features/Header';
+import Loading from './loading';
+
+interface SkillDetailPageProps {
+ mobile?: boolean;
+}
+
+const SkillDetailPage = memo(({ mobile }) => {
+ const params = useParams<{ slug: string }>();
+ const identifier = params.slug ?? '';
+
+ const { version } = useQuery() as { version?: string };
+ const useSkillDetail = useDiscoverStore((s) => s.useFetchSkillDetail);
+ const { data, isLoading } = useSkillDetail({ identifier, version });
+
+ if (isLoading) return ;
+ if (!data) return ;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+});
+
+export const MobileSkillPage = memo<{ mobile?: boolean }>(() => {
+ return ;
+});
+
+export default SkillDetailPage;
diff --git a/src/routes/(main)/community/(detail)/skill/loading.tsx b/src/routes/(main)/community/(detail)/skill/loading.tsx
new file mode 100644
index 0000000000..3da3304eca
--- /dev/null
+++ b/src/routes/(main)/community/(detail)/skill/loading.tsx
@@ -0,0 +1 @@
+export { DetailsLoading as default } from '../../components/ListLoading';
diff --git a/src/routes/(main)/community/(list)/features/SortButton/index.tsx b/src/routes/(main)/community/(list)/features/SortButton/index.tsx
index e68d7ace3d..14f3cfd4da 100644
--- a/src/routes/(main)/community/(list)/features/SortButton/index.tsx
+++ b/src/routes/(main)/community/(list)/features/SortButton/index.tsx
@@ -13,6 +13,7 @@ import {
ModelSorts,
PluginSorts,
ProviderSorts,
+ SkillSorts,
} from '@/types/discover';
const SortButton = memo(() => {
@@ -139,6 +140,30 @@ const SortButton = memo(() => {
},
];
}
+ case DiscoverTab.Skills: {
+ return [
+ {
+ key: SkillSorts.InstallCount,
+ label: t('skills.sorts.installCount'),
+ },
+ {
+ key: SkillSorts.UpdatedAt,
+ label: t('skills.sorts.updatedAt'),
+ },
+ {
+ key: SkillSorts.CreatedAt,
+ label: t('skills.sorts.createdAt'),
+ },
+ {
+ key: SkillSorts.Stars,
+ label: t('skills.sorts.stars'),
+ },
+ {
+ key: SkillSorts.Name,
+ label: t('skills.sorts.name'),
+ },
+ ];
+ }
default: {
return [];
}
diff --git a/src/routes/(main)/community/(list)/skill/_layout/index.tsx b/src/routes/(main)/community/(list)/skill/_layout/index.tsx
new file mode 100644
index 0000000000..f38cc524e3
--- /dev/null
+++ b/src/routes/(main)/community/(list)/skill/_layout/index.tsx
@@ -0,0 +1,21 @@
+import { Flexbox } from '@lobehub/ui';
+import { Outlet } from 'react-router-dom';
+
+import CategoryContainer from '../../../components/CategoryContainer';
+import Category from '../features/Category';
+import { styles } from './style';
+
+const Layout = () => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Layout;
diff --git a/src/routes/(main)/community/(list)/skill/_layout/style.ts b/src/routes/(main)/community/(list)/skill/_layout/style.ts
new file mode 100644
index 0000000000..68b70b458a
--- /dev/null
+++ b/src/routes/(main)/community/(list)/skill/_layout/style.ts
@@ -0,0 +1,7 @@
+import { createStaticStyles } from 'antd-style';
+
+export const styles = createStaticStyles(({ css }) => ({
+ mainContainer: css`
+ position: relative;
+ `,
+}));
diff --git a/src/routes/(main)/community/(list)/skill/features/Category/index.tsx b/src/routes/(main)/community/(list)/skill/features/Category/index.tsx
new file mode 100644
index 0000000000..eec417a2c6
--- /dev/null
+++ b/src/routes/(main)/community/(list)/skill/features/Category/index.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import { Icon, Tag } from '@lobehub/ui';
+import qs from 'query-string';
+import { memo, useMemo } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+
+import { withSuspense } from '@/components/withSuspense';
+import { useQuery } from '@/hooks/useQuery';
+import { useSkillCategory } from '@/hooks/useSkillCategory';
+import { SCROLL_PARENT_ID } from '@/routes/(main)/community/features/const';
+import { useDiscoverStore } from '@/store/discover';
+import { SkillCategory, SkillSorts } from '@/types/discover';
+
+import CategoryMenu from '../../../../components/CategoryMenu';
+
+const Category = memo(() => {
+ const useSkillCategories = useDiscoverStore((s) => s.useSkillCategories);
+ const { category = SkillCategory.All, q } = useQuery() as {
+ category?: SkillCategory;
+ q?: string;
+ };
+ const { data: items = [] } = useSkillCategories({ q });
+ const navigate = useNavigate();
+ const cates = useSkillCategory();
+
+ const genUrl = (key: SkillCategory) =>
+ qs.stringifyUrl(
+ {
+ query: {
+ category: key === SkillCategory.All ? null : key,
+ q,
+ sort: key === SkillCategory.All ? SkillSorts.InstallCount : null,
+ },
+ url: '/community/skill',
+ },
+ { skipNull: true },
+ );
+
+ const handleClick = (key: SkillCategory) => {
+ navigate(genUrl(key));
+ const scrollableElement = document?.querySelector(`#${SCROLL_PARENT_ID}`);
+ if (!scrollableElement) return;
+ scrollableElement.scrollTo({ behavior: 'smooth', top: 0 });
+ };
+ const total = useMemo(() => items.reduce((acc, item) => acc + (item.count || 0), 0), [items]);
+
+ return (
+ {
+ const itemData = items.find((i) => i.category === item.key);
+ return {
+ extra:
+ item.key === 'all'
+ ? total > 0 && (
+
+ {total}
+
+ )
+ : itemData && (
+
+ {itemData.count}
+
+ ),
+ ...item,
+ icon: ,
+ label: {item.label},
+ };
+ })}
+ onClick={(v) => handleClick(v.key as SkillCategory)}
+ />
+ );
+});
+
+export default withSuspense(Category);
diff --git a/src/routes/(main)/community/(list)/skill/features/List/Item.tsx b/src/routes/(main)/community/(list)/skill/features/List/Item.tsx
new file mode 100644
index 0000000000..a78452f89b
--- /dev/null
+++ b/src/routes/(main)/community/(list)/skill/features/List/Item.tsx
@@ -0,0 +1,227 @@
+'use client';
+
+import { Github } from '@lobehub/icons';
+import { ActionIcon, Avatar, Block, Flexbox, Icon, stopPropagation, Tag, Text } from '@lobehub/ui';
+import { Spotlight } from '@lobehub/ui/awesome';
+import { createStaticStyles, cssVar } from 'antd-style';
+import { ClockIcon, FileTextIcon, StarIcon } from 'lucide-react';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Link, useNavigate } from 'react-router-dom';
+import urlJoin from 'url-join';
+
+import PublishedTime from '@/components/PublishedTime';
+import { discoverService } from '@/services/discover';
+import { type DiscoverSkillItem } from '@/types/discover';
+
+import MetaInfo from './MetaInfo';
+
+const styles = createStaticStyles(({ css, cssVar }) => {
+ return {
+ author: css`
+ color: ${cssVar.colorTextDescription};
+ `,
+ desc: css`
+ flex: 1;
+ margin: 0 !important;
+ color: ${cssVar.colorTextSecondary};
+ `,
+ footer: css`
+ margin-block-start: 16px;
+ border-block-start: 1px dashed ${cssVar.colorBorder};
+ background: ${cssVar.colorBgContainer};
+ `,
+ secondaryDesc: css`
+ font-size: 12px;
+ color: ${cssVar.colorTextDescription};
+ `,
+ title: css`
+ margin: 0 !important;
+ font-size: 16px !important;
+ font-weight: 500 !important;
+
+ &:hover {
+ color: ${cssVar.colorLink};
+ }
+ `,
+ };
+});
+
+const SkillItem = memo(
+ ({
+ name,
+ icon,
+ author,
+ description,
+ identifier,
+ category,
+ isFeatured,
+ updatedAt,
+ installCount,
+ github,
+ homepage,
+ ratingAvg,
+ commentCount,
+ resourcesCount = 0,
+ }) => {
+ const { t } = useTranslation('discover');
+ const navigate = useNavigate();
+ const link = urlJoin('/community/skill', identifier);
+
+ const handleClick = useCallback(() => {
+ discoverService
+ .reportSkillEvent({
+ event: 'click',
+ identifier,
+ source: location.pathname,
+ })
+ .catch(() => {});
+
+ navigate(link);
+ }, [identifier, link, navigate]);
+
+ return (
+
+ {isFeatured && }
+
+
+
+
+
+
+
+ {name}
+
+
+
+
+ {Boolean(ratingAvg) && (
+
+
+ {ratingAvg?.toFixed(1)}
+
+ )}
+ {author && {author}
}
+
+
+
+
+ {github?.url && (
+
+
+
+ )}
+
+
+
+
+ {description}
+
+
+ }
+ size={'small'}
+ variant={'filled'}
+ style={{
+ color: 'inherit',
+ fontSize: 'inherit',
+ }}
+ >
+ {(resourcesCount || 0) + 1}
+
+
+ {category && t(`skills.categories.${category}.name` as any)}
+ {isFeatured && (
+
+ {t('isFeatured')}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+);
+
+export default SkillItem;
diff --git a/src/routes/(main)/community/(list)/skill/features/List/MetaInfo.tsx b/src/routes/(main)/community/(list)/skill/features/List/MetaInfo.tsx
new file mode 100644
index 0000000000..a5c94d82c5
--- /dev/null
+++ b/src/routes/(main)/community/(list)/skill/features/List/MetaInfo.tsx
@@ -0,0 +1,39 @@
+import { Flexbox, Icon } from '@lobehub/ui';
+import { DownloadIcon, MessageSquareIcon, StarIcon } from 'lucide-react';
+import { type CSSProperties } from 'react';
+import { memo } from 'react';
+
+interface MetaInfoProps {
+ className?: string;
+ commentCount?: number;
+ installCount?: number;
+ stars?: number;
+ style?: CSSProperties;
+}
+
+const MetaInfo = memo(({ style, stars, installCount, commentCount, className }) => {
+ return (
+
+ {Boolean(installCount) && (
+
+
+ {installCount}
+
+ )}
+ {Boolean(stars) && (
+
+
+ {stars}
+
+ )}
+ {Boolean(commentCount) && (
+
+
+ {commentCount}
+
+ )}
+
+ );
+});
+
+export default MetaInfo;
diff --git a/src/routes/(main)/community/(list)/skill/features/List/index.tsx b/src/routes/(main)/community/(list)/skill/features/List/index.tsx
new file mode 100644
index 0000000000..3e295db59d
--- /dev/null
+++ b/src/routes/(main)/community/(list)/skill/features/List/index.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { Grid } from '@lobehub/ui';
+import { memo } from 'react';
+
+import { type DiscoverSkillItem } from '@/types/discover';
+
+import SkillEmpty from '../../../../features/SkillEmpty';
+import Item from './Item';
+
+interface SkillListProps {
+ data?: DiscoverSkillItem[];
+ rows?: number;
+}
+
+const SkillList = memo(({ data = [], rows = 3 }) => {
+ if (data.length === 0) return ;
+
+ return (
+
+ {data.map((item, index) => (
+
+ ))}
+
+ );
+});
+
+export default SkillList;
diff --git a/src/routes/(main)/community/(list)/skill/index.tsx b/src/routes/(main)/community/(list)/skill/index.tsx
new file mode 100644
index 0000000000..984f847973
--- /dev/null
+++ b/src/routes/(main)/community/(list)/skill/index.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import { Flexbox } from '@lobehub/ui';
+import { memo } from 'react';
+
+import { useQuery } from '@/hooks/useQuery';
+import { useDiscoverStore } from '@/store/discover';
+import { type SkillQueryParams } from '@/types/discover';
+import { DiscoverTab, SkillSorts } from '@/types/discover';
+
+import Pagination from '../features/Pagination';
+import List from './features/List';
+import Loading from './loading';
+
+const SkillPage = memo(() => {
+ const { q, page, category, sort, order } = useQuery() as SkillQueryParams;
+ const useSkillList = useDiscoverStore((s) => s.useFetchSkillList);
+ const { data, isLoading } = useSkillList({
+ category,
+ order,
+ page,
+ pageSize: 21,
+ q,
+ sort: sort ?? SkillSorts.InstallCount,
+ });
+
+ if (isLoading || !data) return ;
+
+ const { items, currentPage, pageSize, totalCount } = data;
+
+ return (
+
+
+
+
+ );
+});
+
+export default SkillPage;
diff --git a/src/routes/(main)/community/(list)/skill/loading.tsx b/src/routes/(main)/community/(list)/skill/loading.tsx
new file mode 100644
index 0000000000..605487f240
--- /dev/null
+++ b/src/routes/(main)/community/(list)/skill/loading.tsx
@@ -0,0 +1 @@
+export { default } from '../../components/ListLoading';
diff --git a/src/routes/(main)/community/_layout/Sidebar/Header/Nav.tsx b/src/routes/(main)/community/_layout/Sidebar/Header/Nav.tsx
index 0e5f4fe00d..b9453193ef 100644
--- a/src/routes/(main)/community/_layout/Sidebar/Header/Nav.tsx
+++ b/src/routes/(main)/community/_layout/Sidebar/Header/Nav.tsx
@@ -1,7 +1,7 @@
'use client';
import { Flexbox } from '@lobehub/ui';
-import { McpIcon, ProviderIcon } from '@lobehub/ui/icons';
+import { McpIcon, ProviderIcon, SkillsIcon } from '@lobehub/ui/icons';
import { Bot, Brain, ShapesIcon } from 'lucide-react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -46,6 +46,12 @@ const Nav = memo(() => {
title: t('tab.assistant'),
url: '/community/agent',
},
+ {
+ icon: SkillsIcon,
+ key: DiscoverTab.Skills,
+ title: t('tab.skill'),
+ url: '/community/skill',
+ },
{
icon: McpIcon,
key: DiscoverTab.Mcp,
diff --git a/src/routes/(main)/community/features/SkillEmpty.tsx b/src/routes/(main)/community/features/SkillEmpty.tsx
new file mode 100644
index 0000000000..7a57bdc866
--- /dev/null
+++ b/src/routes/(main)/community/features/SkillEmpty.tsx
@@ -0,0 +1,35 @@
+import { type EmptyProps } from '@lobehub/ui';
+import { Center, Empty } from '@lobehub/ui';
+import { SkillsIcon } from '@lobehub/ui/icons';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+interface SkillEmptyProps extends Omit {
+ search?: boolean;
+}
+
+const SkillEmpty = memo(({ search, ...rest }) => {
+ const { t } = useTranslation('discover');
+
+ return (
+
+
+
+ );
+});
+
+SkillEmpty.displayName = 'SkillEmpty';
+
+export default SkillEmpty;
diff --git a/src/server/routers/lambda/market/skill.ts b/src/server/routers/lambda/market/skill.ts
index 6cff353d9a..5f6bce80f1 100644
--- a/src/server/routers/lambda/market/skill.ts
+++ b/src/server/routers/lambda/market/skill.ts
@@ -5,6 +5,7 @@ import { z } from 'zod';
import { publicProcedure, router } from '@/libs/trpc/lambda';
import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
import { MarketService } from '@/server/services/market';
+import { SkillSorts } from '@/types/discover';
const log = debug('lambda-router:market:skill');
@@ -24,38 +25,78 @@ const marketProcedure = publicProcedure
});
export const skillRouter = router({
- searchSkill: marketProcedure
+ getSkillCategories: marketProcedure
+ .input(
+ z
+ .object({
+ locale: z.string().optional(),
+ q: z.string().optional(),
+ })
+ .optional(),
+ )
+ .query(async ({ input, ctx }) => {
+ log('getSkillCategories input: %O', input);
+
+ try {
+ return await ctx.marketService.getSkillCategories();
+ } catch (error) {
+ log('Error fetching skill categories: %O', error);
+ throw new TRPCError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'Failed to fetch skill categories',
+ });
+ }
+ }),
+
+ getSkillDetail: marketProcedure
.input(
z.object({
+ identifier: z.string(),
locale: z.string().optional(),
- order: z.enum(['asc', 'desc']).optional(),
- page: z.number().optional(),
- pageSize: z.number().optional(),
- q: z.string().optional(),
- sort: z
- .enum([
- 'createdAt',
- 'forks',
- 'installCount',
- 'name',
- 'relevance',
- 'stars',
- 'updatedAt',
- 'watchers',
- ])
- .optional(),
+ version: z.string().optional(),
}),
)
.query(async ({ input, ctx }) => {
- log('searchSkill input: %O', input);
+ log('getSkillDetail input: %O', input);
try {
- return await ctx.marketService.searchSkill(input);
+ return await ctx.marketService.getSkillDetail(input.identifier, {
+ locale: input.locale,
+ version: input.version,
+ });
} catch (error) {
- log('Error searching skills: %O', error);
+ log('Error fetching skill detail: %O', error);
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
- message: 'Failed to search skills',
+ message: 'Failed to fetch skill detail',
+ });
+ }
+ }),
+
+ getSkillList: marketProcedure
+ .input(
+ z
+ .object({
+ category: z.string().optional(),
+ locale: z.string().optional(),
+ order: z.enum(['asc', 'desc']).optional(),
+ page: z.number().optional(),
+ pageSize: z.number().optional(),
+ q: z.string().optional(),
+ sort: z.nativeEnum(SkillSorts).optional(),
+ })
+ .optional(),
+ )
+ .query(async ({ input, ctx }) => {
+ log('getSkillList input: %O', input);
+
+ try {
+ return await ctx.marketService.searchSkill(input ?? {});
+ } catch (error) {
+ log('Error fetching skill list: %O', error);
+ throw new TRPCError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'Failed to fetch skill list',
});
}
}),
diff --git a/src/server/services/market/index.ts b/src/server/services/market/index.ts
index 6b13009907..4803f2f364 100644
--- a/src/server/services/market/index.ts
+++ b/src/server/services/market/index.ts
@@ -413,12 +413,7 @@ export class MarketService {
log('searchSkill response: %O', result);
- return {
- items: result.items,
- page: result.currentPage,
- pageSize: result.pageSize,
- total: result.totalCount,
- };
+ return result;
}
/**
diff --git a/src/server/services/riskControl/routerAlertNotification.ts b/src/server/services/riskControl/routerAlertNotification.ts
new file mode 100644
index 0000000000..ba59299ff2
--- /dev/null
+++ b/src/server/services/riskControl/routerAlertNotification.ts
@@ -0,0 +1,30 @@
+export interface ChannelStats {
+ errorCount: number;
+ successCount: number;
+ totalCount: number;
+}
+
+export interface AlertThresholds {
+ errorRateThreshold: number;
+ minSampleSize: number;
+}
+
+export const shouldAlert = (_stats: ChannelStats, _thresholds: AlertThresholds): boolean => {
+ return false;
+};
+
+export const sendRouterChannelAlertNotification = async (_params: {
+ channelId: string;
+ model: string;
+ routerId: string;
+ stats: ChannelStats;
+}): Promise => {
+ // Stub implementation
+};
+
+export const sendRouterModelAlertNotification = async (_params: {
+ model: string;
+ stats: ChannelStats;
+}): Promise => {
+ // Stub implementation
+};
diff --git a/src/server/services/toolExecution/serverRuntimes/skillStore.ts b/src/server/services/toolExecution/serverRuntimes/skillStore.ts
index e28dad761c..51609f724d 100644
--- a/src/server/services/toolExecution/serverRuntimes/skillStore.ts
+++ b/src/server/services/toolExecution/serverRuntimes/skillStore.ts
@@ -50,7 +50,13 @@ class SkillStoreServerRuntimeService implements SkillStoreRuntimeService {
try {
const result = await this.marketService.searchSkill(params);
log('Search skills result: %O', result);
- return result;
+ // Transform SDK response to match expected interface
+ return {
+ items: result.items,
+ page: result.currentPage,
+ pageSize: result.pageSize,
+ total: result.totalCount,
+ };
} catch (error) {
log('Error searching skills: %O', error);
throw error;
diff --git a/src/services/discover.ts b/src/services/discover.ts
index 36f6f532a1..2775ad7bd8 100644
--- a/src/services/discover.ts
+++ b/src/services/discover.ts
@@ -23,6 +23,7 @@ import {
type DiscoverModelDetail,
type DiscoverPluginDetail,
type DiscoverProviderDetail,
+ type DiscoverSkillDetail,
type DiscoverUserProfile,
type GroupAgentQueryParams,
type IdentifiersResponse,
@@ -34,6 +35,9 @@ import {
type PluginQueryParams,
type ProviderListResponse,
type ProviderQueryParams,
+ type SkillCategoryItem,
+ type SkillListResponse,
+ type SkillQueryParams,
} from '@/types/discover';
import { type MCPPluginListParams } from '@/types/plugins';
import { cleanObject } from '@/utils/object';
@@ -528,6 +532,52 @@ class DiscoverService {
return null;
}
+ // ============================== Skills Market ==============================
+
+ getSkillCategories = async (params: CategoryListQuery = {}): Promise => {
+ const locale = globalHelpers.getCurrentLanguage();
+ return lambdaClient.market.skill.getSkillCategories.query({
+ ...params,
+ locale,
+ });
+ };
+
+ getSkillDetail = async (params: {
+ identifier: string;
+ locale?: string;
+ version?: string;
+ }): Promise => {
+ const locale = globalHelpers.getCurrentLanguage();
+ return lambdaClient.market.skill.getSkillDetail.query({
+ ...params,
+ locale,
+ });
+ };
+
+ getSkillList = async (params: SkillQueryParams = {}): Promise => {
+ const locale = globalHelpers.getCurrentLanguage();
+ return lambdaClient.market.skill.getSkillList.query({
+ ...params,
+ locale,
+ page: params.page ? Number(params.page) : 1,
+ pageSize: params.pageSize ? Number(params.pageSize) : 20,
+ });
+ };
+
+ reportSkillEvent = async (eventData: { event: string; identifier: string; source?: string }) => {
+ const allow = userGeneralSettingsSelectors.telemetry(useUserStore.getState());
+ if (!allow) return;
+
+ const payload = cleanObject({
+ ...eventData,
+ source: eventData.source ?? 'community/skill',
+ });
+
+ // Note: skill event reporting can be added when the backend supports it
+ // Payload prepared for future backend integration
+ void payload;
+ };
+
// ============================== Group Agent Market ==============================
getGroupAgentCategories = async (params: CategoryListQuery = {}): Promise => {
diff --git a/src/services/marketApi.ts b/src/services/marketApi.ts
index 91e2a00ac0..145be0c68e 100644
--- a/src/services/marketApi.ts
+++ b/src/services/marketApi.ts
@@ -15,6 +15,7 @@ import {
type AgentGroupForkResponse,
type AgentGroupForkSourceResponse,
type AgentGroupForksResponse,
+ type SkillSorts,
} from '@/types/discover';
interface GetOwnAgentsParams {
@@ -210,41 +211,17 @@ export class MarketApiService {
* Search for skills in the LobeHub Market
*/
async searchSkill(params: {
+ category?: string;
locale?: string;
order?: 'asc' | 'desc';
page?: number;
pageSize?: number;
q?: string;
- sort?:
- | 'createdAt'
- | 'forks'
- | 'installCount'
- | 'name'
- | 'relevance'
- | 'stars'
- | 'updatedAt'
- | 'watchers';
- }): Promise<{
- items: Array<{
- category?: string;
- createdAt: string;
- description: string;
- installCount: number;
- identifier: string;
- name: string;
- repository?: string;
- sourceUrl?: string;
- summary?: string;
- updatedAt: string;
- version?: string;
- }>;
- page: number;
- pageSize: number;
- total: number;
- }> {
+ sort?: SkillSorts;
+ }) {
await discoverService.safeInjectMPToken();
- return lambdaClient.market.skill.searchSkill.query(params);
+ return lambdaClient.market.skill.getSkillList.query(params);
}
/**
diff --git a/src/spa/router/desktopRouter.config.tsx b/src/spa/router/desktopRouter.config.tsx
index 84d23a0c97..fa1b6607c2 100644
--- a/src/spa/router/desktopRouter.config.tsx
+++ b/src/spa/router/desktopRouter.config.tsx
@@ -138,6 +138,22 @@ export const desktopRoutes: RouteObject[] = [
),
path: 'provider',
},
+ {
+ children: [
+ {
+ element: dynamicElement(
+ () => import('@/routes/(main)/community/(list)/skill'),
+ 'Desktop > Discover > List > Skill',
+ ),
+ index: true,
+ },
+ ],
+ element: dynamicElement(
+ () => import('@/routes/(main)/community/(list)/skill/_layout'),
+ 'Desktop > Discover > List > Skill > Layout',
+ ),
+ path: 'skill',
+ },
{
children: [
{
@@ -198,6 +214,13 @@ export const desktopRoutes: RouteObject[] = [
),
path: 'provider/:slug',
},
+ {
+ element: dynamicElement(
+ () => import('@/routes/(main)/community/(detail)/skill'),
+ 'Desktop > Discover > Detail > Skill',
+ ),
+ path: 'skill/:slug',
+ },
{
element: dynamicElement(
() => import('@/routes/(main)/community/(detail)/mcp'),
diff --git a/src/store/discover/slices/skill/action.ts b/src/store/discover/slices/skill/action.ts
new file mode 100644
index 0000000000..79f38f9cf9
--- /dev/null
+++ b/src/store/discover/slices/skill/action.ts
@@ -0,0 +1,68 @@
+import { type CategoryListQuery } from '@lobehub/market-sdk';
+import { type SWRResponse } from 'swr';
+
+import { useClientDataSWR } from '@/libs/swr';
+import { discoverService } from '@/services/discover';
+import { type DiscoverStore } from '@/store/discover';
+import { globalHelpers } from '@/store/global/helpers';
+import { type StoreSetter } from '@/store/types';
+import {
+ type DiscoverSkillDetail,
+ type SkillCategoryItem,
+ type SkillListResponse,
+ type SkillQueryParams,
+} from '@/types/discover';
+
+type Setter = StoreSetter;
+
+export const createSkillSlice = (set: Setter, get: () => DiscoverStore, _api?: unknown) =>
+ new SkillActionImpl(set, get, _api);
+
+export class SkillActionImpl {
+ constructor(set: Setter, get: () => DiscoverStore, _api?: unknown) {
+ void _api;
+ void set;
+ void get;
+ }
+
+ useFetchSkillDetail = ({
+ identifier,
+ version,
+ }: {
+ identifier?: string;
+ version?: string;
+ }): SWRResponse => {
+ const locale = globalHelpers.getCurrentLanguage();
+
+ return useClientDataSWR(
+ !identifier ? null : ['skill-detail', locale, identifier, version].filter(Boolean).join('-'),
+ async () => discoverService.getSkillDetail({ identifier: identifier!, version }),
+ );
+ };
+
+ useFetchSkillList = (params: SkillQueryParams): SWRResponse => {
+ const locale = globalHelpers.getCurrentLanguage();
+ return useClientDataSWR(
+ ['skill-list', locale, ...Object.values(params)].filter(Boolean).join('-'),
+ async () =>
+ discoverService.getSkillList({
+ ...params,
+ page: params.page ? Number(params.page) : 1,
+ pageSize: params.pageSize ? Number(params.pageSize) : 21,
+ }),
+ );
+ };
+
+ useSkillCategories = (params: CategoryListQuery = {}): SWRResponse => {
+ const locale = globalHelpers.getCurrentLanguage();
+ return useClientDataSWR(
+ ['skill-categories', locale, ...Object.values(params)].join('-'),
+ async () => discoverService.getSkillCategories(params),
+ {
+ revalidateOnFocus: false,
+ },
+ );
+ };
+}
+
+export type SkillAction = Pick;
diff --git a/src/store/discover/slices/skill/index.ts b/src/store/discover/slices/skill/index.ts
new file mode 100644
index 0000000000..2c0622fbb9
--- /dev/null
+++ b/src/store/discover/slices/skill/index.ts
@@ -0,0 +1 @@
+export * from './action';
diff --git a/src/store/discover/store.ts b/src/store/discover/store.ts
index 0645f7ed99..cfbf1fe9c3 100644
--- a/src/store/discover/store.ts
+++ b/src/store/discover/store.ts
@@ -16,6 +16,8 @@ import { type PluginAction } from './slices/plugin/action';
import { createPluginSlice } from './slices/plugin/action';
import { type ProviderAction } from './slices/provider/action';
import { createProviderSlice } from './slices/provider/action';
+import { type SkillAction } from './slices/skill';
+import { createSkillSlice } from './slices/skill';
import { type SocialAction } from './slices/social';
import { createSocialSlice } from './slices/social';
import { type UserAction } from './slices/user';
@@ -29,6 +31,7 @@ export type DiscoverStore = MCPAction &
ProviderAction &
ModelAction &
PluginAction &
+ SkillAction &
SocialAction &
UserAction;
@@ -38,6 +41,7 @@ type DiscoverStoreAction = MCPAction &
ProviderAction &
ModelAction &
PluginAction &
+ SkillAction &
SocialAction &
UserAction;
@@ -51,6 +55,7 @@ const createStore: StateCreator =
createProviderSlice(...parameters),
createModelSlice(...parameters),
createPluginSlice(...parameters),
+ createSkillSlice(...parameters),
createSocialSlice(...parameters),
createUserSlice(...parameters),
]);
diff --git a/src/store/tool/slices/builtin/executors/lobe-skill-store.ts b/src/store/tool/slices/builtin/executors/lobe-skill-store.ts
index c62ce3acb5..cf7ceeba68 100644
--- a/src/store/tool/slices/builtin/executors/lobe-skill-store.ts
+++ b/src/store/tool/slices/builtin/executors/lobe-skill-store.ts
@@ -39,7 +39,18 @@ const runtime = new SkillStoreExecutionRuntime({
await getToolStoreState().refreshAgentSkills();
},
searchSkill: async (params) => {
- return marketApiService.searchSkill(params);
+ const result = await marketApiService.searchSkill({
+ ...params,
+ // Only pass sort if it's a valid SkillSorts value
+ sort: params.sort as any,
+ });
+ // Transform SDK response to match expected interface
+ return {
+ items: result.items,
+ page: result.currentPage,
+ pageSize: result.pageSize,
+ total: result.totalCount,
+ };
},
},
});