diff --git a/web/src/features/skill/install-command.test.ts b/web/src/features/skill/install-command.test.ts new file mode 100644 index 00000000..6ad22188 --- /dev/null +++ b/web/src/features/skill/install-command.test.ts @@ -0,0 +1,67 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { buildInstallCommand, buildInstallTarget, getBaseUrl } from './install-command' + +describe('install-command', () => { + const originalWindow = globalThis.window + + function setMockWindow(appBaseUrl?: string) { + const location = { + protocol: 'https:', + host: 'fallback.example.com', + } satisfies Pick + + Object.defineProperty(globalThis, 'window', { + configurable: true, + writable: true, + value: { + __SKILLHUB_RUNTIME_CONFIG__: { + appBaseUrl, + }, + location, + } satisfies { + location: Pick + } & { + __SKILLHUB_RUNTIME_CONFIG__: { + appBaseUrl?: string + } + }, + }) + } + + afterEach(() => { + if (originalWindow) { + Object.defineProperty(globalThis, 'window', { + configurable: true, + writable: true, + value: originalWindow, + }) + return + } + Reflect.deleteProperty(globalThis, 'window') + }) + + it('uses the plain slug for the global namespace', () => { + expect(buildInstallTarget('global', 'my-skill')).toBe('my-skill') + expect(buildInstallCommand('global', 'my-skill', 'https://skill.xfyun.cn')).toBe( + 'npx clawhub install my-skill --registry https://skill.xfyun.cn', + ) + }) + + it('prefixes non-global namespaces in the install target', () => { + expect(buildInstallTarget('team-alpha', 'my-skill')).toBe('team-alpha--my-skill') + expect(buildInstallCommand('team-alpha', 'my-skill', 'https://skill.xfyun.cn')).toBe( + 'npx clawhub install team-alpha--my-skill --registry https://skill.xfyun.cn', + ) + }) + + it('uses the runtime app base url when available', () => { + setMockWindow('https://app.example.com') + + expect(getBaseUrl()).toBe('https://app.example.com') + }) + + it('falls back to the browser origin when the app base url is missing', () => { + setMockWindow() + expect(getBaseUrl()).toBe('https://fallback.example.com') + }) +}) diff --git a/web/src/features/skill/install-command.tsx b/web/src/features/skill/install-command.tsx index ce9ef25a..124f4352 100644 --- a/web/src/features/skill/install-command.tsx +++ b/web/src/features/skill/install-command.tsx @@ -9,7 +9,11 @@ interface InstallCommandProps { version?: string } -function getAppBaseUrl(): string { +export function buildInstallTarget(namespace: string, slug: string): string { + return namespace === 'global' ? slug : `${namespace}--${slug}` +} + +export function getBaseUrl(): string { if (typeof window === 'undefined') { return '' } @@ -20,20 +24,18 @@ function getAppBaseUrl(): string { return `${window.location.protocol}//${window.location.host}` } -export function InstallCommand({ slug }: InstallCommandProps) { +export function buildInstallCommand(namespace: string, slug: string, baseUrl: string): string { + const installTarget = buildInstallTarget(namespace, slug) + return `npx clawhub install ${installTarget} --registry ${baseUrl}` +} + +export function InstallCommand({ namespace, slug }: InstallCommandProps) { const { t } = useTranslation() const [copied, setCopied] = useState(false) - const baseUrl = useMemo(() => getAppBaseUrl(), []) + const baseUrl = useMemo(() => getBaseUrl(), []) - const command = useMemo(() => { - const installCmd = `clawhub install ${slug} ` - // 如果是默认的 clawhub.ai 不需要环境变量,否则显示完整配置 - if (baseUrl && !baseUrl.includes('clawhub.ai') && !baseUrl.includes('localhost') && !baseUrl.includes('127.0.0.1')) { - return `CLAWHUB_SITE=${baseUrl} CLAWHUB_REGISTRY=${baseUrl} ${installCmd}` - } - return installCmd - }, [baseUrl, slug]) + const command = useMemo(() => buildInstallCommand(namespace, slug, baseUrl), [baseUrl, namespace, slug]) const handleCopy = async () => { try {