diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index c4f9b79df5e..fad5d1281c9 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -372,6 +372,7 @@ _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [ _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [ "skill.view", "skill.edit", + "skill.publish", "skill.delete", "api_extension.manage", "plugin.install", diff --git a/web/features/skills/__tests__/detail-page.spec.tsx b/web/features/skills/__tests__/detail-page.spec.tsx index 63e58397b32..40d977a13f0 100644 --- a/web/features/skills/__tests__/detail-page.spec.tsx +++ b/web/features/skills/__tests__/detail-page.spec.tsx @@ -24,6 +24,7 @@ const mocks = vi.hoisted(() => ({ fetchSkillFileBlob: vi.fn(), checkDraftFilesMutationFn: vi.fn(), publishSkillMutationFn: vi.fn(), + publishSkillMutationOptions: vi.fn(), routerPush: vi.fn(), restoreSkillMutationFn: vi.fn(), saveDraftFileMutationFn: vi.fn(), @@ -214,7 +215,10 @@ vi.mock('@/service/client', () => ({ }, publish: { post: { - mutationOptions: () => ({ mutationFn: mocks.publishSkillMutationFn }), + mutationOptions: (options?: unknown) => { + mocks.publishSkillMutationOptions(options) + return { mutationFn: mocks.publishSkillMutationFn } + }, }, }, references: { @@ -2786,6 +2790,29 @@ describe('SkillDetailPage', () => { expect(screen.queryByText('已创建用于客户问题分级处理的 skill 草案')).not.toBeInTheDocument() }) + it('shows only the detailed backend error when publishing fails', async () => { + const user = userEvent.setup() + mocks.publishSkillMutationFn.mockRejectedValueOnce( + new Error('SKILL.md frontmatter name is required'), + ) + + renderSkillDetailPage() + + expect(mocks.publishSkillMutationOptions).toHaveBeenCalledWith({ + context: { silent: true }, + }) + await user.click( + await screen.findByRole('button', { + name: 'skill.skillManagement.detail.publishUpdate', + }), + ) + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('SKILL.md frontmatter name is required') + }) + expect(toast.error).toHaveBeenCalledTimes(1) + }) + it('shows a publish confirmation for referenced skills before publishing updates', async () => { const user = userEvent.setup() mocks.skillDetail = createSkillDetail({ reference_count: 1 }) diff --git a/web/features/skills/detail/page.tsx b/web/features/skills/detail/page.tsx index 47e5a5d58dd..3fea22bdbb9 100644 --- a/web/features/skills/detail/page.tsx +++ b/web/features/skills/detail/page.tsx @@ -23,6 +23,7 @@ import { getSkillVersionTitle, isDirectory, setSkillDetailCache, + showSkillErrorToast, } from './shared' import { DetailSkeleton } from './shell' import { RestoreVersionDialog, VersionPanel } from './version-panel' @@ -102,7 +103,9 @@ export function SkillDetailPage({ skillId }: { skillId: string }) { enabled: !!activeVersionId, }) const publishMutation = useMutation( - consoleQuery.workspaces.current.skills.bySkillId.publish.post.mutationOptions(), + consoleQuery.workspaces.current.skills.bySkillId.publish.post.mutationOptions({ + context: { silent: true }, + }), ) const restoreMutation = useMutation( consoleQuery.workspaces.current.skills.bySkillId.restore.post.mutationOptions(), @@ -265,8 +268,11 @@ export function SkillDetailPage({ skillId }: { skillId: string }) { queryKey: consoleQuery.workspaces.current.skills.get.key({ type: 'infinite' }), }) }, - onError: () => { - toast.error(t(($) => $['skillManagement.detail.publishFailed'])) + onError: (error) => { + showSkillErrorToast( + error, + t(($) => $['skillManagement.detail.publishFailed']), + ) }, }, )