mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-09 03:17:52 +00:00
fix: stabilize promotion approval and documentation fallback
This commit is contained in:
parent
df5883ec04
commit
2b0bb25240
6 changed files with 106 additions and 19 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}} 个文件",
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
|
||||
<Tabs defaultValue="readme">
|
||||
<TabsList>
|
||||
<TabsTrigger value="readme">{t('skillDetail.tabReadme')}</TabsTrigger>
|
||||
<TabsTrigger value="readme">{t('skillDetail.tabOverview')}</TabsTrigger>
|
||||
<TabsTrigger value="files">{t('skillDetail.tabFiles')}</TabsTrigger>
|
||||
<TabsTrigger value="versions">{t('skillDetail.tabVersions')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="readme" className="mt-6">
|
||||
{readme ? (
|
||||
<Card className="p-8">
|
||||
<Card className="p-8 space-y-4">
|
||||
{documentationPath ? (
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
||||
{t('skillDetail.documentationSource', { path: documentationPath })}
|
||||
</div>
|
||||
) : null}
|
||||
<MarkdownRenderer content={readme} />
|
||||
</Card>
|
||||
) : readmeError ? (
|
||||
<Card className="p-8 text-muted-foreground text-center">
|
||||
{t('skillDetail.readmeUnavailable')}
|
||||
<Card className="p-8 text-center">
|
||||
<div className="text-base font-semibold text-foreground">{t('skillDetail.documentationUnavailableTitle')}</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{t('skillDetail.documentationUnavailable')}</p>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="p-8 text-muted-foreground text-center">
|
||||
{t('skillDetail.noReadme')}
|
||||
<Card className="p-8 space-y-4">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-foreground">{t('skillDetail.noDocumentationTitle')}</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{t('skillDetail.noDocumentationDescription')}</p>
|
||||
</div>
|
||||
{skill.summary ? (
|
||||
<div className="rounded-xl border border-border/60 bg-secondary/20 p-4">
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
||||
{t('skillDetail.summaryLabel')}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-foreground">{skill.summary}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('skillDetail.noDocumentationHint')}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -39,13 +39,9 @@ async function getSkillVersionDetail(namespace: string, slug: string, version: s
|
|||
return fetchJson<SkillVersionDetail>(`${WEB_API_PREFIX}/skills/${cleanNamespace}/${slug}/versions/${version}`)
|
||||
}
|
||||
|
||||
async function getSkillReadme(namespace: string, slug: string, version: string): Promise<string> {
|
||||
async function getSkillDocumentation(namespace: string, slug: string, version: string, path: string): Promise<string> {
|
||||
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<SkillSummary[]> {
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
33
web/src/shared/lib/skill-documentation.test.ts
Normal file
33
web/src/shared/lib/skill-documentation.test.ts
Normal file
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
18
web/src/shared/lib/skill-documentation.ts
Normal file
18
web/src/shared/lib/skill-documentation.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue