feat: add skill update detection via SHA-256 content hashing

- Add content_hash and updated_at fields to skills table (DB migration v6→v7)
- Compute directory content hash on install/import/restore for version tracking
- Add check_updates command: downloads repos, compares hashes, returns update list
- Add update_skill command: backs up old files, re-downloads and replaces SSOT
- Backfill content_hash for existing skills on first update check
- Add "Check Updates" button and per-skill update badge/button in UnifiedSkillsPanel
- Add i18n keys for zh/en/ja
This commit is contained in:
Jason
2026-04-05 19:19:01 +08:00
parent 46488ecd93
commit e3179ad9e4
13 changed files with 660 additions and 14 deletions
+44
View File
@@ -10,6 +10,7 @@ import {
type DiscoverableSkill,
type ImportSkillSelection,
type InstalledSkill,
type SkillUpdateInfo,
} from "@/lib/api/skills";
import type { AppId } from "@/lib/api/types";
@@ -283,6 +284,48 @@ export function useInstallSkillsFromZip() {
});
}
// ========== 更新检测 ==========
/**
* 检查 Skills 更新(手动触发)
*/
export function useCheckSkillUpdates() {
return useQuery({
queryKey: ["skills", "updates"],
queryFn: () => skillsApi.checkUpdates(),
enabled: false,
staleTime: 5 * 60 * 1000,
});
}
/**
* 更新单个 Skill
*/
export function useUpdateSkill() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => skillsApi.updateSkill(id),
onSuccess: (updatedSkill) => {
queryClient.setQueryData<InstalledSkill[]>(
["skills", "installed"],
(oldData) => {
if (!oldData) return [updatedSkill];
return oldData.map((s) =>
s.id === updatedSkill.id ? updatedSkill : s,
);
},
);
queryClient.setQueryData<SkillUpdateInfo[]>(
["skills", "updates"],
(oldData) => {
if (!oldData) return oldData;
return oldData.filter((u) => u.id !== updatedSkill.id);
},
);
},
});
}
// ========== 辅助类型 ==========
export type {
@@ -290,5 +333,6 @@ export type {
DiscoverableSkill,
ImportSkillSelection,
SkillBackupEntry,
SkillUpdateInfo,
AppId,
};