{/* 消息列表 */}
diff --git a/src/components/skills/UnifiedSkillsPanel.tsx b/src/components/skills/UnifiedSkillsPanel.tsx
index ca5c719e1..8ba3599d5 100644
--- a/src/components/skills/UnifiedSkillsPanel.tsx
+++ b/src/components/skills/UnifiedSkillsPanel.tsx
@@ -110,7 +110,16 @@ const UnifiedSkillsPanel = React.forwardRef<
message: t("skills.uninstallConfirm", { name: skill.name }),
onConfirm: async () => {
try {
- const result = await uninstallMutation.mutateAsync(skill.id);
+ // 构建 skillKey 用于更新 discoverable 缓存
+ const installName =
+ skill.directory.split(/[/\\]/).pop()?.toLowerCase() ||
+ skill.directory.toLowerCase();
+ const skillKey = `${installName}:${skill.repoOwner?.toLowerCase() || ""}:${skill.repoName?.toLowerCase() || ""}`;
+
+ const result = await uninstallMutation.mutateAsync({
+ id: skill.id,
+ skillKey,
+ });
setConfirmDialog(null);
toast.success(t("skills.uninstallSuccess", { name: skill.name }), {
description: result.backupPath
diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts
index 1882010b9..f88804a6a 100644
--- a/src/config/opencodeProviderPresets.ts
+++ b/src/config/opencodeProviderPresets.ts
@@ -514,6 +514,33 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
},
},
+ {
+ name: "StepFun Step Plan",
+ websiteUrl: "https://platform.stepfun.com/docs/zh/step-plan/overview",
+ apiKeyUrl: "https://platform.stepfun.com/interface-key",
+ settingsConfig: {
+ npm: "@ai-sdk/openai-compatible",
+ name: "StepFun Step Plan",
+ options: {
+ baseURL: "https://api.stepfun.com/step_plan/v1",
+ apiKey: "",
+ setCacheKey: true,
+ },
+ models: {
+ "step-3.5-flash": { name: "Step 3.5 Flash" },
+ },
+ },
+ category: "cn_official",
+ icon: "stepfun",
+ iconColor: "#005AFF",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "step-...",
+ editorValue: "",
+ },
+ },
+ },
{
name: "ModelScope",
websiteUrl: "https://modelscope.cn",
diff --git a/src/hooks/useSkills.ts b/src/hooks/useSkills.ts
index 21b9153cb..c5a8250cd 100644
--- a/src/hooks/useSkills.ts
+++ b/src/hooks/useSkills.ts
@@ -1,4 +1,4 @@
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useMutation, useQuery, useQueryClient, keepPreviousData } from "@tanstack/react-query";
import {
skillsApi,
type SkillBackupEntry,
@@ -10,11 +10,15 @@ import type { AppId } from "@/lib/api/types";
/**
* 查询所有已安装的 Skills
+ * 使用 staleTime: Infinity 和 placeholderData: keepPreviousData
+ * 实现首次进入使用缓存,只有刷新时才重新获取
*/
export function useInstalledSkills() {
return useQuery({
queryKey: ["skills", "installed"],
queryFn: () => skillsApi.getInstalled(),
+ staleTime: Infinity,
+ placeholderData: keepPreviousData,
});
}
@@ -38,17 +42,21 @@ export function useDeleteSkillBackup() {
/**
* 发现可安装的 Skills(从仓库获取)
+ * 使用 staleTime: Infinity 和 placeholderData: keepPreviousData
+ * 实现首次进入使用缓存,只有刷新时才重新获取
*/
export function useDiscoverableSkills() {
return useQuery({
queryKey: ["skills", "discoverable"],
queryFn: () => skillsApi.discoverAvailable(),
- staleTime: Infinity, // 无限缓存,直到仓库变化时 invalidate
+ staleTime: Infinity,
+ placeholderData: keepPreviousData,
});
}
/**
* 安装 Skill
+ * 成功后直接更新缓存,不触发重新加载/刷新
*/
export function useInstallSkill() {
const queryClient = useQueryClient();
@@ -60,23 +68,76 @@ export function useInstallSkill() {
skill: DiscoverableSkill;
currentApp: AppId;
}) => skillsApi.installUnified(skill, currentApp),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["skills", "installed"] });
- queryClient.invalidateQueries({ queryKey: ["skills", "discoverable"] });
+ onSuccess: (installedSkill, _vars, _ctx) => {
+ const { skill } = _vars;
+ // 直接更新 installed 缓存
+ queryClient.setQueryData(
+ ["skills", "installed"],
+ (oldData) => {
+ if (!oldData) return [installedSkill];
+ return [...oldData, installedSkill];
+ },
+ );
+
+ // 更新 discoverable 缓存中对应技能的 installed 状态
+ const installName =
+ skill.directory.split(/[/\\]/).pop()?.toLowerCase() ||
+ skill.directory.toLowerCase();
+ const skillKey = `${installName}:${skill.repoOwner.toLowerCase()}:${skill.repoName.toLowerCase()}`;
+
+ queryClient.setQueryData(
+ ["skills", "discoverable"],
+ (oldData) => {
+ if (!oldData) return oldData;
+ return oldData.map((s) => {
+ if (s.key === skillKey) {
+ return { ...s, installed: true };
+ }
+ return s;
+ });
+ },
+ );
},
});
}
/**
* 卸载 Skill
+ * 成功后直接更新缓存,不触发重新加载/刷新
*/
export function useUninstallSkill() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (id: string) => skillsApi.uninstallUnified(id),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["skills", "installed"] });
- queryClient.invalidateQueries({ queryKey: ["skills", "discoverable"] });
+ mutationFn: ({
+ id,
+ skillKey,
+ }: {
+ id: string;
+ skillKey: string;
+ }) => skillsApi.uninstallUnified(id).then((result) => ({ ...result, skillKey })),
+ onSuccess: ({ skillKey }, _vars) => {
+ // 直接更新 installed 缓存,移除该 skill
+ queryClient.setQueryData(
+ ["skills", "installed"],
+ (oldData) => {
+ if (!oldData) return oldData;
+ return oldData.filter((s) => s.id !== _vars.id);
+ },
+ );
+
+ // 更新 discoverable 缓存中对应技能的 installed 状态
+ queryClient.setQueryData(
+ ["skills", "discoverable"],
+ (oldData) => {
+ if (!oldData) return oldData;
+ return oldData.map((s) => {
+ if (s.key === skillKey) {
+ return { ...s, installed: false };
+ }
+ return s;
+ });
+ },
+ );
},
});
}
@@ -132,14 +193,23 @@ export function useScanUnmanagedSkills() {
/**
* 从应用目录导入 Skills
+ * 成功后直接更新缓存,不触发重新加载/刷新
*/
export function useImportSkillsFromApps() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (imports: ImportSkillSelection[]) =>
skillsApi.importFromApps(imports),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["skills", "installed"] });
+ onSuccess: (importedSkills) => {
+ // 直接更新 installed 缓存
+ queryClient.setQueryData(
+ ["skills", "installed"],
+ (oldData) => {
+ if (!oldData) return importedSkills;
+ return [...oldData, ...importedSkills];
+ },
+ );
+ // 刷新 unmanaged 列表(已被导入的应该移除)
queryClient.invalidateQueries({ queryKey: ["skills", "unmanaged"] });
},
});
@@ -186,6 +256,7 @@ export function useRemoveSkillRepo() {
/**
* 从 ZIP 文件安装 Skills
+ * 成功后直接更新缓存,不触发重新加载/刷新
*/
export function useInstallSkillsFromZip() {
const queryClient = useQueryClient();
@@ -197,9 +268,15 @@ export function useInstallSkillsFromZip() {
filePath: string;
currentApp: AppId;
}) => skillsApi.installFromZip(filePath, currentApp),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["skills", "installed"] });
- queryClient.invalidateQueries({ queryKey: ["skills", "unmanaged"] });
+ onSuccess: (installedSkills) => {
+ // 直接更新 installed 缓存
+ queryClient.setQueryData(
+ ["skills", "installed"],
+ (oldData) => {
+ if (!oldData) return installedSkills;
+ return [...oldData, ...installedSkills];
+ },
+ );
},
});
}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index cce07e2a7..0a526bcf4 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -65,7 +65,8 @@
"hideAttribution": "Hide AI Attribution",
"enableTeammates": "Teammates Mode",
"enableToolSearch": "Enable Tool Search",
- "effortHigh": "High Effort Thinking"
+ "effortHigh": "High Effort Thinking",
+ "disableAutoUpgrade": "Disable Auto-Upgrade"
},
"header": {
"viewOnGithub": "View on GitHub",
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index dadbcef49..cd9804193 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -65,7 +65,8 @@
"hideAttribution": "AI署名を非表示",
"enableTeammates": "Teammates モード",
"enableToolSearch": "Tool Search を有効化",
- "effortHigh": "高強度思考"
+ "effortHigh": "高強度思考",
+ "disableAutoUpgrade": "自動アップグレードを無効化"
},
"header": {
"viewOnGithub": "GitHub で見る",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 464433268..a1f063263 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -65,7 +65,8 @@
"hideAttribution": "隐藏 AI 署名",
"enableTeammates": "Teammates 模式",
"enableToolSearch": "启用 Tool Search",
- "effortHigh": "高强度思考"
+ "effortHigh": "高强度思考",
+ "disableAutoUpgrade": "禁用自动升级"
},
"header": {
"viewOnGithub": "在 GitHub 上查看",
diff --git a/tests/components/UnifiedSkillsPanel.test.tsx b/tests/components/UnifiedSkillsPanel.test.tsx
index 5b766ed0d..9fb641c49 100644
--- a/tests/components/UnifiedSkillsPanel.test.tsx
+++ b/tests/components/UnifiedSkillsPanel.test.tsx
@@ -11,6 +11,8 @@ const toggleSkillAppMock = vi.fn();
const uninstallSkillMock = vi.fn();
const importSkillsMock = vi.fn();
const installFromZipMock = vi.fn();
+const deleteSkillBackupMock = vi.fn();
+const restoreSkillBackupMock = vi.fn();
vi.mock("sonner", () => ({
toast: {
@@ -25,9 +27,22 @@ vi.mock("@/hooks/useSkills", () => ({
data: [],
isLoading: false,
}),
+ useSkillBackups: () => ({
+ data: [],
+ refetch: vi.fn(),
+ isFetching: false,
+ }),
+ useDeleteSkillBackup: () => ({
+ mutateAsync: deleteSkillBackupMock,
+ isPending: false,
+ }),
useToggleSkillApp: () => ({
mutateAsync: toggleSkillAppMock,
}),
+ useRestoreSkillBackup: () => ({
+ mutateAsync: restoreSkillBackupMock,
+ isPending: false,
+ }),
useUninstallSkill: () => ({
mutateAsync: uninstallSkillMock,
}),
@@ -68,6 +83,8 @@ describe("UnifiedSkillsPanel", () => {
uninstallSkillMock.mockReset();
importSkillsMock.mockReset();
installFromZipMock.mockReset();
+ deleteSkillBackupMock.mockReset();
+ restoreSkillBackupMock.mockReset();
});
it("opens the import dialog without crashing when app toggles render", async () => {