From 5fea369d60b9f89e5894cc5af1c4e952d0d26a99 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:45:00 +0800 Subject: [PATCH] fix(web): preserve registry in agent onboarding Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/e2e/landing-quick-start-cli.spec.ts | 32 +++++++++++- web/e2e/public-skill-detail-anonymous.spec.ts | 14 +++++- .../i18n/landing-quick-start-locale.test.ts | 3 ++ web/src/i18n/locales/en.json | 3 +- web/src/i18n/locales/ru.json | 3 +- web/src/i18n/locales/zh.json | 3 +- .../shared/components/landing-quick-start.tsx | 6 ++- web/vite.config.ts | 49 +++++++++++++++++-- 8 files changed, 103 insertions(+), 10 deletions(-) diff --git a/web/e2e/landing-quick-start-cli.spec.ts b/web/e2e/landing-quick-start-cli.spec.ts index 1e4ecd77..624fd3c6 100644 --- a/web/e2e/landing-quick-start-cli.spec.ts +++ b/web/e2e/landing-quick-start-cli.spec.ts @@ -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 ', { exact: true }), + page.getByText( + 'npx @astron-team/skillhub@latest search --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() }) }) diff --git a/web/e2e/public-skill-detail-anonymous.spec.ts b/web/e2e/public-skill-detail-anonymous.spec.ts index 753874d2..9a04293c 100644 --- a/web/e2e/public-skill-detail-anonymous.spec.ts +++ b/web/e2e/public-skill-detail-anonymous.spec.ts @@ -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') diff --git a/web/src/i18n/landing-quick-start-locale.test.ts b/web/src/i18n/landing-quick-start-locale.test.ts index 2d74ca92..4e987763 100644 --- a/web/src/i18n/landing-quick-start-locale.test.ts +++ b/web/src/i18n/landing-quick-start-locale.test.ts @@ -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', () => { diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 02a35cd7..5e3cb47c 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -127,7 +127,8 @@ }, "human": { "description": "Use the SkillHub CLI to search for and install Skills", - "command": "npx @astron-team/skillhub@latest search " + "command": "npx @astron-team/skillhub@latest search --registry https://www.example.com", + "commandTemplate": "npx @astron-team/skillhub@latest search --registry {{url}}" }, "steps": { "configureEnv": { diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index b6749e59..99017777 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -127,7 +127,8 @@ }, "human": { "description": "Используйте SkillHub CLI для поиска и установки Skills", - "command": "npx @astron-team/skillhub@latest search " + "command": "npx @astron-team/skillhub@latest search --registry https://www.example.com", + "commandTemplate": "npx @astron-team/skillhub@latest search --registry {{url}}" }, "steps": { "configureEnv": { diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 9c3720a6..9c51636f 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -127,7 +127,8 @@ }, "human": { "description": "使用 SkillHub CLI 搜索和安装技能", - "command": "npx @astron-team/skillhub@latest search " + "command": "npx @astron-team/skillhub@latest search --registry https://www.example.com", + "commandTemplate": "npx @astron-team/skillhub@latest search --registry {{url}}" }, "steps": { "configureEnv": { diff --git a/web/src/shared/components/landing-quick-start.tsx b/web/src/shared/components/landing-quick-start.tsx index e68a6a00..1bd88a4e 100644 --- a/web/src/shared/components/landing-quick-start.tsx +++ b/web/src/shared/components/landing-quick-start.tsx @@ -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', diff --git a/web/vite.config.ts b/web/vite.config.ts index 32566d1d..731ac030 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -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'),