From 2b0bb252403b2008819c0f95afdf2bb58a1ac535 Mon Sep 17 00:00:00 2001 From: yun-zhi-ztl <15071461069@163.com> Date: Mon, 16 Mar 2026 15:51:21 +0800 Subject: [PATCH] fix: stabilize promotion approval and documentation fallback --- web/src/i18n/locales/en.json | 8 ++++ web/src/i18n/locales/zh.json | 8 ++++ web/src/pages/skill-detail.tsx | 42 +++++++++++++++---- web/src/shared/hooks/use-skill-queries.ts | 16 +++---- .../shared/lib/skill-documentation.test.ts | 33 +++++++++++++++ web/src/shared/lib/skill-documentation.ts | 18 ++++++++ 6 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 web/src/shared/lib/skill-documentation.test.ts create mode 100644 web/src/shared/lib/skill-documentation.ts diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index c27177a9..363aceda 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -497,10 +497,18 @@ "accessDenied": "Access Denied", "accessDeniedDesc": "You don't have permission to view this skill", "tabReadme": "README", + "tabOverview": "Overview", "tabFiles": "Files", "tabVersions": "Versions", "noReadme": "No README", "readmeUnavailable": "README is temporarily unavailable. Please try again later or re-upload this skill version.", + "documentationSource": "Source: {{path}}", + "documentationUnavailableTitle": "Documentation is unavailable", + "documentationUnavailable": "The documentation file could not be loaded. You can still inspect the package contents in the file list.", + "noDocumentationTitle": "No package documentation", + "noDocumentationDescription": "This skill version does not include a readable overview file.", + "noDocumentationHint": "Many skills only ship executable files. You can continue with the file list and version details below.", + "summaryLabel": "Summary", "noFiles": "No files", "noVersions": "No versions", "fileCount": "{{count}} files", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 065a0150..04327b33 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -497,10 +497,18 @@ "accessDenied": "访问被拒绝", "accessDeniedDesc": "您没有权限查看该技能", "tabReadme": "README", + "tabOverview": "概览", "tabFiles": "文件", "tabVersions": "版本", "noReadme": "暂无 README", "readmeUnavailable": "README 暂时不可用,请稍后重试或重新上传该技能版本。", + "documentationSource": "来源:{{path}}", + "documentationUnavailableTitle": "文档暂时不可用", + "documentationUnavailable": "当前无法读取这个技能版本的文档文件。你仍然可以在文件列表里查看包内容。", + "noDocumentationTitle": "这个版本没有概览文档", + "noDocumentationDescription": "该技能版本没有包含可直接展示的 README.md 或 SKILL.md。", + "noDocumentationHint": "很多技能包只包含可执行文件或配置文件,可以继续查看下方文件列表和版本信息。", + "summaryLabel": "摘要", "noFiles": "暂无文件", "noVersions": "暂无版本", "fileCount": "{{count}} 个文件", diff --git a/web/src/pages/skill-detail.tsx b/web/src/pages/skill-detail.tsx index 1bc192a2..8f8d5219 100644 --- a/web/src/pages/skill-detail.tsx +++ b/web/src/pages/skill-detail.tsx @@ -13,6 +13,7 @@ import { adminApi, ApiError, WEB_API_PREFIX } from '@/api/client' import { useSubmitSkillReport } from '@/features/report/use-skill-reports' import { formatLocalDateTime } from '@/shared/lib/date-time' import { formatCompactCount } from '@/shared/lib/number-format' +import { resolveDocumentationFilePath } from '@/shared/lib/skill-documentation' import { NamespaceBadge } from '@/shared/components/namespace-badge' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/shared/ui/tabs' import { Button } from '@/shared/ui/button' @@ -92,13 +93,16 @@ export function SkillDetailPage() { const selectedVersion = skill?.latestVersion ?? versions?.[0]?.version const selectedVersionEntry = versions?.find((version) => version.version === selectedVersion) ?? versions?.[0] const { data: files } = useSkillFiles(namespace, slug, selectedVersion) - const { data: readme, error: readmeError } = useSkillReadme(namespace, slug, selectedVersion) + const documentationPath = resolveDocumentationFilePath(files) + const { data: readme, error: readmeError } = useSkillReadme(namespace, slug, selectedVersion, documentationPath) const { data: diffSourceDetail } = useSkillVersionDetail(namespace, slug, diffSourceVersion ?? undefined) const { data: diffCompareDetail } = useSkillVersionDetail(namespace, slug, diffCompareVersion ?? undefined) const { data: diffSourceFiles } = useSkillFiles(namespace, slug, diffSourceVersion ?? undefined) const { data: diffCompareFiles } = useSkillFiles(namespace, slug, diffCompareVersion ?? undefined) - const { data: diffSourceReadme } = useSkillReadme(namespace, slug, diffSourceVersion ?? undefined) - const { data: diffCompareReadme } = useSkillReadme(namespace, slug, diffCompareVersion ?? undefined) + const diffSourceDocumentationPath = resolveDocumentationFilePath(diffSourceFiles) + const diffCompareDocumentationPath = resolveDocumentationFilePath(diffCompareFiles) + const { data: diffSourceReadme } = useSkillReadme(namespace, slug, diffSourceVersion ?? undefined, diffSourceDocumentationPath) + const { data: diffCompareReadme } = useSkillReadme(namespace, slug, diffCompareVersion ?? undefined, diffCompareDocumentationPath) const governanceVisible = hasRole('SKILL_ADMIN') || hasRole('SUPER_ADMIN') const isPendingPreview = skill?.viewingVersionStatus === 'PENDING_REVIEW' const canInteract = skill?.canInteract ?? true @@ -449,23 +453,43 @@ export function SkillDetailPage() { - {t('skillDetail.tabReadme')} + {t('skillDetail.tabOverview')} {t('skillDetail.tabFiles')} {t('skillDetail.tabVersions')} {readme ? ( - + + {documentationPath ? ( +
+ {t('skillDetail.documentationSource', { path: documentationPath })} +
+ ) : null}
) : readmeError ? ( - - {t('skillDetail.readmeUnavailable')} + +
{t('skillDetail.documentationUnavailableTitle')}
+

{t('skillDetail.documentationUnavailable')}

) : ( - - {t('skillDetail.noReadme')} + +
+
{t('skillDetail.noDocumentationTitle')}
+

{t('skillDetail.noDocumentationDescription')}

+
+ {skill.summary ? ( +
+
+ {t('skillDetail.summaryLabel')} +
+

{skill.summary}

+
+ ) : null} +
+ {t('skillDetail.noDocumentationHint')} +
)}
diff --git a/web/src/shared/hooks/use-skill-queries.ts b/web/src/shared/hooks/use-skill-queries.ts index dcf9e1cb..eab25453 100644 --- a/web/src/shared/hooks/use-skill-queries.ts +++ b/web/src/shared/hooks/use-skill-queries.ts @@ -39,13 +39,9 @@ async function getSkillVersionDetail(namespace: string, slug: string, version: s return fetchJson(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/versions/${version}`) } -async function getSkillReadme(namespace: string, slug: string, version: string): Promise { +async function getSkillDocumentation(namespace: string, slug: string, version: string, path: string): Promise { const cleanNamespace = namespace.startsWith('@') ? namespace.slice(1) : namespace - try { - return await fetchText(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/versions/${version}/file?path=SKILL.md`) - } catch { - return await fetchText(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/versions/${version}/file?path=README.md`) - } + return fetchText(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/versions/${version}/file?path=${encodeURIComponent(path)}`) } async function getMySkills(): Promise { @@ -144,11 +140,11 @@ export function useSkillFiles(namespace: string, slug: string, version?: string) }) } -export function useSkillReadme(namespace: string, slug: string, version?: string) { +export function useSkillReadme(namespace: string, slug: string, version?: string, path?: string | null) { return useQuery({ - queryKey: ['skills', namespace, slug, 'versions', version, 'readme'], - queryFn: () => getSkillReadme(namespace, slug, version!), - enabled: !!namespace && !!slug && !!version, + queryKey: ['skills', namespace, slug, 'versions', version, 'readme', path], + queryFn: () => getSkillDocumentation(namespace, slug, version!, path!), + enabled: !!namespace && !!slug && !!version && !!path, }) } diff --git a/web/src/shared/lib/skill-documentation.test.ts b/web/src/shared/lib/skill-documentation.test.ts new file mode 100644 index 00000000..9640e7e8 --- /dev/null +++ b/web/src/shared/lib/skill-documentation.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { resolveDocumentationFilePath } from '@/shared/lib/skill-documentation' +import type { SkillFile } from '@/api/types' + +function createFile(filePath: string): SkillFile { + return { + id: 1, + filePath, + fileSize: 1, + contentType: 'text/plain', + sha256: 'hash', + } +} + +describe('resolveDocumentationFilePath', () => { + it('prefers README over SKILL markdown when both exist', () => { + const files = [createFile('SKILL.md'), createFile('README.md')] + + expect(resolveDocumentationFilePath(files)).toBe('README.md') + }) + + it('falls back to SKILL markdown when readme is absent', () => { + const files = [createFile('SKILL.md'), createFile('_meta.json')] + + expect(resolveDocumentationFilePath(files)).toBe('SKILL.md') + }) + + it('returns null when the package has no documentation files', () => { + const files = [createFile('_meta.json'), createFile('icon.png')] + + expect(resolveDocumentationFilePath(files)).toBeNull() + }) +}) diff --git a/web/src/shared/lib/skill-documentation.ts b/web/src/shared/lib/skill-documentation.ts new file mode 100644 index 00000000..c0d276cb --- /dev/null +++ b/web/src/shared/lib/skill-documentation.ts @@ -0,0 +1,18 @@ +import type { SkillFile } from '@/api/types' + +const DOCUMENTATION_PREFERENCE = ['README.md', 'SKILL.md'] as const + +export function resolveDocumentationFilePath(files?: SkillFile[] | null): string | null { + if (!files || files.length === 0) { + return null + } + + const filePaths = new Set(files.map((file) => file.filePath)) + for (const preferredPath of DOCUMENTATION_PREFERENCE) { + if (filePaths.has(preferredPath)) { + return preferredPath + } + } + + return null +}