♻️ refactor: add the skills in community pages (#12761)

* feat: add the skills in community pages

* feat: add some skills & import the import routes

* feat: add detail used pages & prompt

* feat: add the skill sort way

* fix: ts fixed

* fix: ts fixed

* fix: test fixed

* fix: test fixed
This commit is contained in:
LiJian
2026-03-10 18:00:15 +08:00
committed by GitHub
parent 826a099f8d
commit eec8e113fc
45 changed files with 3137 additions and 58 deletions

View File

@@ -477,11 +477,129 @@
"search.placeholder": "Search by name, description, or keywords...",
"search.result": "{{count}} results about <highlight>{{keyword}}</highlight>",
"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",

View File

@@ -477,11 +477,129 @@
"search.placeholder": "搜索名称介绍或关键词…",
"search.result": "{{count}} 个关于 <highlight>{{keyword}}</highlight> 的搜索结果",
"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日",

View File

@@ -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 {

View File

@@ -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<MarketSkillListItem, 'commentCount'> {
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;

View File

@@ -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);
};

View File

@@ -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',

View File

@@ -0,0 +1,19 @@
'use client';
import { createContext, memo, type ReactNode, use } from 'react';
import { type DiscoverSkillDetail } from '@/types/discover';
export type DetailContextConfig = Partial<DiscoverSkillDetail>;
export const DetailContext = createContext<DetailContextConfig>({});
export const DetailProvider = memo<{ children: ReactNode; config?: DetailContextConfig }>(
({ children, config = {} }) => {
return <DetailContext value={config}>{children}</DetailContext>;
},
);
export const useDetailContext = () => {
return use(DetailContext);
};

View File

@@ -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 (
<Platform
downloadUrl={downloadUrl}
expandCodeByDefault
identifier={identifier}
mobile={mobile}
/>
);
});
export default Installation;

View File

@@ -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 = (
<Tabs
activeKey={activeTab}
compact={mobile}
items={[
{
icon: <Icon icon={BookOpenIcon} size={16} />,
key: SkillNavKey.Overview,
label: t('skills.details.overview.title'),
},
{
icon: <Icon icon={DownloadIcon} size={16} />,
key: SkillNavKey.Installation,
label: t('skills.details.sidebar.installationConfig'),
},
{
icon: <Icon icon={SkillsIcon} size={16} />,
key: SkillNavKey.Skill,
label: 'SKILL.md',
},
{
icon: <Icon icon={FileTextIcon} size={16} />,
key: SkillNavKey.Resources,
label:
resourcesCount > 1 ? (
<Flexbox
horizontal
align={'center'}
gap={6}
style={{
display: 'inline-flex',
}}
>
{t('skills.details.resources.title')}
<Tag>{resourcesCount}</Tag>
</Flexbox>
) : (
t('skills.details.resources.title')
),
},
{
icon: <Icon icon={ListIcon} size={16} />,
key: SkillNavKey.Related,
label: t('skills.details.related.title'),
},
{
icon: <Icon icon={HistoryIcon} size={16} />,
key: SkillNavKey.Version,
label:
versionCount > 1 ? (
<Flexbox
horizontal
align={'center'}
gap={6}
style={{
display: 'inline-flex',
}}
>
{t('skills.details.versions.title')}
<Tag>{versionCount}</Tag>
</Flexbox>
) : (
t('skills.details.versions.title')
),
},
]}
onChange={(key) => setActiveTab?.(key as SkillNavKey)}
/>
);
return mobile ? (
nav
) : (
<Flexbox horizontal align={'center'} className={styles.nav} justify={'space-between'}>
{nav}
<Flexbox horizontal gap={12}>
<a
className={styles.link}
href="https://discord.gg/AYFPHvv2jT"
rel="noopener noreferrer"
target={'_blank'}
>
{t('skills.details.nav.needHelp')}
</a>
{source && (
<a className={styles.link} href={source} rel="noopener noreferrer" target={'_blank'}>
{t('skills.details.nav.viewSourceCode')}
</a>
)}
{issueTarget && (
<a
className={styles.link}
href={urlJoin(issueTarget, 'issues')}
rel="noopener noreferrer"
target={'_blank'}
>
{t('skills.details.nav.reportIssue')}
</a>
)}
</Flexbox>
</Flexbox>
);
});
export default Nav;

View File

@@ -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<PropsWithChildren>(({ children }) => {
const { t } = useTranslation('discover');
const { tags = [], description, overview, category, related } = useDetailContext();
return (
<Flexbox gap={24}>
<Flexbox gap={16}>
<Title>{t('skills.details.summary.title')}</Title>
<Markdown variant={'chat'}>{overview?.summary || description || ''}</Markdown>
</Flexbox>
<Collapse
defaultActiveKey={['summary']}
expandIconPlacement={'end'}
variant={'outlined'}
items={[
{
children: (
<ScrollShadow height={240} offset={16} padding={16} size={16}>
<Flexbox horizontal gap={12}>
{children}
</Flexbox>
</ScrollShadow>
),
key: 'summary',
label: 'SKILL.md',
},
]}
padding={{
body: 0,
}}
/>
{tags.length > 0 && (
<Flexbox horizontal gap={8} wrap={'wrap'}>
{tags.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</Flexbox>
)}
{related && related.length > 0 && (
<Flexbox gap={16}>
<Title
more={t('skills.details.related.more')}
moreLink={qs.stringifyUrl({
query: { category },
url: '/community/skill',
})}
>
{t('skills.details.related.listTitle')}
</Title>
<List data={related} />
</Flexbox>
)}
</Flexbox>
);
});
export default Overview;

View File

@@ -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 (
<Flexbox gap={16}>
<Title
more={t('skills.details.related.more')}
moreLink={qs.stringifyUrl({
query: { category },
url: '/community/skill',
})}
>
{t('skills.details.related.listTitle')}
</Title>
<List data={related} />
</Flexbox>
);
});
export default Related;

View File

@@ -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<ResourceItem[]>(() => {
return Object.entries((resources || {}) as Record<string, ResourceMeta>)
.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 (
<Block variant={'outlined'}>
<Empty description={t('skills.details.resources.empty')} />
</Block>
);
}
return (
<Block style={{ overflow: 'hidden' }} variant={'outlined'}>
<InlineTable
dataSource={dataSource}
pagination={false}
rowKey={'name'}
size={'middle'}
columns={[
{
dataIndex: 'name',
key: 'name',
render: (text) => {
const link = getResourceLink(text);
const node = (
<Flexbox horizontal align={'center'} gap={16}>
<MaterialFileTypeIcon
fallbackUnknownType={false}
filename={text}
size={24}
type={'file'}
/>
<Text code style={{ wordBreak: 'break-all' }} type={'info'}>
{text}
</Text>
</Flexbox>
);
if (!link) {
return node;
}
return (
<a href={link} rel={'noreferrer'} target={'_blank'}>
{node}
</a>
);
},
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 (
<Text code fontSize={13} type={'secondary'}>
{size}
</Text>
);
},
title: t('skills.details.resources.table.size'),
},
]}
/>
</Block>
);
});
export default Resources;

View File

@@ -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 (
<Flexbox gap={16}>
<Title>{t('skills.details.versions.title')}</Title>
<Block variant={'outlined'}>
<InlineTable
columns={[
{
dataIndex: 'version',
render: (_, record) => (
<Link
style={{ color: 'inherit' }}
to={qs.stringifyUrl({
query: {
version: record.version,
},
url: pathname,
})}
>
<Flexbox align={'center'} gap={8} horizontal>
<code style={{ fontSize: 14 }}>{record.version}</code>
{record.isLatest && (
<Tag color={'info'}>{t('skills.details.versions.table.isLatest')}</Tag>
)}
</Flexbox>
</Link>
),
title: t('skills.details.versions.table.version'),
},
{
align: 'end',
dataIndex: 'createdAt',
render: (_, record) => <PublishedTime date={record.createdAt} />,
title: t('skills.details.versions.table.publishAt'),
},
]}
dataSource={versions}
rowKey={'version'}
size={'middle'}
/>
</Block>
</Flexbox>
);
});
export default Versions;

View File

@@ -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<SkillNavKey>(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 = <Markdown variant={'chat'}>{content ?? ''}</Markdown>;
return (
<Flexbox gap={24}>
<Nav activeTab={activeTab} mobile={isMobile} setActiveTab={handleSetActiveTab} />
<Flexbox
gap={48}
horizontal={!isMobile}
style={isMobile ? { flexDirection: 'column-reverse' } : undefined}
>
<Flexbox flex={1} style={{ minWidth: 0 }} width={'100%'}>
{activeTab === SkillNavKey.Overview && <Overview>{skillContent}</Overview>}
{activeTab === SkillNavKey.Installation && <Installation mobile={isMobile} />}
{activeTab === SkillNavKey.Skill && skillContent}
{activeTab === SkillNavKey.Resources && <Resources />}
{activeTab === SkillNavKey.Related && <Related />}
{activeTab === SkillNavKey.Version && <Versions />}
</Flexbox>
<Sidebar activeTab={activeTab} mobile={isMobile} />
</Flexbox>
</Flexbox>
);
});
export default Details;

View File

@@ -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 = (
<Flexbox align={'center'} className={styles.extraTag} gap={16} horizontal>
<Flexbox align={'center'} className={styles.extraTagActive} gap={8} horizontal>
<Icon icon={FileTextIcon} size={14} />
{resourcesCount}
</Flexbox>
</Flexbox>
);
const cateButton = cate ? (
<Link
to={qs.stringifyUrl({
query: { category: cate.key },
url: '/community/skill',
})}
>
<Button icon={<Icon icon={cate.icon} />} size={'middle'} variant={'outlined'}>
{cate.label}
</Button>
</Link>
) : null;
return (
<Flexbox gap={12}>
<Flexbox align={'flex-start'} gap={16} horizontal width={'100%'}>
<Avatar avatar={icon || name} size={mobile ? 48 : 64} />
<Flexbox
flex={1}
gap={4}
style={{
overflow: 'hidden',
}}
>
<Flexbox
align={'center'}
gap={8}
horizontal
justify={'space-between'}
style={{
overflow: 'hidden',
position: 'relative',
}}
>
<Flexbox
align={'center'}
flex={1}
gap={12}
horizontal
style={{
overflow: 'hidden',
position: 'relative',
}}
>
<Text
as={'h1'}
ellipsis
style={{ fontSize: mobile ? 18 : 24, margin: 0 }}
title={identifier}
>
{name}
</Text>
{!mobile && scores}
</Flexbox>
<Flexbox align={'center'} gap={6} horizontal>
{homepage && (
<a
href={homepage}
onClick={stopPropagation}
rel="noopener noreferrer"
target={'_blank'}
>
<ActionIcon fill={cssVar.colorTextDescription} icon={Github} />
</a>
)}
</Flexbox>
</Flexbox>
<Flexbox align={'center'} gap={4} horizontal>
{Boolean(ratingAverage) ? (
<Flexbox align={'center'} gap={8} horizontal>
<Icon fill={cssVar.colorWarning} icon={StarIcon} size={14} />
<Text weight={500}>{ratingAverage?.toFixed(1)}</Text>
</Flexbox>
) : (
<div className={styles.version}>{version}</div>
)}
<Icon icon={DotIcon} />
{author?.url ? (
<a href={author.url} rel="noopener noreferrer" target={'_blank'}>
{author.name}
</a>
) : (
<span>{author?.name}</span>
)}
<Icon icon={DotIcon} />
<PublishedTime
className={styles.time}
date={(updatedAt || createdAt) as string}
template={'MMM DD, YYYY'}
/>
</Flexbox>
</Flexbox>
</Flexbox>
<Flexbox
align={'center'}
gap={mobile ? 12 : 24}
horizontal
style={{
color: cssVar.colorTextSecondary,
}}
wrap={'wrap'}
>
{mobile && scores}
{!mobile && cateButton}
<Flexbox align={'center'} gap={mobile ? 12 : 24} horizontal wrap={'wrap'}>
{Boolean(license?.name) && (
<Flexbox align={'center'} gap={6} horizontal>
<Icon icon={ScaleIcon} size={14} />
{license?.name}
</Flexbox>
)}
{Boolean(installCount) && (
<Flexbox align={'center'} gap={6} horizontal>
<Icon icon={DownloadIcon} size={14} />
{formatCompactNumber(installCount)}
</Flexbox>
)}
{Boolean(github?.stars) && (
<Flexbox align={'center'} gap={6} horizontal>
<Icon icon={StarIcon} size={14} />
{formatCompactNumber(github?.stars)}
</Flexbox>
)}
{Boolean(comments?.totalCount) && (
<Flexbox align={'center'} gap={6} horizontal>
<Icon icon={MessageSquare} size={14} />
{formatCompactNumber(comments?.totalCount)}
</Flexbox>
)}
</Flexbox>
</Flexbox>
</Flexbox>
);
});
export default Header;

View File

@@ -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<typeof Tree.DirectoryTree>;
interface TreeNode {
children: Map<string, TreeNode>;
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: (
<MaterialFileTypeIcon
fallbackUnknownType={false}
filename={node.name}
size={20}
type={isFolder ? 'folder' : 'file'}
/>
),
key: node.key,
title: <Text ellipsis>{node.name}</Text>,
};
};
const FileTree = memo(() => {
const { t } = useTranslation('discover');
const { resources = {} } = useDetailContext();
const { pathname, search } = useLocation();
const [expand, setExpand] = useState<Key[]>([]);
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<string, unknown>);
// 至少包含主文件,避免出现空树
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 (
<Flexbox gap={12}>
<Title more={t('skills.details.sidebar.details')} moreLink={detailLink}>
{t('skills.details.sidebar.files')}
</Title>
<Tree
showIcon
showLine
className={styles.tree}
defaultExpandAll={Object.entries(resources || {})?.length <= 10}
expandedKeys={expand}
switcherIcon={<Icon icon={ChevronDown} size={14} />}
treeData={treeData}
onExpand={onExpand}
onSelect={onSelect}
/>
</Flexbox>
);
});
export default FileTree;

View File

@@ -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 (
<Flexbox gap={12}>
<Title more={t('mcp.details.sidebar.moreServerConfig')} moreLink={installLink}>
{t('skills.details.sidebar.installationConfig')}
</Title>
<Platform downloadUrl={downloadUrl} expandCodeByDefault identifier={identifier} lite />
</Flexbox>
);
});
export default InstallationConfig;

View File

@@ -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 || '<skill-identifier>';
const agentMap: Record<PlatformType, string> = {
[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 || '<skill-identifier>';
const basePathMap: Record<PlatformType, string> = {
[PlatformType.Claude]: `~/.claude/skills/${id}`,
[PlatformType.Cline]: `~/.cline/skills/${id}`,
[PlatformType.Cursor]: `~/.cursor/skills/${id}`,
[PlatformType.LobeHub]: `<managed-by-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<PlatformProps>(
({ 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>(PlatformType.Claude);
const [mode, setMode] = useState<GuideMode>('agent');
const options = [
{
icon: <Claude.Color className={'anticon'} size={18} />,
label: 'Claude Code',
value: PlatformType.Claude,
},
{
icon: <OpenAI className={'anticon'} size={18} />,
label: 'Codex',
value: PlatformType.Codex,
},
{
icon: <Cursor className={'anticon'} size={18} />,
label: 'Cursor',
value: PlatformType.Cursor,
},
{
icon: <VsCodeIcon className={'anticon'} size={18} />,
label: 'VsCode',
value: PlatformType.VsCode,
},
{
icon: <Cline className={'anticon'} size={18} />,
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 (
<Block gap={lite ? 0 : 16} padding={4} variant={lite ? 'outlined' : 'borderless'}>
<Segmented
block
style={{ marginBottom: 8 }}
value={mode}
variant={'filled'}
options={[
{
icon: <Icon icon={BotIcon} />,
label: t('skills.hero.guide.agent'),
value: 'agent',
},
{
icon: <Icon icon={UserRoundIcon} />,
label: t('skills.hero.guide.human'),
value: 'human',
},
]}
onChange={(value) => setMode(value as GuideMode)}
/>
{mode === 'agent' ? (
<Flexbox gap={mobile || lite ? 0 : 16}>
{mobile || lite ? (
<Text align={'center'} as={'h3'} fontSize={14} style={{ padding: 8 }} weight={500}>
{t('skills.details.sidebar.agent.title')}
</Text>
) : (
<Title>{t('skills.details.sidebar.agent.title')}</Title>
)}
<Highlighter
fullFeatured
wrap
className={cx(lite && styles.lite)}
defaultExpand={expandCodeByDefault ?? false}
fileName={'Agent prompt'}
language={'bash'}
style={{ fontSize: 12 }}
variant={lite ? 'borderless' : 'outlined'}
>
{agentPrompt}
</Highlighter>
<Flexbox padding={8}>
<Button
block
icon={<Avatar avatar={DEFAULT_INBOX_AVATAR} size={18} />}
size={'large'}
type={'primary'}
onClick={handleUseOnLobeAI}
>
{t('skills.details.sidebar.agent.useOnLobeAI')}
</Button>
</Flexbox>
</Flexbox>
) : (
<>
{mobile || lite ? (
<Select
value={active}
variant={'filled'}
options={options.map((item) => ({
...item,
label: (
<Flexbox horizontal align={'center'} gap={8}>
{item.icon} {item.label}
</Flexbox>
),
}))}
onSelect={(v) => setActive(v as PlatformType)}
/>
) : (
<Segmented
block
options={options}
value={active}
onChange={(v) => setActive(v as PlatformType)}
/>
)}
<Flexbox>
{!lite && <Title>{t('skills.details.sidebar.platform.title', { platform })}</Title>}
<Markdown variant={'chat'}>{steps}</Markdown>
</Flexbox>
{lite && <Divider dashed style={{ margin: 0 }} />}
<Highlighter
fullFeatured
className={cx(lite && styles.lite)}
defaultExpand={expandCodeByDefault ?? false}
fileName={t('skills.details.sidebar.installCommand')}
language={'bash'}
style={{ fontSize: 12 }}
variant={lite ? 'borderless' : 'outlined'}
>
{command}
</Highlighter>
{lite && <Divider dashed style={{ margin: 0 }} />}
<Highlighter
fullFeatured
className={cx(lite && styles.lite)}
defaultExpand={false}
fileName={t('skills.details.sidebar.directoryLayout')}
language={'text'}
style={{ fontSize: 12 }}
variant={lite ? 'borderless' : 'outlined'}
>
{genLayout(identifier, active, {
lobehub: t('skills.details.sidebar.platform.layout.lobehub'),
resourcesHint: t('skills.details.sidebar.platform.layout.resourcesHint'),
})}
</Highlighter>
{downloadUrl && (
<>
<Divider dashed style={{ margin: 0 }} />
<Flexbox padding={8}>
<Button
block
href={downloadUrl}
size={'large'}
target={'_blank'}
type={'primary'}
>
{t('skills.details.sidebar.downloadSkill')}
</Button>
</Flexbox>
</>
)}
</>
)}
</Block>
);
},
);
export default Platform;

View File

@@ -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 (
<svg
height={size}
style={{ flex: 'none', lineHeight: 1, ...style }}
viewBox="0 0 100 100"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<mask height="100" id={a.id} maskUnits="userSpaceOnUse" width="100" x="0" y="0">
<path
clipRule="evenodd"
d="M70.912 99.317a6.223 6.223 0 004.96-.19l20.589-9.907A6.25 6.25 0 00100 83.587V16.413a6.25 6.25 0 00-3.54-5.632L75.874.874a6.226 6.226 0 00-7.104 1.21L29.355 38.04 12.187 25.01a4.162 4.162 0 00-5.318.236l-5.506 5.009a4.168 4.168 0 00-.004 6.162L16.247 50 1.36 63.583a4.168 4.168 0 00.004 6.162l5.506 5.01a4.162 4.162 0 005.318.236l17.168-13.032L68.77 97.917a6.217 6.217 0 002.143 1.4zM75.015 27.3L45.11 50l29.906 22.701V27.3z"
fill="#fff"
fillRule="evenodd"
/>
</mask>
<g mask={a.fill}>
<path
d="M96.461 10.796L75.857.876a6.23 6.23 0 00-7.107 1.207l-67.451 61.5a4.167 4.167 0 00.004 6.162l5.51 5.009a4.167 4.167 0 005.32.236l81.228-61.62c2.725-2.067 6.639-.124 6.639 3.297v-.24a6.25 6.25 0 00-3.539-5.63z"
fill="#0065A9"
/>
<g filter={b.fill}>
<path
d="M96.461 89.204l-20.604 9.92a6.229 6.229 0 01-7.107-1.207l-67.451-61.5a4.167 4.167 0 01.004-6.162l5.51-5.009a4.167 4.167 0 015.32-.236l81.228 61.62c2.725 2.067 6.639.124 6.639-3.297v.24a6.25 6.25 0 01-3.539 5.63z"
fill="#007ACC"
/>
</g>
<g filter={c.fill}>
<path
d="M75.858 99.126a6.232 6.232 0 01-7.108-1.21c2.306 2.307 6.25.674 6.25-2.588V4.672c0-3.262-3.944-4.895-6.25-2.589a6.232 6.232 0 017.108-1.21l20.6 9.908A6.25 6.25 0 01100 16.413v67.174a6.25 6.25 0 01-3.541 5.633l-20.601 9.906z"
fill="#1F9CF0"
/>
</g>
<path
clipRule="evenodd"
d="M70.851 99.317a6.224 6.224 0 004.96-.19L96.4 89.22a6.25 6.25 0 003.54-5.633V16.413a6.25 6.25 0 00-3.54-5.632L75.812.874a6.226 6.226 0 00-7.104 1.21L29.294 38.04 12.126 25.01a4.162 4.162 0 00-5.317.236l-5.507 5.009a4.168 4.168 0 00-.004 6.162L16.186 50 1.298 63.583a4.168 4.168 0 00.004 6.162l5.507 5.009a4.162 4.162 0 005.317.236L29.294 61.96l39.414 35.958a6.218 6.218 0 002.143 1.4zM74.954 27.3L45.048 50l29.906 22.701V27.3z"
fill={d.fill}
fillRule="evenodd"
opacity=".25"
/>
</g>
<defs>
<filter
filterUnits="userSpaceOnUse"
height="92.246"
id={b.id}
width="116.727"
x="-8.394"
y="15.829"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="4.167" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend in2="BackgroundImageFix" mode="overlay" result="effect1_dropShadow" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
</filter>
<filter
filterUnits="userSpaceOnUse"
height="116.151"
id={c.id}
width="47.917"
x="60.417"
y="-8.076"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="4.167" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend in2="BackgroundImageFix" mode="overlay" result="effect1_dropShadow" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
</filter>
<linearGradient
gradientUnits="userSpaceOnUse"
id={d.id}
x1="49.939"
x2="49.939"
y1=".258"
y2="99.742"
>
<stop stopColor="#fff" />
<stop offset="1" stopColor="#fff" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
});
export default VsCodeIcon;

View File

@@ -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 = (
<ShareButton
block
size={'large'}
meta={{
avatar: icon,
desc: description,
hashtags: tags,
title: name,
url: urlJoin('https://lobehub.com/skills', identifier || ''),
}}
>
{t('share')}
</ShareButton>
);
if (mobile) {
if (activeTab !== SkillNavKey.Overview && activeTab !== SkillNavKey.Resources) return null;
return (
<Flexbox gap={24} width={'100%'}>
{showInstallationConfig && <InstallationConfig />}
{shareButton}
{showFileTree && <FileTree />}
</Flexbox>
);
}
return (
<ScrollShadow
hideScrollBar
flex={'none'}
gap={24}
size={4}
width={360}
style={{
maxHeight: 'calc(100vh - 114px)',
paddingBottom: 24,
position: 'sticky',
top: 114,
}}
>
{showInstallationConfig && <InstallationConfig />}
{shareButton}
{showFileTree && <FileTree />}
</ScrollShadow>
);
},
);
export default Sidebar;

View File

@@ -0,0 +1,8 @@
export enum SkillNavKey {
Installation = 'installation',
Overview = 'overview',
Related = 'related',
Resources = 'resources',
Skill = 'skill',
Version = 'version',
}

View File

@@ -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<SkillDetailPageProps>(({ 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 <Loading />;
if (!data) return <NotFound />;
return (
<TocProvider>
<DetailProvider config={data}>
<Flexbox data-testid="skill-detail-content" gap={16}>
<Header mobile={mobile} />
<Details mobile={mobile} />
</Flexbox>
</DetailProvider>
</TocProvider>
);
});
export const MobileSkillPage = memo<{ mobile?: boolean }>(() => {
return <SkillDetailPage mobile={true} />;
});
export default SkillDetailPage;

View File

@@ -0,0 +1 @@
export { DetailsLoading as default } from '../../components/ListLoading';

View File

@@ -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 [];
}

View File

@@ -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 (
<Flexbox horizontal className={styles.mainContainer} gap={24} width={'100%'}>
<CategoryContainer>
<Category />
</CategoryContainer>
<Flexbox flex={1} gap={16}>
<Outlet />
</Flexbox>
</Flexbox>
);
};
export default Layout;

View File

@@ -0,0 +1,7 @@
import { createStaticStyles } from 'antd-style';
export const styles = createStaticStyles(({ css }) => ({
mainContainer: css`
position: relative;
`,
}));

View File

@@ -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 (
<CategoryMenu
mode={'inline'}
selectedKeys={[category]}
items={cates.map((item) => {
const itemData = items.find((i) => i.category === item.key);
return {
extra:
item.key === 'all'
? total > 0 && (
<Tag
size={'small'}
style={{
borderRadius: 12,
paddingInline: 6,
}}
>
{total}
</Tag>
)
: itemData && (
<Tag
size={'small'}
style={{
borderRadius: 12,
paddingInline: 6,
}}
>
{itemData.count}
</Tag>
),
...item,
icon: <Icon icon={item.icon} size={18} />,
label: <Link to={genUrl(item.key)}>{item.label}</Link>,
};
})}
onClick={(v) => handleClick(v.key as SkillCategory)}
/>
);
});
export default withSuspense(Category);

View File

@@ -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<DiscoverSkillItem>(
({
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 (
<Block
clickable
data-testid="skill-item"
height={'100%'}
variant={'outlined'}
width={'100%'}
style={{
overflow: 'hidden',
position: 'relative',
}}
onClick={handleClick}
>
{isFeatured && <Spotlight size={400} />}
<Flexbox
horizontal
align={'flex-start'}
gap={16}
justify={'space-between'}
padding={16}
width={'100%'}
>
<Flexbox
horizontal
gap={12}
title={identifier}
style={{
overflow: 'hidden',
}}
>
<Avatar avatar={icon || name} size={40} style={{ flex: 'none' }} />
<Flexbox
flex={1}
gap={6}
style={{
overflow: 'hidden',
}}
>
<Flexbox
horizontal
align={'center'}
flex={1}
gap={8}
style={{
overflow: 'hidden',
}}
>
<Link style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
<Text ellipsis as={'h2'} className={styles.title}>
{name}
</Text>
</Link>
</Flexbox>
<Flexbox horizontal align={'center'} className={styles.author} gap={8}>
{Boolean(ratingAvg) && (
<Flexbox horizontal align={'center'} gap={4} style={{ fontSize: 13 }}>
<Icon fill={cssVar.colorTextDescription} icon={StarIcon} size={12} />
{ratingAvg?.toFixed(1)}
</Flexbox>
)}
{author && <div>{author}</div>}
</Flexbox>
</Flexbox>
</Flexbox>
<Flexbox horizontal align={'center'} gap={4}>
{github?.url && (
<a
href={github.url}
rel="noopener noreferrer"
target={'_blank'}
onClick={stopPropagation}
>
<ActionIcon fill={cssVar.colorTextDescription} icon={Github} />
</a>
)}
</Flexbox>
</Flexbox>
<Flexbox flex={1} gap={12} paddingInline={16}>
<Text
as={'p'}
className={styles.desc}
ellipsis={{
rows: 3,
}}
>
{description}
</Text>
<Flexbox
horizontal
align={'center'}
className={styles.secondaryDesc}
justify={'space-between'}
>
<Tag
icon={<Icon icon={FileTextIcon} />}
size={'small'}
variant={'filled'}
style={{
color: 'inherit',
fontSize: 'inherit',
}}
>
{(resourcesCount || 0) + 1}
</Tag>
<Flexbox horizontal align={'center'} className={styles.secondaryDesc} gap={8}>
{category && t(`skills.categories.${category}.name` as any)}
{isFeatured && (
<Tag
size={'small'}
variant={'outlined'}
style={{
color: 'inherit',
fontSize: 'inherit',
}}
>
{t('isFeatured')}
</Tag>
)}
</Flexbox>
</Flexbox>
</Flexbox>
<Flexbox
horizontal
align={'center'}
className={styles.footer}
justify={'space-between'}
padding={16}
>
<Flexbox horizontal align={'center'} gap={4}>
<Icon className={styles.secondaryDesc} icon={ClockIcon} size={14} />
<PublishedTime className={styles.secondaryDesc} date={updatedAt} />
</Flexbox>
<MetaInfo
className={styles.secondaryDesc}
commentCount={commentCount}
installCount={installCount}
stars={github?.stars}
/>
</Flexbox>
</Block>
);
},
);
export default SkillItem;

View File

@@ -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<MetaInfoProps>(({ style, stars, installCount, commentCount, className }) => {
return (
<Flexbox horizontal align={'center'} className={className} gap={8} style={style}>
{Boolean(installCount) && (
<Flexbox horizontal align={'center'} gap={4}>
<Icon icon={DownloadIcon} size={14} />
{installCount}
</Flexbox>
)}
{Boolean(stars) && (
<Flexbox horizontal align={'center'} gap={4}>
<Icon icon={StarIcon} size={14} />
{stars}
</Flexbox>
)}
{Boolean(commentCount) && (
<Flexbox horizontal align={'center'} gap={4}>
<Icon icon={MessageSquareIcon} size={14} />
{commentCount}
</Flexbox>
)}
</Flexbox>
);
});
export default MetaInfo;

View File

@@ -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<SkillListProps>(({ data = [], rows = 3 }) => {
if (data.length === 0) return <SkillEmpty />;
return (
<Grid rows={rows} width={'100%'}>
{data.map((item, index) => (
<Item key={index} {...item} />
))}
</Grid>
);
});
export default SkillList;

View File

@@ -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 <Loading />;
const { items, currentPage, pageSize, totalCount } = data;
return (
<Flexbox gap={32} width={'100%'}>
<List data={items} />
<Pagination
currentPage={currentPage}
pageSize={pageSize}
tab={DiscoverTab.Skills}
total={totalCount}
/>
</Flexbox>
);
});
export default SkillPage;

View File

@@ -0,0 +1 @@
export { default } from '../../components/ListLoading';

View File

@@ -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,

View File

@@ -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<EmptyProps, 'icon'> {
search?: boolean;
}
const SkillEmpty = memo<SkillEmptyProps>(({ search, ...rest }) => {
const { t } = useTranslation('discover');
return (
<Center height="100%" style={{ minHeight: '50vh' }} width="100%">
<Empty
description={search ? t('skillEmpty.search') : t('skillEmpty.description')}
icon={SkillsIcon}
title={search ? undefined : t('skillEmpty.title')}
type={search ? 'default' : 'page'}
descriptionProps={{
fontSize: 14,
}}
style={{
maxWidth: 400,
}}
{...rest}
/>
</Center>
);
});
SkillEmpty.displayName = 'SkillEmpty';
export default SkillEmpty;

View File

@@ -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',
});
}
}),

View File

@@ -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;
}
/**

View File

@@ -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<void> => {
// Stub implementation
};
export const sendRouterModelAlertNotification = async (_params: {
model: string;
stats: ChannelStats;
}): Promise<void> => {
// Stub implementation
};

View File

@@ -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;

View File

@@ -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<SkillCategoryItem[]> => {
const locale = globalHelpers.getCurrentLanguage();
return lambdaClient.market.skill.getSkillCategories.query({
...params,
locale,
});
};
getSkillDetail = async (params: {
identifier: string;
locale?: string;
version?: string;
}): Promise<DiscoverSkillDetail> => {
const locale = globalHelpers.getCurrentLanguage();
return lambdaClient.market.skill.getSkillDetail.query({
...params,
locale,
});
};
getSkillList = async (params: SkillQueryParams = {}): Promise<SkillListResponse> => {
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<CategoryItem[]> => {

View File

@@ -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);
}
/**

View File

@@ -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'),

View File

@@ -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<DiscoverStore>;
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<DiscoverSkillDetail> => {
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<SkillListResponse> => {
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<SkillCategoryItem[]> => {
const locale = globalHelpers.getCurrentLanguage();
return useClientDataSWR(
['skill-categories', locale, ...Object.values(params)].join('-'),
async () => discoverService.getSkillCategories(params),
{
revalidateOnFocus: false,
},
);
};
}
export type SkillAction = Pick<SkillActionImpl, keyof SkillActionImpl>;

View File

@@ -0,0 +1 @@
export * from './action';

View File

@@ -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<DiscoverStore, [['zustand/devtools', never]]> =
createProviderSlice(...parameters),
createModelSlice(...parameters),
createPluginSlice(...parameters),
createSkillSlice(...parameters),
createSocialSlice(...parameters),
createUserSlice(...parameters),
]);

View File

@@ -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,
};
},
},
});