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.
This commit is contained in:
Jason
2026-04-23 11:59:25 +08:00
parent dc04165f18
commit cdcc423122
11 changed files with 4 additions and 561 deletions
+1 -14
View File
@@ -41,11 +41,7 @@ import {
import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env";
import { useProviderActions } from "@/hooks/useProviderActions";
import { openclawKeys, useOpenClawHealth } from "@/hooks/useOpenClaw";
import {
hermesKeys,
useHermesHealth,
useOpenHermesWebUI,
} from "@/hooks/useHermes";
import { hermesKeys, useOpenHermesWebUI } from "@/hooks/useHermes";
import { hermesApi } from "@/lib/api/hermes";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { useAutoCompact } from "@/hooks/useAutoCompact";
@@ -91,7 +87,6 @@ import EnvPanel from "@/components/openclaw/EnvPanel";
import ToolsPanel from "@/components/openclaw/ToolsPanel";
import AgentsDefaultsPanel from "@/components/openclaw/AgentsDefaultsPanel";
import OpenClawHealthBanner from "@/components/openclaw/OpenClawHealthBanner";
import HermesHealthBanner from "@/components/hermes/HermesHealthBanner";
import HermesMemoryPanel from "@/components/hermes/HermesMemoryPanel";
type View =
@@ -274,8 +269,6 @@ function App() {
currentView === "openclawAgents");
const { data: openclawHealthWarnings = [] } =
useOpenClawHealth(isOpenClawView);
const isHermesView = activeApp === "hermes" && currentView === "providers";
const { data: hermesHealthWarnings = [] } = useHermesHealth(isHermesView);
const hasSkillsSupport = true;
const hasSessionSupport =
activeApp === "claude" ||
@@ -685,9 +678,6 @@ function App() {
await queryClient.invalidateQueries({
queryKey: hermesKeys.liveProviderIds,
});
await queryClient.invalidateQueries({
queryKey: hermesKeys.health,
});
}
toast.success(
t("notifications.removeFromConfigSuccess", {
@@ -1555,9 +1545,6 @@ function App() {
{isOpenClawView && openclawHealthWarnings.length > 0 && (
<OpenClawHealthBanner warnings={openclawHealthWarnings} />
)}
{isHermesView && hermesHealthWarnings.length > 0 && (
<HermesHealthBanner warnings={hermesHealthWarnings} />
)}
{renderContent()}
</main>
@@ -1,127 +0,0 @@
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { ExternalLink, TriangleAlert } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { useOpenHermesWebUI } from "@/hooks/useHermes";
import type { HermesHealthWarning } from "@/types";
interface HermesHealthBannerProps {
warnings: HermesHealthWarning[];
}
function getWarningText(
code: string,
fallback: string,
t: ReturnType<typeof useTranslation>["t"],
) {
switch (code) {
case "config_parse_failed":
return t("hermes.health.parseFailed", {
defaultValue:
"config.yaml could not be parsed as valid YAML. Fix the file before editing it here.",
});
case "config_not_found":
return t("hermes.health.configNotFound", {
defaultValue:
"Hermes config.yaml not found. Create it at ~/.hermes/config.yaml or configure the path in settings.",
});
case "env_parse_failed":
return t("hermes.health.envParseFailed", {
defaultValue: "The .env file could not be parsed.",
});
case "model_no_default":
return t("hermes.health.modelNoDefault", {
defaultValue:
"No default model or provider is configured in the 'model' section.",
});
case "custom_providers_not_list":
return t("hermes.health.customProvidersNotList", {
defaultValue:
"custom_providers should be a YAML list (items prefixed with '-'), not a mapping.",
});
case "model_provider_unknown":
return t("hermes.health.modelProviderUnknown", {
defaultValue:
"model.provider references a provider that is not configured.",
});
case "model_default_not_in_provider":
return t("hermes.health.modelDefaultNotInProvider", {
defaultValue:
"model.default is not in the selected provider's models list.",
});
case "duplicate_provider_name":
return t("hermes.health.duplicateProviderName", {
defaultValue:
"custom_providers contains duplicate provider names — only one entry will be used.",
});
case "duplicate_provider_base_url":
return t("hermes.health.duplicateProviderBaseUrl", {
defaultValue:
"custom_providers contains duplicate base_urls — possible accidental copy.",
});
case "schema_migrated_v12":
return t("hermes.health.schemaMigratedV12", {
defaultValue:
"Hermes' newer schema moved some providers into the 'providers:' dict. They are shown read-only in CC Switch — edit or remove those entries via Hermes Web UI.",
});
default:
return fallback;
}
}
const HermesHealthBanner: React.FC<HermesHealthBannerProps> = ({
warnings,
}) => {
const { t } = useTranslation();
const openHermesWebUI = useOpenHermesWebUI();
const items = useMemo(
() =>
warnings.map((warning) => ({
...warning,
text: getWarningText(warning.code, warning.message, t),
})),
[t, warnings],
);
if (warnings.length === 0) {
return null;
}
return (
<div className="px-6 pt-4">
<Alert className="border-amber-500/30 bg-amber-500/5">
<TriangleAlert className="h-4 w-4" />
<AlertTitle className="flex items-center justify-between gap-2">
<span>
{t("hermes.health.title", {
defaultValue: "Hermes config warnings detected",
})}
</span>
<Button
variant="outline"
size="sm"
onClick={() => void openHermesWebUI("/config")}
className="shrink-0"
>
<ExternalLink className="w-3.5 h-3.5 mr-1" />
{t("hermes.webui.fixInWebUI")}
</Button>
</AlertTitle>
<AlertDescription>
<ul className="list-disc space-y-1 pl-5">
{items.map((warning) => (
<li key={`${warning.code}:${warning.path ?? warning.message}`}>
{warning.text}
{warning.path ? ` (${warning.path})` : ""}
</li>
))}
</ul>
</AlertDescription>
</Alert>
</div>
);
};
export default HermesHealthBanner;
-11
View File
@@ -27,7 +27,6 @@ export const hermesKeys = {
all: ["hermes"] as const,
liveProviderIds: ["hermes", "liveProviderIds"] as const,
modelConfig: ["hermes", "modelConfig"] as const,
health: ["hermes", "health"] as const,
memory: (kind: HermesMemoryKind) => ["hermes", "memory", kind] as const,
memoryLimits: ["hermes", "memoryLimits"] as const,
};
@@ -41,7 +40,6 @@ export function invalidateHermesProviderCaches(queryClient: QueryClient) {
return Promise.all([
queryClient.invalidateQueries({ queryKey: hermesKeys.liveProviderIds }),
queryClient.invalidateQueries({ queryKey: hermesKeys.modelConfig }),
queryClient.invalidateQueries({ queryKey: hermesKeys.health }),
]);
}
@@ -65,15 +63,6 @@ export function useHermesModelConfig(enabled: boolean) {
});
}
export function useHermesHealth(enabled: boolean) {
return useQuery({
queryKey: hermesKeys.health,
queryFn: () => hermesApi.scanHealth(),
staleTime: 30_000,
enabled,
});
}
export function useHermesMemory(kind: HermesMemoryKind, enabled: boolean) {
return useQuery({
queryKey: hermesKeys.memory(kind),
-14
View File
@@ -1682,26 +1682,12 @@
"open": "Open Hermes Web UI",
"offline": "Hermes Web UI is not running. Start it with `hermes dashboard` first.",
"openFailed": "Failed to open Hermes Web UI",
"fixInWebUI": "Fix in Hermes Web UI",
"launchConfirmTitle": "Hermes Dashboard is not running",
"launchConfirmMessage": "Open a terminal and start it now with `hermes dashboard`?\n\nThe browser will open automatically once startup completes.\n\nIf the terminal reports that `hermes` cannot be found or the web extras are missing, run first:\npip install hermes-agent[web]",
"launchConfirmAction": "Open terminal & launch",
"launching": "Started `hermes dashboard` in a terminal.",
"launchFailed": "Failed to open terminal"
},
"health": {
"title": "Hermes config warnings detected",
"parseFailed": "config.yaml could not be parsed as valid YAML. Fix the file before editing it here.",
"configNotFound": "Hermes config.yaml not found. Create it at ~/.hermes/config.yaml or configure the path in settings.",
"envParseFailed": "The .env file could not be parsed.",
"modelNoDefault": "No default model or provider is configured in the 'model' section.",
"customProvidersNotList": "custom_providers should be a YAML list (items prefixed with '-'), not a mapping.",
"modelProviderUnknown": "model.provider references a provider that is not configured.",
"modelDefaultNotInProvider": "model.default is not in the selected provider's models list.",
"duplicateProviderName": "custom_providers contains duplicate provider names — only one entry will be used.",
"duplicateProviderBaseUrl": "custom_providers contains duplicate base_urls — possible accidental copy.",
"schemaMigratedV12": "Hermes' newer schema moved some providers into the 'providers:' dict. They are shown read-only in CC Switch — edit or remove those entries via Hermes Web UI."
},
"memory": {
"title": "Memory",
"agentTab": "Agent Memory (MEMORY.md)",
-14
View File
@@ -1682,26 +1682,12 @@
"open": "Hermes Web UI を開く",
"offline": "Hermes Web UI が起動していません。まず `hermes dashboard` を実行してください。",
"openFailed": "Hermes Web UI を開けませんでした",
"fixInWebUI": "Hermes Web UI で修正",
"launchConfirmTitle": "Hermes Dashboard が起動していません",
"launchConfirmMessage": "ターミナルを開いて `hermes dashboard` を実行しますか?\n\n起動完了後、ブラウザが自動的に開きます。\n\nターミナルに `hermes` が見つからない、または web 依存が不足するというエラーが表示される場合は、まず次を実行してください:\npip install hermes-agent[web]",
"launchConfirmAction": "ターミナルを開いて起動",
"launching": "ターミナルで hermes dashboard を起動しました。",
"launchFailed": "ターミナルを開けませんでした"
},
"health": {
"title": "Hermes 設定の警告を検出",
"parseFailed": "config.yaml を有効な YAML として解析できません。ここで編集する前にファイルを修正してください。",
"configNotFound": "Hermes config.yaml が見つかりません。~/.hermes/config.yaml に作成するか、設定でパスを構成してください。",
"envParseFailed": ".env ファイルを解析できません。",
"modelNoDefault": "'model' セクションに既定モデルまたはプロバイダーが設定されていません。",
"customProvidersNotList": "custom_providers はマッピングではなく、YAML リスト('-' で始まる項目)である必要があります。",
"modelProviderUnknown": "model.provider が指すプロバイダーは設定に存在しません。",
"modelDefaultNotInProvider": "model.default は選択中のプロバイダーのモデル一覧に含まれていません。",
"duplicateProviderName": "custom_providers にプロバイダー名の重複があります。1 件のみ有効になります。",
"duplicateProviderBaseUrl": "custom_providers に重複した base_url があります。誤ってコピーされた可能性があります。",
"schemaMigratedV12": "Hermes の新しいスキーマでは一部のプロバイダーが 'providers:' dict に移動しました。CC Switch では読み取り専用で表示されます。編集・削除は Hermes Web UI から行ってください。"
},
"memory": {
"title": "メモリ",
"agentTab": "エージェント記憶 (MEMORY.md)",
-14
View File
@@ -1682,26 +1682,12 @@
"open": "打开 Hermes Web UI",
"offline": "Hermes Web UI 未启动,请先运行 `hermes dashboard` 启动服务。",
"openFailed": "打开 Hermes Web UI 失败",
"fixInWebUI": "在 Hermes Web UI 修复",
"launchConfirmTitle": "Hermes Dashboard 未启动",
"launchConfirmMessage": "是否打开终端并运行 `hermes dashboard` 启动服务?\n\n启动完成后会自动打开浏览器。\n\n若终端提示找不到 hermes 或缺少 web 依赖,请先运行:\npip install hermes-agent[web]",
"launchConfirmAction": "打开终端并启动",
"launching": "已在终端启动 hermes dashboard",
"launchFailed": "打开终端失败"
},
"health": {
"title": "检测到 Hermes 配置警告",
"parseFailed": "config.yaml 无法解析为有效 YAML。请先修复文件再在此编辑。",
"configNotFound": "未找到 Hermes config.yaml。请在 ~/.hermes/config.yaml 创建或在设置中配置路径。",
"envParseFailed": ".env 文件无法解析。",
"modelNoDefault": "model 段未设置默认模型或供应商。",
"customProvidersNotList": "custom_providers 应为 YAML 列表(以 `-` 开头),而不是映射。",
"modelProviderUnknown": "model.provider 指向的供应商未在配置中找到。",
"modelDefaultNotInProvider": "model.default 指向的模型不在所选供应商的模型列表中。",
"duplicateProviderName": "custom_providers 中存在重复的供应商名,只会有一条生效。",
"duplicateProviderBaseUrl": "custom_providers 中存在重复的 base_url,可能是意外复制。",
"schemaMigratedV12": "Hermes 新版 schema 把部分供应商移到了 'providers:' dict。CC Switch 中以只读方式显示,编辑或删除请通过 Hermes Web UI 操作。"
},
"memory": {
"title": "记忆管理",
"agentTab": "Agent 记忆 (MEMORY.md)",
+3 -8
View File
@@ -1,6 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import type {
HermesHealthWarning,
HermesMemoryKind,
HermesMemoryLimits,
HermesModelConfig,
@@ -12,19 +11,15 @@ import type {
* CC Switch intentionally keeps its Hermes surface minimal — deep configuration
* (model, agent behavior, env vars, skills, cron, logs, analytics) lives in
* the Hermes Web UI at http://127.0.0.1:9119. CC Switch only reads the `model`
* section to highlight the active provider, scans config health, and launches
* the Hermes Web UI for everything else. Writes to `model` happen implicitly
* via `apply_switch_defaults` when the user switches providers.
* section to highlight the active provider and launches the Hermes Web UI for
* everything else. Writes to `model` happen implicitly via
* `apply_switch_defaults` when the user switches providers.
*/
export const hermesApi = {
async getModelConfig(): Promise<HermesModelConfig | null> {
return await invoke("get_hermes_model_config");
},
async scanHealth(): Promise<HermesHealthWarning[]> {
return await invoke("scan_hermes_config_health");
},
/**
* Probe the local Hermes Web UI and open it in the system browser.
* Optional `path` lets callers deep-link to specific pages like `/config`.
-11
View File
@@ -589,17 +589,6 @@ export interface HermesModelConfig {
[key: string]: unknown;
}
export interface HermesHealthWarning {
code: string;
message: string;
path?: string;
}
export interface HermesWriteOutcome {
backupPath?: string;
warnings: HermesHealthWarning[];
}
export type HermesMemoryKind = "memory" | "user";
export interface HermesMemoryLimits {