diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 347492506..98a667144 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -871,6 +871,24 @@ export function ProviderForm({ return providerId || ""; }); + // OpenClaw: query existing providers for duplicate key checking + const { data: openclawProvidersData } = useProvidersQuery("openclaw"); + const existingOpenclawKeys = useMemo(() => { + if (!openclawProvidersData?.providers) return []; + // Exclude current provider ID when in edit mode + return Object.keys(openclawProvidersData.providers).filter( + (k) => k !== providerId, + ); + }, [openclawProvidersData?.providers, providerId]); + + // OpenClaw Provider Key state + const [openclawProviderKey, setOpenclawProviderKey] = useState(() => { + if (appId !== "openclaw") return ""; + // In edit mode, use the existing provider ID as the key + return providerId || ""; + }); + + // OpenCode 配置状态 const [opencodeNpm, setOpencodeNpm] = useState(() => { if (appId !== "opencode") return OPENCODE_DEFAULT_NPM; return initialOpencodeConfig?.npm || OPENCODE_DEFAULT_NPM; @@ -1261,6 +1279,24 @@ export function ProviderForm({ } } + // OpenClaw: validate provider key + if (appId === "openclaw") { + const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; + if (!openclawProviderKey.trim()) { + toast.error(t("openclaw.providerKeyRequired")); + return; + } + if (!keyPattern.test(openclawProviderKey)) { + toast.error(t("openclaw.providerKeyInvalid")); + return; + } + if (!isEditMode && existingOpenclawKeys.includes(openclawProviderKey)) { + toast.error(t("openclaw.providerKeyDuplicate")); + return; + } + } + + // 非官方供应商必填校验:端点和 API Key if (category !== "official") { if (appId === "claude") { if (!baseUrl.trim()) { @@ -1394,6 +1430,8 @@ export function ProviderForm({ } else { payload.providerKey = opencodeProviderKey; } + } else if (appId === "openclaw") { + payload.providerKey = openclawProviderKey; } if (category === "omo" && !payload.presetCategory) { @@ -1594,6 +1632,7 @@ export function ProviderForm({ } // OpenClaw 自定义模式:重置为空配置 if (appId === "openclaw") { + setOpenclawProviderKey(""); setOpenclawBaseUrl(""); setOpenclawApiKey(""); setOpenclawApi("openai-completions"); @@ -1687,6 +1726,9 @@ export function ProviderForm({ const preset = entry.preset as OpenClawProviderPreset; const config = preset.settingsConfig; + // Clear provider key (user must enter their own unique key) + setOpenclawProviderKey(""); + // Update OpenClaw-specific states setOpenclawBaseUrl(config.baseUrl || ""); setOpenclawApiKey(config.apiKey || ""); @@ -1808,6 +1850,54 @@ export function ProviderForm({

)} + ) : appId === "openclaw" ? ( +
+ + + setOpenclawProviderKey( + e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""), + ) + } + placeholder={t("openclaw.providerKeyPlaceholder")} + disabled={isEditMode} + className={ + (existingOpenclawKeys.includes(openclawProviderKey) && + !isEditMode) || + (openclawProviderKey.trim() !== "" && + !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey)) + ? "border-destructive" + : "" + } + /> + {existingOpenclawKeys.includes(openclawProviderKey) && + !isEditMode && ( +

+ {t("openclaw.providerKeyDuplicate")} +

+ )} + {openclawProviderKey.trim() !== "" && + !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey) && ( +

+ {t("openclaw.providerKeyInvalid")} +

+ )} + {!( + existingOpenclawKeys.includes(openclawProviderKey) && + !isEditMode + ) && + (openclawProviderKey.trim() === "" || + /^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey)) && ( +

+ {t("openclaw.providerKeyHint")} +

+ )} +
) : undefined } /> diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b1ab8e460..2f52a6b49 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -732,6 +732,14 @@ "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" }, + "openclaw": { + "providerKey": "Provider Key", + "providerKeyPlaceholder": "my-provider", + "providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.", + "providerKeyRequired": "Provider key is required", + "providerKeyDuplicate": "This key is already in use", + "providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only." + }, "providerPreset": { "label": "Provider Preset", "custom": "Custom Configuration", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 3412ee1d5..4fe768253 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -732,6 +732,14 @@ "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" }, + "openclaw": { + "providerKey": "プロバイダーキー", + "providerKeyPlaceholder": "my-provider", + "providerKeyHint": "設定ファイル内のユニーク識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用可能。", + "providerKeyRequired": "プロバイダーキーを入力してください", + "providerKeyDuplicate": "このキーは既に使用されています", + "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用可能。" + }, "providerPreset": { "label": "プロバイダータイプ", "custom": "カスタム設定", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 1deeb99c0..7308d55c8 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -732,6 +732,14 @@ "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" }, + "openclaw": { + "providerKey": "供应商标识", + "providerKeyPlaceholder": "my-provider", + "providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符", + "providerKeyRequired": "请填写供应商标识", + "providerKeyDuplicate": "此标识已被使用,请更换", + "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符" + }, "providerPreset": { "label": "预设供应商", "custom": "自定义配置", diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index 89e3e486b..a05ca8d49 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -16,12 +16,12 @@ export const useAddProviderMutation = (appId: AppId) => { ) => { let id: string; - if (appId === "opencode") { + if (appId === "opencode" || appId === "openclaw") { if (providerInput.category === "omo") { id = `omo-${generateUUID()}`; } else { if (!providerInput.providerKey) { - throw new Error("Provider key is required for OpenCode"); + throw new Error(`Provider key is required for ${appId}`); } id = providerInput.providerKey; }