From 8f411d015069565d196683106e708be214445cc8 Mon Sep 17 00:00:00 2001 From: tanhuan Date: Fri, 6 Nov 2026 18:57:57 +0800 Subject: [PATCH] fix: publish Ascend article and clarify recipe details --- .../blog-20261104-ascend-support/index.md | 1 - docs/design/page-contracts.md | 2 +- src/features/catalog/DetailPage.tsx | 5 +- src/features/catalog/RecipeOverview.tsx | 57 ++++++++++------- src/features/catalog/styles.module.css | 37 ++++++++++- tests/e2e/catalog.spec.ts | 2 +- tests/e2e/detail-loading.spec.ts | 2 +- tests/e2e/recipe-overview.spec.ts | 61 ++++++++++++++++++- tests/pages/deployment.spec.ts | 4 +- tests/unit/blog-content.test.ts | 13 ++++ 10 files changed, 155 insertions(+), 29 deletions(-) diff --git a/content/blog/blog-20261104-ascend-support/index.md b/content/blog/blog-20261104-ascend-support/index.md index 1f2ea73..4cbb51a 100644 --- a/content/blog/blog-20261104-ascend-support/index.md +++ b/content/blog/blog-20261104-ascend-support/index.md @@ -5,7 +5,6 @@ summary: ModelSphere 模型目录新增昇腾部署方案。本文以 GLM-5.3-Fl kind: internal slug: ascend-support publishedAt: 2026-11-06 -draft: true updatedAt: 2026-11-06T17:19:00+08:00 evidenceIds: [] categoryId: performance diff --git a/docs/design/page-contracts.md b/docs/design/page-contracts.md index 290ee11..14f87d5 100644 --- a/docs/design/page-contracts.md +++ b/docs/design/page-contracts.md @@ -147,7 +147,7 @@ Catalog 卡片桌面最多 4 列、平板 2 列、手机 1 列;硬件使用 ch - `sort` 默认 `improvement-desc`,本期只比较已明确标注的源记录最佳负载百分比,保留条件和测量版本,不解释为统一吞吐性能排名;另支持 name-asc/name-desc/updated-desc。无值置后,同值按稳定 id。模型视图按“调优配方最佳提升”排序并展示对应 baseline、负载和版本,模型卡片仅显示可用配方数量,完整配方在模型详情和表格中展示,不复用默认配方卡片。 - 配方卡片:模型名称为主标题并进入模型详情,下方依次显示配方名称、算力卡型号和数量;后两项使用相同的小字号与弱化颜色,配方名称进入配方详情,禁止从配方名称推断硬件配置。 - `page`:默认 1,每页 12 条;先过滤再排序和分页。初始 HTML 提供完整精简列表与原生详情链接,增强成功后接管交互。 -- 配方页:`tab` 为 deployment / performance,缺省、旧 history 或未知值显示部署配置。切换保留配方身份、`version`、`dataVersion` 与 `returnTo`,支持分享、刷新及前进后退;方向键和 Home/End 移动 Tab 焦点,Enter/Space 激活。 +- 配方页:主标题显示模型名称(缺失时回退模型 ID),紧随其后突出目录提供的硬件型号与数量;配方自定义名称和当前版本降为辅助信息,浏览器标题同步包含模型与硬件,不从配方名称推断显卡配置。`tab` 为 deployment / performance,缺省、旧 history 或未知值显示部署配置。切换保留配方身份、`version`、`dataVersion` 与 `returnTo`,支持分享、刷新及前进后退;方向键和 Home/End 移动 Tab 焦点,Enter/Space 激活。 - 性能 Tab:`modelId`、`recipeId` 定位优化配方,`dataVersion` 固定目录提交;展示该目录中配方最新测量版本关联的首份报告,不支持任意配方比较或历史报告选择。参数不完整、记录不存在、无关联报告和目录版本不可用分别提示,不替换为其他记录。 - `returnTo`:模型、配方和报告页传递原目录查询,仅允许站内 `/models` 地址,保留搜索、筛选、视图、排序及页码;缺省或无效时返回 `/models`。报告的 `dataVersion` 不写回原目录查询。 - 旧 `/models/report?…`、`/models?benchmark={modelId}/{recipeId}…#benchmark` 和 `/models/recipe?…#benchmark` 链接以 history.replace 转入配方性能 Tab;移除返回地址中的旧 benchmark,避免返回循环。 diff --git a/src/features/catalog/DetailPage.tsx b/src/features/catalog/DetailPage.tsx index 4b5facb..d03fc93 100644 --- a/src/features/catalog/DetailPage.tsx +++ b/src/features/catalog/DetailPage.tsx @@ -9,6 +9,7 @@ import useIsBrowser from '@docusaurus/useIsBrowser'; import { useHistory, useLocation } from '@docusaurus/router'; import { config, + hardwareText, loadHistory, loadYaml, modelHref, @@ -202,7 +203,9 @@ function DetailContent({ kind }: { kind: 'model' | 'recipe' }) { kind === 'recipe' && recipe ? translate( { id: 'site.fc599ecea048', message: '{v0} · 配方详情' }, - { v0: recipe.variant.id }, + { + v0: `${recipe.model.displayName?.trim() || recipe.model.name} · ${translateCopy(hardwareText(recipe.variant))}`, + }, ) : (model?.displayName ?? translate({ id: 'site.f7121092ad68', message: '模型与配方详情' })) diff --git a/src/features/catalog/RecipeOverview.tsx b/src/features/catalog/RecipeOverview.tsx index e24e277..98cd56a 100644 --- a/src/features/catalog/RecipeOverview.tsx +++ b/src/features/catalog/RecipeOverview.tsx @@ -13,7 +13,7 @@ import { topologyLabel, type Recipe, } from './data'; -import { SourceLinks } from './components'; +import { Icon, SourceLinks } from './components'; import styles from './styles.module.css'; import GainMeter from './GainMeter'; @@ -40,6 +40,7 @@ export default function RecipeOverview({ i18n: { currentLocale }, } = useDocusaurusContext(); const { model, variant } = recipe; + const modelName = model.displayName?.trim() || model.name; const badge = role(recipe); const image = variant.image; const imageText = image @@ -101,28 +102,42 @@ export default function RecipeOverview({ aria-label={translate({ id: 'site.9770f40287d5', message: '配方概览' })} >
-
-

{variant.id}

-
- {isDefault(recipe) && ( - - {translate({ id: 'site.bb88a59baf57', message: '默认配方' })} +
+
+

{modelName}

+
+ {isDefault(recipe) && ( + + {translate({ id: 'site.bb88a59baf57', message: '默认配方' })} + + )} + + {badge === 'optimized' + ? translate({ id: 'site.c805b06152c6', message: '已调优' }) + : badge === 'baseline' + ? translate({ id: 'site.a7ac587ab52d', message: '基线' }) + : translate({ + id: 'site.8be3a0d72f4d', + message: '部署配方', + })} - )} - - {badge === 'optimized' - ? translate({ id: 'site.c805b06152c6', message: '已调优' }) - : badge === 'baseline' - ? translate({ id: 'site.a7ac587ab52d', message: '基线' }) - : translate({ id: 'site.8be3a0d72f4d', message: '部署配方' })} - +
+
+
+ + {translateCopy(hardwareText(variant))}
-

- {model.displayName ?? model.name}{' '} +

+ {translate({ id: 'catalog.recipeName', message: '配方' })}{' '} + {variant.id} {translate({ id: 'site.c40529702643', message: ' · 当前配方版本 ', diff --git a/src/features/catalog/styles.module.css b/src/features/catalog/styles.module.css index dd76d45..d3524b3 100644 --- a/src/features/catalog/styles.module.css +++ b/src/features/catalog/styles.module.css @@ -1322,16 +1322,51 @@ .recipeOverview { margin-top: 0; } +.recipeIdentity { + flex: 1; + min-width: 0; + max-width: 100%; +} +.recipeHeading { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 16px; +} .recipeTitle { + min-width: 0; margin: 0; font: 700 clamp(22px, 3vw, 32px) / 1.4 var(--font-heading); overflow-wrap: anywhere; } +.recipeHardwareSummary { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 8px 0 12px; + color: var(--color-text-primary); + font: 600 clamp(18px, 2vw, 22px) / 1.4 var(--font-heading); +} +.recipeHardwareSummary > svg { + flex: none; + width: 1em; + height: 1em; + margin-top: 0.2em; + color: var(--color-text-muted); +} +.recipeHardwareSummary > strong { + min-width: 0; + font: inherit; + overflow-wrap: anywhere; +} +.recipeMetadata { + overflow-wrap: anywhere; +} .recipeBadges { display: flex; flex-wrap: wrap; gap: 6px; - margin: 8px 0; + margin: 0; } .recipeBadges > span { font-size: 12px; diff --git a/tests/e2e/catalog.spec.ts b/tests/e2e/catalog.spec.ts index 551536e..39e80cc 100644 --- a/tests/e2e/catalog.spec.ts +++ b/tests/e2e/catalog.spec.ts @@ -846,7 +846,7 @@ for (const layout of ['cards', 'table']) baseline.commit, ); await expect(page.getByRole('heading', { level: 1 })).toHaveText( - variant.id, + m.displayName?.trim() || m.name, ); if (layout === 'cards') { await expect( diff --git a/tests/e2e/detail-loading.spec.ts b/tests/e2e/detail-loading.spec.ts index f8c2eb4..1856764 100644 --- a/tests/e2e/detail-loading.spec.ts +++ b/tests/e2e/detail-loading.spec.ts @@ -111,7 +111,7 @@ for (const kind of ['model', 'recipe'] as const) ); await expect(loading(page)).toHaveCount(0); await expect(page.getByRole('heading', { level: 1 })).toHaveText( - kind === 'model' ? (model.displayName ?? model.name) : recipe.id, + model.displayName?.trim() || model.name, ); expect(await page.locator('main').innerText()).not.toContain( 'JavaScript', diff --git a/tests/e2e/recipe-overview.spec.ts b/tests/e2e/recipe-overview.spec.ts index 08ecf4c..b4615ea 100644 --- a/tests/e2e/recipe-overview.spec.ts +++ b/tests/e2e/recipe-overview.spec.ts @@ -88,12 +88,51 @@ for (const width of [320, 1440]) await expect( page.getByRole('tabpanel', { name: '部署配置' }).locator('pre'), ).toBeVisible(); + const title = overview(page).getByRole('heading', { level: 1 }); + const hardware = overview(page) + .getByRole('group', { name: '硬件', exact: true }) + .locator('strong'); + const recipeName = overview(page) + .locator('p') + .filter({ hasText: '当前配方版本' }); + await expect(title).toHaveText('GLM 5.3 (NVFP4)'); + await expect(hardware).toHaveText('4 × B300'); + await expect(recipeName).toContainText(id); + await expect(recipeName).toContainText('1.0.4'); + await expect(page).toHaveTitle( + /GLM 5\.3 \(NVFP4\) · 4 × B300 · 配方详情/, + ); + const fontSize = (element: ReturnType) => + element.evaluate((node) => parseFloat(getComputedStyle(node).fontSize)); + expect(await fontSize(title)).toBeGreaterThan(await fontSize(hardware)); + expect(await fontSize(hardware)).toBeGreaterThan( + await fontSize(recipeName), + ); + expect((await hardware.boundingBox())!.y).toBeGreaterThan( + (await title.boundingBox())!.y, + ); + expect((await recipeName.boundingBox())!.y).toBeGreaterThan( + (await hardware.boundingBox())!.y, + ); await expect( overview(page).getByText('默认配方', { exact: true }), ).toBeVisible(); await expect( overview(page).getByText('已调优', { exact: true }), ).toBeVisible(); + const titleBox = (await title.boundingBox())!; + const hardwareBox = (await hardware.boundingBox())!; + for (const label of ['默认配方', '已调优']) { + const badgeBox = (await overview(page) + .getByText(label, { exact: true }) + .boundingBox())!; + expect(badgeBox.y + badgeBox.height).toBeLessThan(hardwareBox.y); + if (width >= 768) { + expect(badgeBox.x).toBeGreaterThan(titleBox.x + titleBox.width); + expect(badgeBox.y).toBeLessThan(titleBox.y + titleBox.height); + expect(badgeBox.y + badgeBox.height).toBeGreaterThan(titleBox.y); + } + } await expect(fact(page, '版本')).toContainText('1.0.4'); await expect(fact(page, '版本')).toContainText('2026-10-12'); await expect(fact(page, '引擎')).toHaveText('sglang'); @@ -161,8 +200,16 @@ for (const width of [320, 1440]) }); await baselineLink.click(); await expect(page.getByRole('heading', { level: 1 })).toHaveText( - 'sglang-tp8-b300-baseline', + 'GLM 5.3 (NVFP4)', ); + await expect( + overview(page) + .getByRole('group', { name: '硬件', exact: true }) + .locator('strong'), + ).toHaveText('8 × B300'); + await expect(overview(page).locator('p')).toContainText([ + 'sglang-tp8-b300-baseline', + ]); await expect( overview(page).getByText('基线', { exact: true }), ).toBeVisible(); @@ -189,6 +236,8 @@ test('missing fields stay missing and a measured zero or regression is preserved const catalog = parseCatalog(baseline); const model = catalog.models.find((m) => m.name === 'glm5.3')!; const recipe = model.variants.find((v) => v.id === id)!; + delete model.displayName; + recipe.requires = { gpuProduct: [] }; delete recipe.releaseDate; delete recipe.image; delete recipe.servedName; @@ -205,6 +254,16 @@ test('missing fields stay missing and a measured zero or regression is preserved ]; await mount(page, catalog); await page.goto(href); + await expect(overview(page).getByRole('heading', { level: 1 })).toHaveText( + 'glm5.3', + ); + await expect( + overview(page) + .getByRole('group', { name: '硬件', exact: true }) + .locator('strong'), + ).toHaveText('GPU 未指定'); + await expect(fact(page, '硬件')).toHaveText('GPU 未指定'); + await expect(page).toHaveTitle(/glm5\.3 · GPU 未指定 · 配方详情/); await expect(fact(page, '版本')).toContainText('日期未提供'); for (const label of ['镜像', '服务名称', 'Chart']) await expect(fact(page, label)).toHaveText('未提供'); diff --git a/tests/pages/deployment.spec.ts b/tests/pages/deployment.spec.ts index abef580..4c70e06 100644 --- a/tests/pages/deployment.spec.ts +++ b/tests/pages/deployment.spec.ts @@ -139,7 +139,9 @@ test('catalog filters, recipe tabs, legacy redirects and return links preserve t page.getByRole('tab', { name: '部署配置', selected: true }), ).toBeVisible(); await page.reload(); - await expect(page.getByRole('heading', { level: 1 })).toHaveText(recipe.id); + await expect(page.getByRole('heading', { level: 1 })).toHaveText( + model.displayName?.trim() || model.name, + ); await expect(page.getByRole('tab')).toHaveCount(2); await expect(page.getByRole('tab', { name: '版本历史' })).toHaveCount(0); await page.getByRole('tab', { name: '性能对照' }).click(); diff --git a/tests/unit/blog-content.test.ts b/tests/unit/blog-content.test.ts index 8505938..7779da0 100644 --- a/tests/unit/blog-content.test.ts +++ b/tests/unit/blog-content.test.ts @@ -92,6 +92,19 @@ async function withArticle( }); } describe('blog publication boundary', () => { + it('publishes the Ascend support article in the formal release', async () => { + const result = await readBlog(process.cwd(), { + ...release, + blogSource: 'content', + }); + expect(result.articles.map((article) => article.meta)).toContainEqual( + expect.objectContaining({ + id: 'blog-20261104-ascend-support', + kind: 'internal', + slug: 'ascend-support', + }), + ); + }); it('publishes the selected example blog in an explicitly enabled release', async () => { const mode = resolveBuildMode({ SITE_ORIGIN: 'https://example.test',