fix(web): preserve registry in agent onboarding

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-04 12:45:00 +08:00
parent 400240b5d6
commit 5fea369d60
8 changed files with 103 additions and 10 deletions

View file

@ -37,16 +37,44 @@ test.describe('Landing Quick Start CLI Tab (Real API)', () => {
const humanTab = page.getByRole('button', { name: 'I am Human', exact: true })
await expect(
page.getByText(/Connect SkillHub using .+\/install\/skillhub\.md/),
page.getByText(
'Connect SkillHub using http://127.0.0.1:3000/install/skillhub.md',
{ exact: true },
),
).toBeVisible()
const guideResponse = await page.request.get('/install/skillhub.md')
expect(guideResponse.status()).toBe(200)
const guide = await guideResponse.text()
expect(guide).toContain('http://127.0.0.1:3000')
expect(guideResponse.headers()['cache-control']).toContain('no-cache')
const legacyGuideResponse = await page.request.get('/registry/skill.md')
expect(legacyGuideResponse.status()).toBe(200)
expect(await legacyGuideResponse.text()).toBe(guide)
const hostileHostResponse = await page.request.get('/install/skillhub.md', {
headers: { Host: 'attacker.example' },
})
expect(hostileHostResponse.status()).toBe(403)
const extensionHostResponse = await page.request.get('/install/skillhub.md', {
headers: { Host: 'chrome-extension:evil;echo_injected' },
})
expect(extensionHostResponse.status()).toBe(400)
await humanTab.click()
await expect(humanTab).toHaveAttribute('aria-pressed', 'true')
await expect(
page.getByText('npx @astron-team/skillhub@latest search <keyword>', { exact: true }),
page.getByText(
'npx @astron-team/skillhub@latest search <keyword> --registry http://127.0.0.1:3000',
{ exact: true },
),
).toBeVisible()
await agentTab.click()
await expect(agentTab).toHaveAttribute('aria-pressed', 'true')
await expect(
page.getByText(
'Connect SkillHub using http://127.0.0.1:3000/install/skillhub.md',
{ exact: true },
),
).toBeVisible()
})
})

View file

@ -52,11 +52,23 @@ test.describe('Public Skill Detail Anonymous Access (Real API)', () => {
? current.skill.slug
: `${current.skill.namespace}--${current.skill.slug}`
const skillhubCoordinate = `@${current.skill.namespace}/${current.skill.slug}`
const registryUrl = new URL(page.url()).origin
await expect(page.getByRole('tab', { name: 'SkillHub CLI' })).toHaveAttribute('aria-selected', 'true')
await expect(page.getByText(new RegExp(`npx @astron-team/skillhub@latest install ${escapeRegExp(skillhubCoordinate)} --version ${escapeRegExp(current.skill.version)} --registry`))).toBeVisible()
await expect(page.getByText(
`npx @astron-team/skillhub@latest install ${skillhubCoordinate} --version ${current.skill.version} --registry ${registryUrl}`,
{ exact: true },
)).toBeVisible()
await expect(page.getByRole('button', { name: 'Copy' }).first()).toBeVisible()
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { origin: registryUrl })
await page.getByTestId('install-for-agent-button').click()
const agentPrompt = await page.evaluate(() => navigator.clipboard.readText())
expect(agentPrompt).toContain(`${registryUrl}/install/skillhub.md`)
expect(agentPrompt).toContain(skillhubCoordinate)
expect(agentPrompt).toContain(current.skill.version)
expect(agentPrompt).toContain('ask for my confirmation')
await page.getByRole('tab', { name: 'ClawHub CLI' }).click()
await expect(page.getByRole('tab', { name: 'ClawHub CLI' })).toHaveAttribute('aria-selected', 'true')

View file

@ -14,6 +14,9 @@ describe('landing quick start locales', () => {
expect(zh.landing.quickStart.agent.commandTemplate).toBe('请根据 {{url}} 接入 SkillHub')
expect(en.landing.quickStart.agent.commandTemplate).toBe('Connect SkillHub using {{url}}')
expect(ru.landing.quickStart.agent.commandTemplate).toBe('Подключите SkillHub по инструкции {{url}}')
expect(zh.landing.quickStart.human.commandTemplate).toContain('--registry {{url}}')
expect(en.landing.quickStart.human.commandTemplate).toContain('--registry {{url}}')
expect(ru.landing.quickStart.human.commandTemplate).toContain('--registry {{url}}')
})
it('exposes CLI install command in both locales', () => {

View file

@ -127,7 +127,8 @@
},
"human": {
"description": "Use the SkillHub CLI to search for and install Skills",
"command": "npx @astron-team/skillhub@latest search <keyword>"
"command": "npx @astron-team/skillhub@latest search <keyword> --registry https://www.example.com",
"commandTemplate": "npx @astron-team/skillhub@latest search <keyword> --registry {{url}}"
},
"steps": {
"configureEnv": {

View file

@ -127,7 +127,8 @@
},
"human": {
"description": "Используйте SkillHub CLI для поиска и установки Skills",
"command": "npx @astron-team/skillhub@latest search <keyword>"
"command": "npx @astron-team/skillhub@latest search <keyword> --registry https://www.example.com",
"commandTemplate": "npx @astron-team/skillhub@latest search <keyword> --registry {{url}}"
},
"steps": {
"configureEnv": {

View file

@ -127,7 +127,8 @@
},
"human": {
"description": "使用 SkillHub CLI 搜索和安装技能",
"command": "npx @astron-team/skillhub@latest search <keyword>"
"command": "npx @astron-team/skillhub@latest search <keyword> --registry https://www.example.com",
"commandTemplate": "npx @astron-team/skillhub@latest search <keyword> --registry {{url}}"
},
"steps": {
"configureEnv": {

View file

@ -74,6 +74,10 @@ export function LandingQuickStartSection() {
defaultValue: t('landing.quickStart.agent.command'),
url: `${baseUrl}/install/skillhub.md`,
})
const humanCommand = t('landing.quickStart.human.commandTemplate', {
defaultValue: t('landing.quickStart.human.command'),
url: baseUrl,
})
const tabs: LandingQuickStartTab[] = [
{
@ -86,7 +90,7 @@ export function LandingQuickStartSection() {
id: 'human',
label: t('landing.quickStart.tabs.human'),
description: t('landing.quickStart.human.description'),
command: t('landing.quickStart.human.command'),
command: humanCommand,
},
{
id: 'cli',

View file

@ -1,14 +1,57 @@
import { defineConfig } from 'vite'
import { readFileSync } from 'node:fs'
import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { validateBasePath } from './base-path-config'
const JS_BUILD_TARGET = 'es2020'
const LEGACY_BROWSER_TARGETS = ['chrome83', 'edge83', 'firefox78', 'safari14']
const basePath = validateBasePath(process.env.VITE_BASE_PATH ?? '/')
const guideTemplate = readFileSync(path.resolve(__dirname, 'src/docs/skill.md.template'), 'utf8')
const safeHostPattern = /^(?:[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\])(?::[0-9]{1,5})?$/
function installGuideDevPlugin(): Plugin {
const basePrefix = basePath === '/' ? '' : basePath.slice(0, -1)
const guidePaths = new Set([
`${basePrefix}/install/skillhub.md`,
`${basePrefix}/registry/skill.md`,
])
return {
name: 'skillhub-install-guide-dev',
configureServer(server) {
// Install after Vite's built-in Host check so an untrusted Host can never
// be reflected into CLI commands. originalUrl survives SPA/base rewrites.
return () => {
server.middlewares.use((request, response, next) => {
const requestPath = new URL(request.originalUrl ?? request.url ?? '/', 'http://localhost').pathname
if (!guidePaths.has(requestPath)) {
next()
return
}
const host = request.headers.host
if (!host || !safeHostPattern.test(host)) {
response.statusCode = 400
response.end('Invalid Host')
return
}
const publicBaseUrl = `http://${host}${basePrefix}`
const guide = guideTemplate.replaceAll('${SKILLHUB_PUBLIC_BASE_URL}', publicBaseUrl)
response.statusCode = 200
response.setHeader('Content-Type', 'text/markdown; charset=utf-8')
response.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
response.end(guide)
})
}
},
}
}
export default defineConfig({
base: validateBasePath(process.env.VITE_BASE_PATH ?? '/'),
plugins: [react()],
base: basePath,
plugins: [installGuideDevPlugin(), react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),