mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-07 02:57:51 +00:00
feat(skill): add install-for-agent prompt
Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
This commit is contained in:
parent
ac42c2346c
commit
769b03ee39
6 changed files with 133 additions and 0 deletions
49
web/src/features/skill/install-for-agent-button.test.tsx
Normal file
49
web/src/features/skill/install-for-agent-button.test.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { createElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { InstallForAgentButton, buildAgentInstallPrompt } from './install-for-agent-button'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('install-for-agent-button', () => {
|
||||
const formatPrompt = (guideUrl: string, skill: string) => (
|
||||
`Please follow [the guide](${guideUrl}) to install and use ${skill}.`
|
||||
)
|
||||
|
||||
it('builds a prompt for a global skill using the instance guide', () => {
|
||||
expect(buildAgentInstallPrompt('global', 'my-skill', 'https://skill.example.com', formatPrompt)).toBe(
|
||||
'Please follow [the guide](https://skill.example.com/registry/skill.md) to install and use my-skill.',
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the namespace in the coordinate for an agent to resolve', () => {
|
||||
expect(buildAgentInstallPrompt('team-alpha', 'my-skill', 'https://skill.example.com/', formatPrompt)).toBe(
|
||||
'Please follow [the guide](https://skill.example.com/registry/skill.md) to install and use @team-alpha/my-skill.',
|
||||
)
|
||||
})
|
||||
|
||||
it('renders an accessible copy button', () => {
|
||||
const html = renderToStaticMarkup(createElement(InstallForAgentButton, {
|
||||
namespace: 'global',
|
||||
slug: 'my-skill',
|
||||
}))
|
||||
|
||||
expect(html).toContain('data-testid="install-for-agent-button"')
|
||||
expect(html).toContain('aria-label="skillDetail.installForAgent.button"')
|
||||
expect(html).toContain('skillDetail.installForAgent.button')
|
||||
})
|
||||
|
||||
it('can be disabled when the selected skill version is not installable', () => {
|
||||
const html = renderToStaticMarkup(createElement(InstallForAgentButton, {
|
||||
namespace: 'global',
|
||||
slug: 'my-skill',
|
||||
disabled: true,
|
||||
}))
|
||||
|
||||
expect(html).toContain('disabled=""')
|
||||
})
|
||||
})
|
||||
62
web/src/features/skill/install-for-agent-button.tsx
Normal file
62
web/src/features/skill/install-for-agent-button.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Bot, Check } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCopyToClipboard } from '@/shared/lib/clipboard'
|
||||
import { getBaseUrl } from './install-command'
|
||||
|
||||
interface InstallForAgentButtonProps {
|
||||
namespace: string
|
||||
slug: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type FormatAgentPrompt = (guideUrl: string, skill: string) => string
|
||||
|
||||
export function buildAgentInstallPrompt(
|
||||
namespace: string,
|
||||
slug: string,
|
||||
baseUrl: string,
|
||||
formatPrompt: FormatAgentPrompt,
|
||||
): string {
|
||||
const skill = namespace === 'global' ? slug : `@${namespace}/${slug}`
|
||||
const guideUrl = `${baseUrl.replace(/\/+$/, '')}/registry/skill.md`
|
||||
|
||||
return formatPrompt(guideUrl, skill)
|
||||
}
|
||||
|
||||
export function InstallForAgentButton({ namespace, slug, disabled = false }: InstallForAgentButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const [copied, copy] = useCopyToClipboard()
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await copy(buildAgentInstallPrompt(
|
||||
namespace,
|
||||
slug,
|
||||
getBaseUrl(),
|
||||
(guideUrl, skill) => t('skillDetail.installForAgent.prompt', { guideUrl, skill }),
|
||||
))
|
||||
} catch (err) {
|
||||
console.error('Failed to copy agent installation prompt:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const label = copied
|
||||
? t('skillDetail.installForAgent.copied')
|
||||
: t('skillDetail.installForAgent.button')
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="install-for-agent-button"
|
||||
onClick={handleCopy}
|
||||
disabled={disabled}
|
||||
aria-label={label}
|
||||
className="relative w-full overflow-hidden rounded-xl border border-border/60 bg-muted/50 px-4 py-3 transition-colors hover:bg-muted/70 active:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{copied ? <Check className="h-4 w-4" /> : <Bot className="h-4 w-4" />}
|
||||
<span className="text-[13px] leading-relaxed text-foreground sm:text-sm">{label}</span>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
@ -1013,6 +1013,11 @@
|
|||
"button": "Share",
|
||||
"copied": "Copied",
|
||||
"defaultDescription": "A useful skill"
|
||||
},
|
||||
"installForAgent": {
|
||||
"button": "Install for Agent",
|
||||
"copied": "Agent prompt copied",
|
||||
"prompt": "Please follow the [SkillHub skill installation guide]({{guideUrl}}) to install and use {{skill}}."
|
||||
}
|
||||
},
|
||||
"skillCompare": {
|
||||
|
|
|
|||
|
|
@ -1078,6 +1078,11 @@
|
|||
"button": "Поделиться",
|
||||
"copied": "Скопировано",
|
||||
"defaultDescription": "Полезный скилл"
|
||||
},
|
||||
"installForAgent": {
|
||||
"button": "Установить для агента",
|
||||
"copied": "Инструкция скопирована",
|
||||
"prompt": "Следуйте [инструкции SkillHub по установке скиллов]({{guideUrl}}), чтобы установить и использовать {{skill}}."
|
||||
}
|
||||
},
|
||||
"skillCompare": {
|
||||
|
|
|
|||
|
|
@ -1013,6 +1013,11 @@
|
|||
"button": "分享",
|
||||
"copied": "已复制",
|
||||
"defaultDescription": "实用技能"
|
||||
},
|
||||
"installForAgent": {
|
||||
"button": "为 Agent 安装",
|
||||
"copied": "安装指令已复制",
|
||||
"prompt": "请根据 [SkillHub 技能安装指南]({{guideUrl}}),安装并使用 {{skill}}。"
|
||||
}
|
||||
},
|
||||
"skillCompare": {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type { FileTreeNode } from '@/features/skill/file-tree-builder'
|
|||
import type { SkillFile } from '@/api/types'
|
||||
import { InstallCommand } from '@/features/skill/install-command'
|
||||
import { ShareButton } from '@/features/skill/share-button'
|
||||
import { InstallForAgentButton } from '@/features/skill/install-for-agent-button'
|
||||
import { SkillLabelPanel } from '@/features/skill/skill-label-panel'
|
||||
import { ComplianceSnapshotPanel } from '@/features/skill/compliance-snapshot-panel'
|
||||
import {
|
||||
|
|
@ -1253,6 +1254,12 @@ export function SkillDetailPage() {
|
|||
description={skill.summary}
|
||||
/>
|
||||
|
||||
<InstallForAgentButton
|
||||
namespace={namespace}
|
||||
slug={slug}
|
||||
disabled={!selectedVersionEntry || skill.status === 'ARCHIVED' || !isVersionDownloadable}
|
||||
/>
|
||||
|
||||
{skill.canManageLifecycle && selectedVersionEntry && (
|
||||
<SecurityAuditSummary skillId={skill.id} versionId={selectedVersionEntry.id} versionStatus={selectedVersionEntry.status} />
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue