feat(hermes): render providers: dict overlays as read-only cards

ProviderCard now detects Hermes provider entries sourced from the
v12+ `providers:` dict via the `_cc_source` marker that the backend
injects, and renders a "Hermes Managed" badge beside the title.
ProviderActions receives an `isReadOnly` prop that disables the Edit
and Delete buttons (with a tooltip pointing the user at Hermes Web
UI) while keeping Switch and Duplicate enabled — switching only
touches `model.*`, and duplicate lets users fork the overlay into
their own `custom_providers:` list.

Three-locale i18n keys `provider.managedByHermes` /
`provider.managedByHermesHint` added.
This commit is contained in:
Jason
2026-04-20 10:36:09 +08:00
parent 3f4739365e
commit abb305a82f
5 changed files with 42 additions and 5 deletions
+16 -5
View File
@@ -38,6 +38,8 @@ interface ProviderActionsProps {
isInFailoverQueue?: boolean;
onToggleFailover?: (enabled: boolean) => void;
isOfficialBlockedByProxy?: boolean;
// Hermes v12+ providers: dict overlay — edit/delete must go through Web UI
isReadOnly?: boolean;
// OpenClaw: default model
isDefaultModel?: boolean;
onSetAsDefault?: () => void;
@@ -63,6 +65,7 @@ export function ProviderActions({
isInFailoverQueue = false,
onToggleFailover,
isOfficialBlockedByProxy = false,
isReadOnly = false,
// OpenClaw: default model
isDefaultModel = false,
onSetAsDefault,
@@ -205,7 +208,11 @@ export function ProviderActions({
const buttonState = getMainButtonState();
const canDelete = isOmo || isAdditiveMode ? true : !isCurrent;
const canDelete =
!isReadOnly && (isOmo || isAdditiveMode ? true : !isCurrent);
const readOnlyHint = t("provider.managedByHermesHint", {
defaultValue: "由 Hermes 管理,请在 Hermes Web UI 中编辑",
});
return (
<div className="flex items-center gap-1.5">
@@ -255,9 +262,13 @@ export function ProviderActions({
<Button
size="icon"
variant="ghost"
onClick={onEdit}
title={t("common.edit")}
className={iconButtonClass}
onClick={isReadOnly ? undefined : onEdit}
disabled={isReadOnly}
title={isReadOnly ? readOnlyHint : t("common.edit")}
className={cn(
iconButtonClass,
isReadOnly && "opacity-40 cursor-not-allowed text-muted-foreground",
)}
>
<Edit className="h-4 w-4" />
</Button>
@@ -323,7 +334,7 @@ export function ProviderActions({
size="icon"
variant="ghost"
onClick={canDelete ? onDelete : undefined}
title={t("common.delete")}
title={isReadOnly ? readOnlyHint : t("common.delete")}
className={cn(
iconButtonClass,
canDelete && "hover:text-red-500 dark:hover:text-red-400",
+20
View File
@@ -180,6 +180,12 @@ export function ProviderCard({
const isCopilot =
provider.meta?.providerType === PROVIDER_TYPES.GITHUB_COPILOT ||
provider.meta?.usage_script?.templateType === "github_copilot";
// Hermes v12+ overlay entries live under the `providers:` dict and are
// read-only here — writes have to go through Hermes Web UI.
const isHermesReadOnly =
appId === "hermes" &&
(provider.settingsConfig as Record<string, unknown>)?._cc_source ===
"providers_dict";
const isCodexOauth =
provider.meta?.providerType === PROVIDER_TYPES.CODEX_OAUTH;
@@ -336,6 +342,19 @@ export function ProviderCard({
</span>
)}
{isHermesReadOnly && (
<span
className="inline-flex items-center rounded-md bg-slate-200 px-1.5 py-0.5 text-[10px] font-semibold text-slate-700 dark:bg-slate-700/60 dark:text-slate-200"
title={t("provider.managedByHermesHint", {
defaultValue: "由 Hermes 管理,请在 Hermes Web UI 中编辑",
})}
>
{t("provider.managedByHermes", {
defaultValue: "Hermes Managed",
})}
</span>
)}
</div>
{displayUrl && (
@@ -429,6 +448,7 @@ export function ProviderCard({
isTesting={isTesting}
isProxyTakeover={isProxyTakeover}
isOfficialBlockedByProxy={isOfficialBlockedByProxy}
isReadOnly={isHermesReadOnly}
isOmo={isAnyOmo}
onSwitch={() => onSwitch(provider)}
onEdit={() => onEdit(provider)}
+2
View File
@@ -141,6 +141,8 @@
"sortUpdateFailed": "Failed to update sort order",
"configureUsage": "Configure usage query",
"officialPartner": "Official Partner",
"managedByHermes": "Hermes Managed",
"managedByHermesHint": "Defined in Hermes' providers: dict. Edit or remove it via Hermes Web UI.",
"openTerminal": "Open Terminal",
"terminalOpened": "Terminal opened",
"terminalOpenFailed": "Failed to open terminal",
+2
View File
@@ -141,6 +141,8 @@
"sortUpdateFailed": "並び順の更新に失敗しました",
"configureUsage": "利用状況を設定",
"officialPartner": "公式パートナー",
"managedByHermes": "Hermes 管理",
"managedByHermesHint": "Hermes の providers: dict で定義されています。Hermes Web UI で編集または削除してください。",
"openTerminal": "ターミナルを開く",
"terminalOpened": "ターミナルを開きました",
"terminalOpenFailed": "ターミナルを開けませんでした",
+2
View File
@@ -141,6 +141,8 @@
"sortUpdateFailed": "排序更新失败",
"configureUsage": "配置用量查询",
"officialPartner": "官方合作伙伴",
"managedByHermes": "Hermes 托管",
"managedByHermesHint": "该条目定义在 Hermes 的 providers: dict,请在 Hermes Web UI 中编辑或删除。",
"openTerminal": "打开终端",
"terminalOpened": "终端已打开",
"terminalOpenFailed": "打开终端失败",