mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-11 03:37:57 +00:00
feat: Implement core web application structure including landing page, skill registry, layout, routing, and internationalization. (#87)
This commit is contained in:
parent
f5db6edde9
commit
42e6fff693
9 changed files with 386 additions and 4 deletions
|
|
@ -10,6 +10,14 @@ export function Layout() {
|
|||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||
const { user, isLoading } = useAuth()
|
||||
|
||||
if (pathname === '/registry/skill') {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
const navItems: Array<{
|
||||
label: string
|
||||
to: string
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ const SearchPage = createLazyRouteComponent(() => import('@/pages/search'), 'Sea
|
|||
const TermsOfServicePage = createLazyRouteComponent(() => import('@/pages/terms'), 'TermsOfServicePage')
|
||||
const NamespacePage = createLazyRouteComponent(() => import('@/pages/namespace'), 'NamespacePage')
|
||||
const SkillDetailPage = createLazyRouteComponent(() => import('@/pages/skill-detail'), 'SkillDetailPage')
|
||||
const RegistrySkillPage = createLazyRouteComponent(() => import('@/pages/registry-skill'), 'RegistrySkillPage')
|
||||
const DashboardPage = createLazyRouteComponent(() => import('@/pages/dashboard'), 'DashboardPage')
|
||||
const MySkillsPage = createLazyRouteComponent(() => import('@/pages/dashboard/my-skills'), 'MySkillsPage')
|
||||
const PublishPage = createLazyRouteComponent(() => import('@/pages/dashboard/publish'), 'PublishPage')
|
||||
|
|
@ -214,6 +215,12 @@ const skillDetailRoute = createRoute({
|
|||
component: SkillDetailPage,
|
||||
})
|
||||
|
||||
const registrySkillRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/registry/skill',
|
||||
component: RegistrySkillPage,
|
||||
})
|
||||
|
||||
const dashboardRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: 'dashboard',
|
||||
|
|
@ -360,6 +367,7 @@ const routeTree = rootRoute.addChildren([
|
|||
termsRoute,
|
||||
namespaceRoute,
|
||||
skillDetailRoute,
|
||||
registrySkillRoute,
|
||||
dashboardRoute,
|
||||
dashboardSkillsRoute,
|
||||
dashboardPublishRoute,
|
||||
|
|
|
|||
193
web/src/docs/skill.md
Normal file
193
web/src/docs/skill.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
---
|
||||
name: skillhub-registry
|
||||
description: Use this when you need to search, inspect, install, or publish agent skills against a SkillHub registry. SkillHub is a self-hosted skill registry with a ClawHub-compatible API layer, so prefer the `clawhub` CLI for registry operations instead of making raw HTTP calls.
|
||||
---
|
||||
|
||||
# SkillHub Registry
|
||||
|
||||
Use this skill when you need to work with a SkillHub deployment: search skills, inspect metadata, install a package, or publish a new version.
|
||||
|
||||
> Important: Prefer the `clawhub` CLI for registry workflows. SkillHub exposes a ClawHub-compatible API surface and a discovery endpoint at `/.well-known/clawhub.json`, so the CLI is the safest path for auth, resolution, and download behavior. Only fall back to raw HTTP when debugging the server itself.
|
||||
|
||||
## What SkillHub Is
|
||||
|
||||
SkillHub is a self-hosted, enterprise-oriented skill registry. It stores versioned skill packages, supports namespace-based governance, and keeps `SKILL.md` compatibility with OpenSkills-style packages.
|
||||
|
||||
Key facts:
|
||||
|
||||
- Internal coordinates use `@{namespace}/{skill_slug}`.
|
||||
- ClawHub-compatible clients use a canonical slug instead.
|
||||
- `latest` always means the latest published version, never draft or pending review.
|
||||
- Public skills in `@global` can be downloaded anonymously.
|
||||
- Team namespace skills and non-public skills require authentication.
|
||||
|
||||
## Configure The CLI
|
||||
|
||||
Point `clawhub` at the SkillHub base URL:
|
||||
|
||||
```bash
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
```
|
||||
|
||||
If you need authenticated access, provide an API token:
|
||||
|
||||
```bash
|
||||
export CLAWHUB_API_TOKEN=sk_your_api_token_here
|
||||
```
|
||||
|
||||
Optional local check:
|
||||
|
||||
```bash
|
||||
curl https://skillhub.your-company.com/.well-known/clawhub.json
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{ "apiBase": "/api/v1" }
|
||||
```
|
||||
|
||||
## Coordinate Rules
|
||||
|
||||
SkillHub has two naming forms:
|
||||
|
||||
| SkillHub coordinate | Canonical slug for `clawhub` |
|
||||
|---|---|
|
||||
| `@global/my-skill` | `my-skill` |
|
||||
| `@team-name/my-skill` | `team-name--my-skill` |
|
||||
|
||||
Rules:
|
||||
|
||||
- `--` is the namespace separator in the compatibility layer.
|
||||
- If there is no `--`, the skill is treated as `@global/...`.
|
||||
- `latest` resolves to the latest published version only.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
npx clawhub install my-skill
|
||||
npx clawhub install my-skill@1.2.0
|
||||
npx clawhub install team-name--my-skill
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Search
|
||||
|
||||
```bash
|
||||
npx clawhub search email
|
||||
```
|
||||
|
||||
Use an empty query when you want a broad listing:
|
||||
|
||||
```bash
|
||||
npx clawhub search ""
|
||||
```
|
||||
|
||||
### Inspect A Skill
|
||||
|
||||
```bash
|
||||
npx clawhub info my-skill
|
||||
npx clawhub info team-name--my-skill
|
||||
```
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npx clawhub install my-skill
|
||||
npx clawhub install my-skill@1.2.0
|
||||
npx clawhub install team-name--my-skill
|
||||
```
|
||||
|
||||
### Publish
|
||||
|
||||
Prepare a skill package directory, then publish it:
|
||||
|
||||
```bash
|
||||
npx clawhub publish ./my-skill
|
||||
```
|
||||
|
||||
Publishing requires authentication and sufficient permissions in the target namespace.
|
||||
|
||||
## Authentication And Visibility
|
||||
|
||||
Download and search permissions depend on namespace and visibility:
|
||||
|
||||
- `@global` + `PUBLIC`: anonymous search, inspect, and download are allowed.
|
||||
- Team namespace + `PUBLIC`: authentication required for download.
|
||||
- `NAMESPACE_ONLY`: authenticated namespace members only.
|
||||
- `PRIVATE`: owner or explicitly authorized users only.
|
||||
- Publish, star, and other write operations always require authentication.
|
||||
|
||||
If a request fails with `403`, check:
|
||||
|
||||
- whether the skill belongs to a team namespace,
|
||||
- whether the skill is `NAMESPACE_ONLY` or `PRIVATE`,
|
||||
- whether your token is valid,
|
||||
- whether you have namespace publish permissions.
|
||||
|
||||
## Skill Package Contract
|
||||
|
||||
SkillHub expects OpenSkills-style packages with `SKILL.md` as the entry point.
|
||||
|
||||
Minimum valid `SKILL.md` frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-skill
|
||||
description: When to use this skill
|
||||
---
|
||||
```
|
||||
|
||||
Required structure:
|
||||
|
||||
```text
|
||||
my-skill/
|
||||
├── SKILL.md
|
||||
├── references/
|
||||
├── scripts/
|
||||
└── assets/
|
||||
```
|
||||
|
||||
Contract notes:
|
||||
|
||||
- `name` and `description` are required.
|
||||
- `name` becomes the immutable skill slug on first publish.
|
||||
- `description` becomes the registry summary.
|
||||
- `references/`, `scripts/`, and `assets/` are optional.
|
||||
- The package is treated as a text-first resource bundle, not a binary artifact bucket.
|
||||
|
||||
## Publishing Guidance
|
||||
|
||||
Before publishing:
|
||||
|
||||
1. Ensure `SKILL.md` exists at the package root.
|
||||
2. Keep the skill name in kebab-case.
|
||||
3. Make sure the version you are publishing is semver-compatible.
|
||||
4. Avoid relying on `latest` as a rollback tool; SkillHub keeps `latest` automatically pinned to the newest published version.
|
||||
5. Use custom tags like `beta` or `stable` for release channels when needed.
|
||||
|
||||
## When To Use Raw HTTP
|
||||
|
||||
Use direct HTTP only for server debugging, contract testing, or compatibility work. Relevant endpoints exposed by the current codebase include:
|
||||
|
||||
- `GET /.well-known/clawhub.json`
|
||||
- `GET /api/v1/search`
|
||||
- `GET /api/v1/resolve`
|
||||
- `GET /api/v1/download/{slug}`
|
||||
- `GET /api/v1/skills/{slug}`
|
||||
- `POST /api/v1/publish`
|
||||
- `GET /api/v1/whoami`
|
||||
|
||||
For normal registry usage, stay on the `clawhub` CLI.
|
||||
|
||||
## Project References
|
||||
|
||||
Read these local documents when you need more detail about SkillHub behavior:
|
||||
|
||||
- `docs/00-product-direction.md`
|
||||
- `docs/06-api-design.md`
|
||||
- `docs/07-skill-protocol.md`
|
||||
- `docs/14-skill-lifecycle.md`
|
||||
- `docs/openclaw-integration.md`
|
||||
- `README.md`
|
||||
|
|
@ -99,8 +99,20 @@
|
|||
"quickStart": {
|
||||
"title": "Quick Start",
|
||||
"subtitle": "快速开始",
|
||||
"description": "Get started with SkillHub in just a few simple steps",
|
||||
"description": "Choose how you want to work, then copy the setup instruction and continue",
|
||||
"tip": "💡 Tip: Visit the skill detail page for one-click install commands with environment variables",
|
||||
"tabs": {
|
||||
"agent": "I am Agent",
|
||||
"human": "I am Human"
|
||||
},
|
||||
"agent": {
|
||||
"description": "Send a prompt to your Agent to set up the SkillHub Registry",
|
||||
"command": "Read https://www.example.com/registry/skill and follow the instructions to setup SkillHub Skills Registry"
|
||||
},
|
||||
"human": {
|
||||
"description": "Use the CLI tool to install Skills",
|
||||
"command": "npx clawhub search <keyword>"
|
||||
},
|
||||
"steps": {
|
||||
"configureEnv": {
|
||||
"title": "1. Configure Environment Variables",
|
||||
|
|
|
|||
|
|
@ -99,8 +99,20 @@
|
|||
"quickStart": {
|
||||
"title": "快速开始",
|
||||
"subtitle": "Quick Start",
|
||||
"description": "只需几个简单步骤,即可开始使用 SkillHub",
|
||||
"description": "选择你的使用方式,复制说明后继续完成接入",
|
||||
"tip": "💡 提示:访问技能详情页可获取带环境变量的一键复制安装命令",
|
||||
"tabs": {
|
||||
"agent": "我是 Agent",
|
||||
"human": "我是 Human"
|
||||
},
|
||||
"agent": {
|
||||
"description": "发送提示词给你的 Agent,以设置SkillHub Registry",
|
||||
"command": "Read https://www.example.com/registry/skill and follow the instructions to setup SkillHub Skills Registry"
|
||||
},
|
||||
"human": {
|
||||
"description": "使用CLI工具安装Skills",
|
||||
"command": "npx clawhub search <keyword>"
|
||||
},
|
||||
"steps": {
|
||||
"configureEnv": {
|
||||
"title": "1. 配置环境变量",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Link, useNavigate } from '@tanstack/react-router'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { normalizeSearchQuery } from '@/shared/lib/search-query'
|
||||
import { PackageOpen, Terminal, Shield, Users, GitBranch, Search as SearchIcon, Settings } from 'lucide-react'
|
||||
import { QuickStartSection } from '@/shared/components/quick-start'
|
||||
import { LandingQuickStartSection } from '@/shared/components/landing-quick-start'
|
||||
import { SkillCard } from '@/features/skill/skill-card'
|
||||
import { SkeletonList } from '@/shared/components/skeleton-loader'
|
||||
import { useSearchSkills } from '@/shared/hooks/use-skill-queries'
|
||||
|
|
@ -195,7 +195,7 @@ export function LandingPage() {
|
|||
|
||||
{/* Quick Start */}
|
||||
<div ref={quickStartView.ref} className={`scroll-fade-up${quickStartView.inView ? ' in-view' : ''}`}>
|
||||
<QuickStartSection variant="landing" ns="landing" />
|
||||
<LandingQuickStartSection />
|
||||
</div>
|
||||
|
||||
{/* Popular Downloads Section */}
|
||||
|
|
|
|||
9
web/src/pages/registry-skill.tsx
Normal file
9
web/src/pages/registry-skill.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import skillContent from '@/docs/skill.md?raw'
|
||||
|
||||
export function RegistrySkillPage() {
|
||||
return (
|
||||
<pre className="whitespace-pre-wrap break-words p-0 m-0 font-mono text-sm">
|
||||
{skillContent}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
136
web/src/shared/components/landing-quick-start.tsx
Normal file
136
web/src/shared/components/landing-quick-start.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Bot, Check, Copy, UserRound } from 'lucide-react'
|
||||
|
||||
type LandingQuickStartTabId = 'agent' | 'human'
|
||||
|
||||
interface LandingQuickStartTab {
|
||||
id: LandingQuickStartTabId
|
||||
label: string
|
||||
description: string
|
||||
command: string
|
||||
}
|
||||
|
||||
function CompactCopyButton({ text }: { text: string }) {
|
||||
const { t } = useTranslation()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const label = copied ? (t('copyButton.copied') || 'Copied') : (t('copyButton.copy') || 'Copy')
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
className="absolute right-2.5 top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-xl border bg-white transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer"
|
||||
style={{ borderColor: 'hsl(var(--border))', color: 'hsl(var(--foreground))' }}
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function LandingQuickStartSection() {
|
||||
const { t } = useTranslation()
|
||||
const [activeTab, setActiveTab] = useState<LandingQuickStartTabId>('agent')
|
||||
|
||||
const tabs: LandingQuickStartTab[] = [
|
||||
{
|
||||
id: 'agent',
|
||||
label: t('landing.quickStart.tabs.agent'),
|
||||
description: t('landing.quickStart.agent.description'),
|
||||
command: t('landing.quickStart.agent.command'),
|
||||
},
|
||||
{
|
||||
id: 'human',
|
||||
label: t('landing.quickStart.tabs.human'),
|
||||
description: t('landing.quickStart.human.description'),
|
||||
command: t('landing.quickStart.human.command'),
|
||||
},
|
||||
]
|
||||
|
||||
const currentTab = tabs.find((tab) => tab.id === activeTab) ?? tabs[0]
|
||||
|
||||
return (
|
||||
<section className="relative z-10 w-full px-6 py-14 md:py-16" style={{ background: 'var(--bg-page, hsl(var(--background)))' }}>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-7 md:mb-8">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight mb-3" style={{ color: 'hsl(var(--foreground))' }}>
|
||||
{t('landing.quickStart.title')}
|
||||
</h2>
|
||||
<p className="text-base md:text-lg max-w-2xl mx-auto leading-relaxed" style={{ color: 'hsl(var(--text-secondary))' }}>
|
||||
{t('landing.quickStart.description', { defaultValue: t('landing.quickStart.subtitle') })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mx-auto max-w-2xl rounded-[28px] border bg-white p-3 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.25)]"
|
||||
style={{ borderColor: 'hsl(var(--border-card))' }}
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-2 gap-2 rounded-2xl p-1.5"
|
||||
style={{ background: 'linear-gradient(180deg, rgba(248,250,252,0.98) 0%, rgba(241,245,249,0.92) 100%)' }}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === currentTab.id
|
||||
const Icon = tab.id === 'agent' ? Bot : UserRound
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
aria-pressed={isActive}
|
||||
className="flex min-h-11 items-center justify-center gap-2 rounded-[14px] px-4 py-3 text-base font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer"
|
||||
style={{
|
||||
background: isActive ? 'rgba(255,255,255,0.96)' : 'transparent',
|
||||
color: isActive ? 'hsl(var(--foreground))' : 'hsl(var(--muted-foreground))',
|
||||
boxShadow: isActive ? '0 6px 18px rgba(15, 23, 42, 0.08)' : 'none',
|
||||
}}
|
||||
>
|
||||
<Icon className="h-4 w-4" strokeWidth={1.75} />
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4 pt-8 md:px-8 md:pb-6 md:pt-9">
|
||||
<p
|
||||
className="mx-auto mb-6 max-w-xl text-center text-xl font-medium leading-relaxed md:text-[1.75rem]"
|
||||
style={{ color: 'hsl(var(--foreground))' }}
|
||||
>
|
||||
{currentTab.description}
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="relative rounded-2xl border bg-slate-50/90 px-4 py-3 pr-14 shadow-[inset_0_1px_0_rgba(255,255,255,0.7)]"
|
||||
style={{ borderColor: 'hsl(var(--border))' }}
|
||||
>
|
||||
<div className="overflow-x-auto whitespace-nowrap">
|
||||
<code
|
||||
className="font-mono text-sm md:text-base"
|
||||
style={{ color: currentTab.id === 'agent' ? '#16A34A' : '#0F172A' }}
|
||||
>
|
||||
{currentTab.command}
|
||||
</code>
|
||||
</div>
|
||||
<CompactCopyButton text={currentTab.command} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
4
web/src/types/markdown.d.ts
vendored
Normal file
4
web/src/types/markdown.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.md?raw' {
|
||||
const value: string
|
||||
export default value
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue