diff --git a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx index 1f3f303d..9abe28f2 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -54,6 +54,7 @@ import { useInstallSkillUpload, useSetSkillPinned, useSetSkillPublished, + useSkillContent, useSkills, useValidateSkillUpload, } from "@/core/skills/hooks"; @@ -256,6 +257,7 @@ function SkillRow({ const setPinned = useSetSkillPinned(); const deleteSkill = useDeleteCustomSkill(); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + const [detailOpen, setDetailOpen] = useState(false); const isCustom = skill.category === "custom"; // Built-in flag falls back to "no DB owner" so legacy responses without @@ -306,7 +308,10 @@ function SkillRow({ return ( <> - + setDetailOpen(true)} + >
{skill.name} @@ -471,10 +476,55 @@ function SkillRow({ + ); } +function SkillDetailDialog({ + skill, + open, + onOpenChange, +}: { + skill: Skill; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const { t } = useI18n(); + const { data, isLoading, error } = useSkillContent(open ? skill.name : null); + + return ( + + + + {skill.name} + {skill.description} + +
+ {isLoading ? ( +
+ + {t.common.loading} +
+ ) : error ? ( +
+ {error instanceof Error ? error.message : String(error)} +
+ ) : ( +
+              {data?.content}
+            
+ )} +
+
+
+ ); +} + function EmptySkill({ onCreateSkill }: { onCreateSkill: () => void }) { const { t } = useI18n(); return ( diff --git a/frontend-web/src/core/skills/api.ts b/frontend-web/src/core/skills/api.ts index e4b953e0..4991425e 100644 --- a/frontend-web/src/core/skills/api.ts +++ b/frontend-web/src/core/skills/api.ts @@ -174,6 +174,24 @@ export async function setSkillPinned( return response.json() as Promise; } +export interface SkillContentResponse { + name: string; + category: string; + content: string; +} + +export async function getSkillContent( + skillName: string, +): Promise { + const response = await apiFetch( + `${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/content`, + ); + if (!response.ok) { + throw await parseDetailedError(response, "Failed to load skill content"); + } + return response.json() as Promise; +} + export async function deleteCustomSkill(skillName: string): Promise { const response = await apiFetch( `${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}`, diff --git a/frontend-web/src/core/skills/hooks.ts b/frontend-web/src/core/skills/hooks.ts index bb341b67..68caef26 100644 --- a/frontend-web/src/core/skills/hooks.ts +++ b/frontend-web/src/core/skills/hooks.ts @@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { deleteCustomSkill, enableSkill, + getSkillContent, installSkillUpload, setSkillPinned, setSkillPublished, @@ -21,6 +22,14 @@ export function useSkills() { return { skills: data ?? [], isLoading, error }; } +export function useSkillContent(skillName: string | null) { + return useQuery({ + queryKey: ["skill-content", skillName], + queryFn: () => getSkillContent(skillName!), + enabled: !!skillName, + }); +} + export function useEnableSkill() { const queryClient = useQueryClient(); return useMutation({ diff --git a/offline-backend-20260512/backend/app/gateway/routers/skills.py b/offline-backend-20260512/backend/app/gateway/routers/skills.py index f4ccda17..5159d040 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/skills.py +++ b/offline-backend-20260512/backend/app/gateway/routers/skills.py @@ -816,6 +816,36 @@ async def get_skill(skill_name: str, config: AppConfig = Depends(get_config)) -> raise HTTPException(status_code=500, detail=f"Failed to get skill: {str(e)}") +class SkillContentResponse(BaseModel): + """SKILL.md 原文内容,适用于公共和自定义技能。""" + + name: str + category: SkillCategory + content: str + + +@router.get( + "/skills/{skill_name}/content", + response_model=SkillContentResponse, + summary="Get Skill Markdown Content", + description="Retrieve the raw SKILL.md content of a specific skill (public or custom).", +) +async def get_skill_content(skill_name: str, config: AppConfig = Depends(get_config)) -> SkillContentResponse: + try: + skill_name = skill_name.replace("\r\n", "").replace("\n", "") + skills = get_or_new_skill_storage(app_config=config).load_skills(enabled_only=False) + skill = next((s for s in skills if s.name == skill_name), None) + if skill is None: + raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found") + content = skill.skill_file.read_text(encoding="utf-8") + return SkillContentResponse(name=skill.name, category=skill.category, content=content) + except HTTPException: + raise + except Exception as e: + logger.error("Failed to get skill content %s: %s", skill_name, e, exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to get skill content: {str(e)}") + + @router.put( "/skills/{skill_name}", response_model=SkillResponse,