feat(providers): show API key link in Claude Desktop, OpenClaw and Hermes forms

The "Get API Key" link (and partner promotion) below the API key input was
only wired for claude/codex/gemini/opencode. Claude Desktop uses a separate
form that never rendered it, and OpenClaw/Hermes were blocked by two gaps:

- useApiKeyLink whitelisted only 4 app ids, so the link was suppressed for
  claude-desktop/openclaw/hermes even when a preset carried apiKeyUrl.
- useProviderCategory only parsed claude/codex/gemini/opencode preset ids, so
  OpenClaw/Hermes category stayed undefined and the link condition
  (cn_official/aggregator/third_party) never held.

Changes:
- ClaudeDesktopProviderForm: call useApiKeyLink and replace the bare API key
  Input with the shared ApiKeySection.
- useApiKeyLink: add claude-desktop/openclaw/hermes to the whitelist and add
  ClaudeDesktopProviderPreset to the PresetEntry union.
- useProviderCategory: parse and resolve openclaw/hermes preset categories.
- Hermes/OpenClaw form fields: don't let an "official" category disable the
  API key input, since these apps have no OAuth-only official providers
  (e.g. Hermes' Nous Research is "official" but still needs a user key).
This commit is contained in:
Jason
2026-06-29 18:49:41 +08:00
parent 238cd5d7c9
commit 36e0378e78
5 changed files with 52 additions and 14 deletions
@@ -39,9 +39,11 @@ import { Switch } from "@/components/ui/switch";
import { BasicFormFields } from "./BasicFormFields";
import { CodexOAuthSection } from "./CodexOAuthSection";
import { CopilotAuthSection } from "./CopilotAuthSection";
import { ApiKeySection } from "./shared/ApiKeySection";
import { EndpointField } from "./shared/EndpointField";
import { ModelDropdown } from "./shared/ModelDropdown";
import { ProviderPresetSelector } from "./ProviderPresetSelector";
import { useApiKeyLink } from "./hooks/useApiKeyLink";
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
import type {
ClaudeApiFormat,
@@ -383,6 +385,21 @@ export function ClaudeDesktopProviderForm({
activeProviderType === "github_copilot" ||
activeProviderType === "codex_oauth";
// API Key 获取/邀请链接(与 Claude Code 表单同款,见 ClaudeFormFields
const apiKeyLinkCategory = activePreset?.category ?? initialData?.category;
const {
shouldShowApiKeyLink,
websiteUrl: apiKeyLinkWebsiteUrl,
isPartner: apiKeyLinkIsPartner,
partnerPromotionKey: apiKeyLinkPromotionKey,
} = useApiKeyLink({
appId: "claude-desktop",
category: apiKeyLinkCategory,
selectedPresetId,
presetEntries,
formWebsiteUrl: form.watch("websiteUrl") || "",
});
const applyDesktopPreset = (preset: ClaudeDesktopProviderPreset) => {
form.setValue("name", preset.nameKey ? t(preset.nameKey) : preset.name);
form.setValue("websiteUrl", preset.websiteUrl);
@@ -766,15 +783,15 @@ export function ClaudeDesktopProviderForm({
)}
</div>
) : (
<div className="space-y-1">
<Label>{"API Key"}</Label>
<Input
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
type="password"
placeholder="sk-..."
/>
</div>
<ApiKeySection
value={apiKey}
onChange={setApiKey}
category={apiKeyLinkCategory}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={apiKeyLinkWebsiteUrl}
isPartner={apiKeyLinkIsPartner}
partnerPromotionKey={apiKeyLinkPromotionKey}
/>
)}
<EndpointField
@@ -330,7 +330,9 @@ export function HermesFormFields({
<ApiKeySection
value={apiKey}
onChange={onApiKeyChange}
category={category}
// Hermes 没有 OAuth-only 的免 key 官方供应商:即便是 official 预设
// (如 Nous Research)也需用户自填 key,故不让 official 禁用输入框。
category={category === "official" ? undefined : category}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
@@ -257,7 +257,9 @@ export function OpenClawFormFields({
<ApiKeySection
value={apiKey}
onChange={onApiKeyChange}
category={category}
// OpenClaw 的 API key 始终由用户自填,没有 OAuth-only 的免 key 官方供应商,
// 故不让 official 禁用输入框(与 Hermes 对齐)。
category={category === "official" ? undefined : category}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
@@ -5,6 +5,7 @@ import type { ProviderPreset } from "@/config/claudeProviderPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
import type { GeminiProviderPreset } from "@/config/geminiProviderPresets";
import type { OpenCodeProviderPreset } from "@/config/opencodeProviderPresets";
import type { ClaudeDesktopProviderPreset } from "@/config/claudeDesktopProviderPresets";
type PresetEntry = {
id: string;
@@ -12,7 +13,8 @@ type PresetEntry = {
| ProviderPreset
| CodexProviderPreset
| GeminiProviderPreset
| OpenCodeProviderPreset;
| OpenCodeProviderPreset
| ClaudeDesktopProviderPreset;
};
interface UseApiKeyLinkProps {
@@ -80,9 +82,12 @@ export function useApiKeyLink({
return {
shouldShowApiKeyLink:
appId === "claude" ||
appId === "claude-desktop" ||
appId === "codex" ||
appId === "gemini" ||
appId === "opencode"
appId === "opencode" ||
appId === "openclaw" ||
appId === "hermes"
? shouldShowApiKeyLink
: false,
websiteUrl: getWebsiteUrl,
@@ -5,6 +5,8 @@ import { providerPresets } from "@/config/claudeProviderPresets";
import { codexProviderPresets } from "@/config/codexProviderPresets";
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
import { opencodeProviderPresets } from "@/config/opencodeProviderPresets";
import { openclawProviderPresets } from "@/config/openclawProviderPresets";
import { hermesProviderPresets } from "@/config/hermesProviderPresets";
interface UseProviderCategoryProps {
appId: AppId;
@@ -44,7 +46,7 @@ export function useProviderCategory({
// 从预设 ID 提取索引
const match = selectedPresetId.match(
/^(claude|codex|gemini|opencode)-(\d+)$/,
/^(claude|codex|gemini|opencode|openclaw|hermes)-(\d+)$/,
);
if (!match) return;
@@ -75,6 +77,16 @@ export function useProviderCategory({
if (preset) {
setCategory(preset.category || undefined);
}
} else if (type === "openclaw" && appId === "openclaw") {
const preset = openclawProviderPresets[index];
if (preset) {
setCategory(preset.category || undefined);
}
} else if (type === "hermes" && appId === "hermes") {
const preset = hermesProviderPresets[index];
if (preset) {
setCategory(preset.category || undefined);
}
}
}, [appId, selectedPresetId, isEditMode, initialCategory]);