From 7b2cf6681281ff7e0e1cb705ba530294160819ab Mon Sep 17 00:00:00 2001
From: Jason
Date: Thu, 5 Feb 2026 21:42:54 +0800
Subject: [PATCH] feat(openclaw): add providerKey input field with validation
- Add providerKey input field in ProviderForm for OpenClaw
- Implement duplicate key detection by querying existing providers
- Add format validation (lowercase letters, numbers, hyphens only)
- Disable field in edit mode (key cannot be changed after creation)
- Update mutations.ts to support OpenClaw providerKey
- Add i18n translations for zh/en/ja
---
.../providers/forms/ProviderForm.tsx | 90 +++++++++++++++++++
src/i18n/locales/en.json | 8 ++
src/i18n/locales/ja.json | 8 ++
src/i18n/locales/zh.json | 8 ++
src/lib/query/mutations.ts | 4 +-
5 files changed, 116 insertions(+), 2 deletions(-)
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;
}