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 c43b2d1e..10e3d3d1 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -71,9 +71,11 @@ import { useDeleteCustomSkill, useDuplicateSkill, useGenerateMissingSkillNameZh, + useGenerateSkillNameZh, useInstallSkillUpload, useReconcileCustomSkills, useSetSkillFeatured, + useSetSkillNameZh, useSetSkillPinned, useSetSkillPublished, useSkills, @@ -1015,8 +1017,16 @@ function InstallSkillDialog({ const [validationError, setValidationError] = useState(null); const [isDragging, setIsDragging] = useState(false); const [targetCategory, setTargetCategory] = useState<"custom" | "public">("custom"); + // After a successful install the dialog switches to a "set Chinese name" step. + // `installedName` holds the just-installed skill name (null = still on the upload step). + const [installedName, setInstalledName] = useState(null); + const [nameZhDraft, setNameZhDraft] = useState(""); + // Optional model override for the "AI generate" button (null = configured default). + const [nameZhModel, setNameZhModel] = useState(null); const validateMutation = useValidateSkillUpload(); const installMutation = useInstallSkillUpload(); + const setNameZhMutation = useSetSkillNameZh(); + const generateNameZhMutation = useGenerateSkillNameZh(); const reset = useCallback(() => { setFile(null); @@ -1024,14 +1034,30 @@ function InstallSkillDialog({ setValidationError(null); setIsDragging(false); setTargetCategory("custom"); + setInstalledName(null); + setNameZhDraft(""); + setNameZhModel(null); if (inputRef.current) inputRef.current.value = ""; }, []); const handleClose = useCallback(() => { - if (validateMutation.isPending || installMutation.isPending) return; + if ( + validateMutation.isPending || + installMutation.isPending || + setNameZhMutation.isPending || + generateNameZhMutation.isPending + ) + return; reset(); onClose(); - }, [installMutation.isPending, onClose, reset, validateMutation.isPending]); + }, [ + generateNameZhMutation.isPending, + installMutation.isPending, + onClose, + reset, + setNameZhMutation.isPending, + validateMutation.isPending, + ]); const acceptFile = useCallback( async (selected: File) => { @@ -1087,15 +1113,47 @@ function InstallSkillDialog({ result.skill_name, ), ); - reset(); - onClose(); + // Stay open and advance to the optional "set Chinese name" step. + setInstalledName(result.skill_name); + setNameZhDraft(""); + setNameZhModel(null); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } }, - [dialog.installSuccess, dialog.installSuccessReplaced, file, installMutation, onClose, reset, targetCategory], + [dialog.installSuccess, dialog.installSuccessReplaced, file, installMutation, targetCategory], ); + const handleGenerateNameZh = useCallback(async () => { + if (!installedName) return; + try { + const result = await generateNameZhMutation.mutateAsync({ + skillName: installedName, + modelName: nameZhModel, + }); + setNameZhDraft(result.name_zh ?? ""); + toast.success(dialog.nameZhGenerated); + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }, [dialog.nameZhGenerated, generateNameZhMutation, installedName, nameZhModel]); + + const handleSaveNameZh = useCallback(async () => { + if (!installedName) return; + const trimmed = nameZhDraft.trim(); + try { + // Empty draft = keep whatever is stored (or none); just close. + if (trimmed) { + await setNameZhMutation.mutateAsync({ skillName: installedName, nameZh: trimmed }); + toast.success(dialog.nameZhSaved); + } + reset(); + onClose(); + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)); + } + }, [dialog.nameZhSaved, installedName, nameZhDraft, onClose, reset, setNameZhMutation]); + const isPublicConflict = validation?.conflict && validation.conflict_kind === "public"; const isCustomConflict = validation?.conflict && validation.conflict_kind === "custom"; // Admin targeting public directory can overwrite an existing public skill. @@ -1107,8 +1165,70 @@ function InstallSkillDialog({ !o && handleClose()}> - {dialog.title} + {installedName ? dialog.nameZhStepTitle : dialog.title} + + {installedName && ( + <> +
+

+ {dialog.nameZhStepHint.replace("{name}", desensitize(installedName))} +

+
+ setNameZhDraft(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void handleSaveNameZh(); + }} + placeholder={dialog.nameZhPlaceholder} + className="h-9 flex-1" + autoFocus + disabled={setNameZhMutation.isPending || generateNameZhMutation.isPending} + /> + + +
+
+ + + + + + )} + + {!installedName && ( + <>
{ @@ -1271,6 +1391,8 @@ function InstallSkillDialog({ {installMutation.isPending ? dialog.installing : dialog.install} + + )}
); diff --git a/frontend-web/src/core/i18n/locales/en-US.ts b/frontend-web/src/core/i18n/locales/en-US.ts index 8e9c2475..943dbcd5 100644 --- a/frontend-web/src/core/i18n/locales/en-US.ts +++ b/frontend-web/src/core/i18n/locales/en-US.ts @@ -598,6 +598,14 @@ export const enUS: Translations = { targetCategoryLabel: "Install to", categoryCustom: "Custom skill", categoryPublic: "Built-in (admin)", + nameZhStepTitle: "Set Chinese name", + nameZhStepHint: + "Skill '{name}' is installed. You can give it a Chinese display name, or let the model generate a default one (leave blank to use the English name).", + nameZhPlaceholder: "Enter a Chinese name (optional)", + nameZhGenerate: "AI generate", + nameZhSkip: "Skip", + nameZhGenerated: "Chinese name generated", + nameZhSaved: "Chinese name saved", }, }, skillEvolution: { diff --git a/frontend-web/src/core/i18n/locales/types.ts b/frontend-web/src/core/i18n/locales/types.ts index c1e053f9..4e0e4b65 100644 --- a/frontend-web/src/core/i18n/locales/types.ts +++ b/frontend-web/src/core/i18n/locales/types.ts @@ -523,6 +523,13 @@ export interface Translations { targetCategoryLabel: string; categoryCustom: string; categoryPublic: string; + nameZhStepTitle: string; + nameZhStepHint: string; + nameZhPlaceholder: string; + nameZhGenerate: string; + nameZhSkip: string; + nameZhGenerated: string; + nameZhSaved: string; }; }; skillEvolution: { diff --git a/frontend-web/src/core/i18n/locales/zh-CN.ts b/frontend-web/src/core/i18n/locales/zh-CN.ts index c0606781..9c1215c1 100644 --- a/frontend-web/src/core/i18n/locales/zh-CN.ts +++ b/frontend-web/src/core/i18n/locales/zh-CN.ts @@ -571,6 +571,14 @@ export const zhCN: Translations = { targetCategoryLabel: `安装到`, categoryCustom: `自定义技能`, categoryPublic: `内置技能(管理员)`, + nameZhStepTitle: "设置中文名", + nameZhStepHint: + "技能 “{name}” 已安装。你可以为它填写一个中文名,或让大模型自动生成一个默认中文名(留空则使用英文名)。", + nameZhPlaceholder: "请输入中文名(可留空)", + nameZhGenerate: "AI 生成", + nameZhSkip: "跳过", + nameZhGenerated: "已生成中文名称", + nameZhSaved: "已保存中文名", }, }, skillEvolution: { diff --git a/offline-backend-20260512/backend/app/gateway/routers/skills.py b/offline-backend-20260512/backend/app/gateway/routers/skills.py index 9bd72756..09f277da 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/skills.py +++ b/offline-backend-20260512/backend/app/gateway/routers/skills.py @@ -2444,11 +2444,11 @@ async def _generate_skill_name_zh(skill: Skill, app_config: AppConfig, model: An ], config={"run_name": "skill_name_zh"}, ), - timeout=30, + timeout=60, ) except asyncio.TimeoutError as e: logger.warning("Skill name_zh LLM call timed out for %s", skill.name) - raise SkillNameZhModelError("大模型响应超时(30 秒)") from e + raise SkillNameZhModelError("大模型响应超时(60 秒)") from e except Exception as e: logger.warning("Skill name_zh LLM call failed for %s", skill.name, exc_info=True) raise SkillNameZhModelError(str(e) or "大模型调用失败") from e