Files
CC-Switch/src/hooks/useHermes.ts
T
Jason cdcc423122 refactor(hermes): drop config health check scanner
The Hermes config.yaml schema has stabilized and users have migrated to
the current provider fields, so the value of scanning for model.provider
dangling references, custom_providers shape errors, v12 migration residue
etc. no longer justifies the maintenance surface — and the scan produces
false positives when users keep some providers under Hermes' v12+
providers: dict (Hermes' runtime merges both shapes, but CC Switch's
scanner only looked at the list form).

Removes the whole HermesHealthWarning type, scan_hermes_config_health
command, HermesHealthBanner React component, useHermesHealth hook,
warnings field on HermesWriteOutcome, and the three helper functions
(yaml_as_non_empty_str, collect_mapping_string_keys, hermes_warning)
that only served the scanner. Drops the matching i18n keys in
zh/en/ja and the fixInWebUI button label that only the banner used.
2026-04-23 16:18:38 +08:00

175 lines
5.1 KiB
TypeScript

import { useCallback } from "react";
import {
useMutation,
useQuery,
useQueryClient,
type QueryClient,
} from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { hermesApi } from "@/lib/api/hermes";
import { providersApi } from "@/lib/api/providers";
import type { HermesMemoryKind } from "@/types";
import { extractErrorMessage } from "@/utils/errorUtils";
/**
* Error code returned by the Rust `open_hermes_web_ui` command when probing
* `/api/status` fails. Must match the string constant in
* `src-tauri/src/commands/hermes.rs`.
*/
export const HERMES_WEB_OFFLINE_ERROR = "hermes_web_offline";
/**
* Centralized query keys for all Hermes-related queries.
* Import this from any file that needs to invalidate Hermes caches.
*/
export const hermesKeys = {
all: ["hermes"] as const,
liveProviderIds: ["hermes", "liveProviderIds"] as const,
modelConfig: ["hermes", "modelConfig"] as const,
memory: (kind: HermesMemoryKind) => ["hermes", "memory", kind] as const,
memoryLimits: ["hermes", "memoryLimits"] as const,
};
/**
* Invalidate all Hermes caches that may change when a provider is
* added/updated/deleted/switched. Runs invalidations in parallel so the
* caller doesn't await three sequential refetches.
*/
export function invalidateHermesProviderCaches(queryClient: QueryClient) {
return Promise.all([
queryClient.invalidateQueries({ queryKey: hermesKeys.liveProviderIds }),
queryClient.invalidateQueries({ queryKey: hermesKeys.modelConfig }),
]);
}
// ============================================================
// Query hooks
// ============================================================
export function useHermesLiveProviderIds(enabled: boolean) {
return useQuery({
queryKey: hermesKeys.liveProviderIds,
queryFn: () => providersApi.getHermesLiveProviderIds(),
enabled,
});
}
export function useHermesModelConfig(enabled: boolean) {
return useQuery({
queryKey: hermesKeys.modelConfig,
queryFn: () => hermesApi.getModelConfig(),
enabled,
});
}
export function useHermesMemory(kind: HermesMemoryKind, enabled: boolean) {
return useQuery({
queryKey: hermesKeys.memory(kind),
queryFn: () => hermesApi.getMemory(kind),
enabled,
});
}
export function useHermesMemoryLimits(enabled: boolean) {
return useQuery({
queryKey: hermesKeys.memoryLimits,
queryFn: () => hermesApi.getMemoryLimits(),
staleTime: 60_000,
enabled,
});
}
// ============================================================
// Mutation hooks
// ============================================================
/**
* Save a Hermes memory file atomically and refresh the corresponding query.
* Error toasts are emitted here so caller components don't need their own
* try/catch; success toasts are intentionally left to the caller (to pick
* the right localized message per tab).
*/
export function useSaveHermesMemory() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: ({
kind,
content,
}: {
kind: HermesMemoryKind;
content: string;
}) => hermesApi.setMemory(kind, content),
onSuccess: async (_data, variables) => {
await queryClient.invalidateQueries({
queryKey: hermesKeys.memory(variables.kind),
});
},
onError: (error) => {
toast.error(t("hermes.memory.saveFailed"), {
description: extractErrorMessage(error) || undefined,
});
},
});
}
/**
* Toggle one memory blob's on/off flag in Hermes' `config.yaml`. Invalidates
* the limits query so the switch UI and disabled banner update immediately.
*/
export function useToggleHermesMemoryEnabled() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: ({
kind,
enabled,
}: {
kind: HermesMemoryKind;
enabled: boolean;
}) => hermesApi.setMemoryEnabled(kind, enabled),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: hermesKeys.memoryLimits,
});
},
onError: (error) => {
toast.error(t("hermes.memory.toggleFailed"), {
description: extractErrorMessage(error) || undefined,
});
},
});
}
/**
* Returns a handler that probes the local Hermes Web UI, opens it in the
* system browser, and surfaces a localized toast on failure. When
* `onOffline` is provided, it replaces the default offline toast —
* callers can use this to open a launch-dashboard confirm dialog instead.
*/
export function useOpenHermesWebUI(onOffline?: () => void) {
const { t } = useTranslation();
return useCallback(
async (path?: string) => {
try {
await hermesApi.openWebUI(path);
} catch (error) {
const detail = extractErrorMessage(error);
if (detail === HERMES_WEB_OFFLINE_ERROR) {
if (onOffline) {
onOffline();
} else {
toast.error(t("hermes.webui.offline"));
}
} else {
toast.error(t("hermes.webui.openFailed"), {
description: detail || undefined,
});
}
}
},
[t, onOffline],
);
}