mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-09 03:27:05 +08:00
feat(openclaw): add provider form fields and UI components
- Add OpenClawFormFields component for provider configuration - Add Collapsible UI component (radix-ui dependency) - Update ProviderForm with OpenClaw-specific handlers and preset logic - Extend OpenClawModel type with reasoning, input, maxTokens fields - Exclude OpenClaw from universal provider tab in AddProviderDialog
This commit is contained in:
@@ -54,6 +54,7 @@
|
||||
"@lobehub/icons-static-svg": "^1.73.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -50,6 +50,9 @@ importers:
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collapsible':
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
@@ -35,7 +35,8 @@ export function AddProviderDialog({
|
||||
onSubmit,
|
||||
}: AddProviderDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const showUniversalTab = appId !== "opencode";
|
||||
// OpenCode and OpenClaw don't support universal providers
|
||||
const showUniversalTab = appId !== "opencode" && appId !== "openclaw";
|
||||
const [activeTab, setActiveTab] = useState<"app-specific" | "universal">(
|
||||
"app-specific",
|
||||
);
|
||||
@@ -185,6 +186,11 @@ export function AddProviderDialog({
|
||||
if (options?.baseURL) {
|
||||
addUrl(options.baseURL);
|
||||
}
|
||||
} else if (appId === "openclaw") {
|
||||
// OpenClaw uses baseUrl directly
|
||||
if (parsedConfig.baseUrl) {
|
||||
addUrl(parsedConfig.baseUrl as string);
|
||||
}
|
||||
}
|
||||
|
||||
const urls = Array.from(urlSet);
|
||||
|
||||
452
src/components/providers/forms/OpenClawFormFields.tsx
Normal file
452
src/components/providers/forms/OpenClawFormFields.tsx
Normal file
@@ -0,0 +1,452 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { ApiKeySection } from "./shared";
|
||||
import { openclawApiProtocols } from "@/config/openclawProviderPresets";
|
||||
import type { ProviderCategory, OpenClawModel } from "@/types";
|
||||
|
||||
interface OpenClawFormFieldsProps {
|
||||
// Base URL
|
||||
baseUrl: string;
|
||||
onBaseUrlChange: (value: string) => void;
|
||||
|
||||
// API Key
|
||||
apiKey: string;
|
||||
onApiKeyChange: (value: string) => void;
|
||||
category?: ProviderCategory;
|
||||
shouldShowApiKeyLink: boolean;
|
||||
websiteUrl: string;
|
||||
isPartner?: boolean;
|
||||
partnerPromotionKey?: string;
|
||||
|
||||
// API Protocol
|
||||
api: string;
|
||||
onApiChange: (value: string) => void;
|
||||
|
||||
// Models
|
||||
models: OpenClawModel[];
|
||||
onModelsChange: (models: OpenClawModel[]) => void;
|
||||
}
|
||||
|
||||
export function OpenClawFormFields({
|
||||
baseUrl,
|
||||
onBaseUrlChange,
|
||||
apiKey,
|
||||
onApiKeyChange,
|
||||
category,
|
||||
shouldShowApiKeyLink,
|
||||
websiteUrl,
|
||||
isPartner,
|
||||
partnerPromotionKey,
|
||||
api,
|
||||
onApiChange,
|
||||
models,
|
||||
onModelsChange,
|
||||
}: OpenClawFormFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expandedModels, setExpandedModels] = useState<Record<number, boolean>>(
|
||||
{}
|
||||
);
|
||||
|
||||
// Toggle advanced section for a model
|
||||
const toggleModelAdvanced = (index: number) => {
|
||||
setExpandedModels((prev) => ({ ...prev, [index]: !prev[index] }));
|
||||
};
|
||||
|
||||
// Add a new model entry
|
||||
const handleAddModel = () => {
|
||||
onModelsChange([
|
||||
...models,
|
||||
{
|
||||
id: "",
|
||||
name: "",
|
||||
contextWindow: undefined,
|
||||
maxTokens: undefined,
|
||||
cost: undefined,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// Remove a model entry
|
||||
const handleRemoveModel = (index: number) => {
|
||||
const newModels = [...models];
|
||||
newModels.splice(index, 1);
|
||||
onModelsChange(newModels);
|
||||
// Clean up expanded state
|
||||
setExpandedModels((prev) => {
|
||||
const updated = { ...prev };
|
||||
delete updated[index];
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Update model field
|
||||
const handleModelChange = (
|
||||
index: number,
|
||||
field: keyof OpenClawModel,
|
||||
value: unknown
|
||||
) => {
|
||||
const newModels = [...models];
|
||||
newModels[index] = { ...newModels[index], [field]: value };
|
||||
onModelsChange(newModels);
|
||||
};
|
||||
|
||||
// Update model cost
|
||||
const handleCostChange = (
|
||||
index: number,
|
||||
costField: "input" | "output" | "cacheRead" | "cacheWrite",
|
||||
value: string
|
||||
) => {
|
||||
const newModels = [...models];
|
||||
const numValue = parseFloat(value);
|
||||
const currentCost = newModels[index].cost || { input: 0, output: 0 };
|
||||
newModels[index] = {
|
||||
...newModels[index],
|
||||
cost: {
|
||||
...currentCost,
|
||||
[costField]: isNaN(numValue) ? undefined : numValue,
|
||||
},
|
||||
};
|
||||
onModelsChange(newModels);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* API Protocol Selector */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="openclaw-api">
|
||||
{t("openclaw.apiProtocol", {
|
||||
defaultValue: "API 协议",
|
||||
})}
|
||||
</FormLabel>
|
||||
<Select value={api} onValueChange={onApiChange}>
|
||||
<SelectTrigger id="openclaw-api">
|
||||
<SelectValue
|
||||
placeholder={t("openclaw.selectProtocol", {
|
||||
defaultValue: "选择 API 协议",
|
||||
})}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{openclawApiProtocols.map((protocol) => (
|
||||
<SelectItem key={protocol.value} value={protocol.value}>
|
||||
{protocol.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("openclaw.apiProtocolHint", {
|
||||
defaultValue:
|
||||
"选择与供应商 API 兼容的协议类型。大多数供应商使用 OpenAI Completions 格式。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Base URL */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="openclaw-baseurl">
|
||||
{t("openclaw.baseUrl", { defaultValue: "API 端点" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="openclaw-baseurl"
|
||||
value={baseUrl}
|
||||
onChange={(e) => onBaseUrlChange(e.target.value)}
|
||||
placeholder="https://api.example.com/v1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("openclaw.baseUrlHint", {
|
||||
defaultValue: "供应商的 API 端点地址。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<ApiKeySection
|
||||
value={apiKey}
|
||||
onChange={onApiKeyChange}
|
||||
category={category}
|
||||
shouldShowLink={shouldShowApiKeyLink}
|
||||
websiteUrl={websiteUrl}
|
||||
isPartner={isPartner}
|
||||
partnerPromotionKey={partnerPromotionKey}
|
||||
/>
|
||||
|
||||
{/* Models Editor */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>
|
||||
{t("openclaw.models", { defaultValue: "模型列表" })}
|
||||
</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddModel}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{t("openclaw.addModel", { defaultValue: "添加模型" })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{models.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
{t("openclaw.noModels", {
|
||||
defaultValue: "暂无模型配置。点击添加模型来配置可用模型。",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{models.map((model, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-3 border border-border/50 rounded-lg space-y-3"
|
||||
>
|
||||
{/* Model ID and Name row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.modelId", { defaultValue: "模型 ID" })}
|
||||
</label>
|
||||
<Input
|
||||
value={model.id}
|
||||
onChange={(e) =>
|
||||
handleModelChange(index, "id", e.target.value)
|
||||
}
|
||||
placeholder={t("openclaw.modelIdPlaceholder", {
|
||||
defaultValue: "claude-3-sonnet",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.modelName", { defaultValue: "显示名称" })}
|
||||
</label>
|
||||
<Input
|
||||
value={model.name}
|
||||
onChange={(e) =>
|
||||
handleModelChange(index, "name", e.target.value)
|
||||
}
|
||||
placeholder={t("openclaw.modelNamePlaceholder", {
|
||||
defaultValue: "Claude 3 Sonnet",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveModel(index)}
|
||||
className="h-9 w-9 mt-5 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Context Window, Max Tokens and Reasoning row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.contextWindow", {
|
||||
defaultValue: "上下文窗口",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={model.contextWindow ?? ""}
|
||||
onChange={(e) =>
|
||||
handleModelChange(
|
||||
index,
|
||||
"contextWindow",
|
||||
e.target.value ? parseInt(e.target.value) : undefined
|
||||
)
|
||||
}
|
||||
placeholder="200000"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.maxTokens", {
|
||||
defaultValue: "最大输出 Tokens",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={model.maxTokens ?? ""}
|
||||
onChange={(e) =>
|
||||
handleModelChange(
|
||||
index,
|
||||
"maxTokens",
|
||||
e.target.value ? parseInt(e.target.value) : undefined
|
||||
)
|
||||
}
|
||||
placeholder="32000"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.reasoning", {
|
||||
defaultValue: "推理模式",
|
||||
})}
|
||||
</label>
|
||||
<div className="flex items-center h-9 gap-2">
|
||||
<Switch
|
||||
checked={model.reasoning ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleModelChange(index, "reasoning", checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{model.reasoning
|
||||
? t("openclaw.reasoningOn", { defaultValue: "启用" })
|
||||
: t("openclaw.reasoningOff", { defaultValue: "关闭" })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Spacer for alignment with delete button */}
|
||||
<div className="w-9" />
|
||||
</div>
|
||||
|
||||
{/* Basic Cost row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.inputCost", {
|
||||
defaultValue: "输入价格 ($/M tokens)",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.001"
|
||||
value={model.cost?.input ?? ""}
|
||||
onChange={(e) =>
|
||||
handleCostChange(index, "input", e.target.value)
|
||||
}
|
||||
placeholder="3"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.outputCost", {
|
||||
defaultValue: "输出价格 ($/M tokens)",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.001"
|
||||
value={model.cost?.output ?? ""}
|
||||
onChange={(e) =>
|
||||
handleCostChange(index, "output", e.target.value)
|
||||
}
|
||||
placeholder="15"
|
||||
/>
|
||||
</div>
|
||||
{/* Spacer for alignment */}
|
||||
<div className="flex-1" />
|
||||
<div className="w-9" />
|
||||
</div>
|
||||
|
||||
{/* Advanced Options (Collapsible) */}
|
||||
<Collapsible
|
||||
open={expandedModels[index] ?? false}
|
||||
onOpenChange={() => toggleModelAdvanced(index)}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{expandedModels[index] ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("openclaw.advancedOptions", {
|
||||
defaultValue: "高级选项",
|
||||
})}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.cacheReadCost", {
|
||||
defaultValue: "缓存读取价格 ($/M tokens)",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.001"
|
||||
value={model.cost?.cacheRead ?? ""}
|
||||
onChange={(e) =>
|
||||
handleCostChange(index, "cacheRead", e.target.value)
|
||||
}
|
||||
placeholder="0.3"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.cacheWriteCost", {
|
||||
defaultValue: "缓存写入价格 ($/M tokens)",
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.001"
|
||||
value={model.cost?.cacheWrite ?? ""}
|
||||
onChange={(e) =>
|
||||
handleCostChange(
|
||||
index,
|
||||
"cacheWrite",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="3.75"
|
||||
/>
|
||||
</div>
|
||||
{/* Spacer for alignment */}
|
||||
<div className="flex-1" />
|
||||
<div className="w-9" />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{t("openclaw.cacheCostHint", {
|
||||
defaultValue:
|
||||
"缓存价格用于计算 Prompt Caching 的成本。如不使用缓存可留空。",
|
||||
})}
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("openclaw.modelsHint", {
|
||||
defaultValue:
|
||||
"配置该供应商支持的模型。模型 ID 用于 API 调用,显示名称用于界面展示。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,13 @@ import {
|
||||
OPENCODE_PRESET_MODEL_VARIANTS,
|
||||
type OpenCodeProviderPreset,
|
||||
} from "@/config/opencodeProviderPresets";
|
||||
import {
|
||||
openclawProviderPresets,
|
||||
type OpenClawProviderPreset,
|
||||
} from "@/config/openclawProviderPresets";
|
||||
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
||||
import { OpenClawFormFields } from "./OpenClawFormFields";
|
||||
import type { OpenCodeModel, OpenClawModel } from "@/types";
|
||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||
import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
||||
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
||||
@@ -172,13 +178,25 @@ function toOpencodeExtraOptions(
|
||||
return extra;
|
||||
}
|
||||
|
||||
const OPENCLAW_DEFAULT_CONFIG = JSON.stringify(
|
||||
{
|
||||
baseUrl: "",
|
||||
apiKey: "",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
type PresetEntry = {
|
||||
id: string;
|
||||
preset:
|
||||
| ProviderPreset
|
||||
| CodexProviderPreset
|
||||
| GeminiProviderPreset
|
||||
| OpenCodeProviderPreset;
|
||||
| OpenCodeProviderPreset
|
||||
| OpenClawProviderPreset;
|
||||
};
|
||||
|
||||
interface ProviderFormProps {
|
||||
@@ -337,7 +355,9 @@ export function ProviderForm({
|
||||
? GEMINI_DEFAULT_CONFIG
|
||||
: appId === "opencode"
|
||||
? OPENCODE_DEFAULT_CONFIG
|
||||
: CLAUDE_DEFAULT_CONFIG,
|
||||
: appId === "openclaw"
|
||||
? OPENCLAW_DEFAULT_CONFIG
|
||||
: CLAUDE_DEFAULT_CONFIG,
|
||||
icon: initialData?.icon ?? "",
|
||||
iconColor: initialData?.iconColor ?? "",
|
||||
}),
|
||||
@@ -464,6 +484,11 @@ export function ProviderForm({
|
||||
id: `opencode-${index}`,
|
||||
preset,
|
||||
}));
|
||||
} else if (appId === "openclaw") {
|
||||
return openclawProviderPresets.map<PresetEntry>((preset, index) => ({
|
||||
id: `openclaw-${index}`,
|
||||
preset,
|
||||
}));
|
||||
}
|
||||
return providerPresets.map<PresetEntry>((preset, index) => ({
|
||||
id: `claude-${index}`,
|
||||
@@ -987,6 +1012,128 @@ export function ProviderForm({
|
||||
setUseOmoCommonConfig(useCommonConfig);
|
||||
}, []);
|
||||
|
||||
// OpenClaw 配置状态
|
||||
const [openclawBaseUrl, setOpenclawBaseUrl] = useState<string>(() => {
|
||||
if (appId !== "openclaw") return "";
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
initialData?.settingsConfig
|
||||
? JSON.stringify(initialData.settingsConfig)
|
||||
: OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
return config.baseUrl || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const [openclawApiKey, setOpenclawApiKey] = useState<string>(() => {
|
||||
if (appId !== "openclaw") return "";
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
initialData?.settingsConfig
|
||||
? JSON.stringify(initialData.settingsConfig)
|
||||
: OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
return config.apiKey || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const [openclawApi, setOpenclawApi] = useState<string>(() => {
|
||||
if (appId !== "openclaw") return "openai-completions";
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
initialData?.settingsConfig
|
||||
? JSON.stringify(initialData.settingsConfig)
|
||||
: OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
return config.api || "openai-completions";
|
||||
} catch {
|
||||
return "openai-completions";
|
||||
}
|
||||
});
|
||||
|
||||
const [openclawModels, setOpenclawModels] = useState<OpenClawModel[]>(() => {
|
||||
if (appId !== "openclaw") return [];
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
initialData?.settingsConfig
|
||||
? JSON.stringify(initialData.settingsConfig)
|
||||
: OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
return config.models || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// OpenClaw handlers - sync state to form
|
||||
const handleOpenclawBaseUrlChange = useCallback(
|
||||
(baseUrl: string) => {
|
||||
setOpenclawBaseUrl(baseUrl);
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
form.getValues("settingsConfig") || OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
config.baseUrl = baseUrl.trim().replace(/\/+$/, "");
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleOpenclawApiKeyChange = useCallback(
|
||||
(apiKey: string) => {
|
||||
setOpenclawApiKey(apiKey);
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
form.getValues("settingsConfig") || OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
config.apiKey = apiKey;
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleOpenclawApiChange = useCallback(
|
||||
(api: string) => {
|
||||
setOpenclawApi(api);
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
form.getValues("settingsConfig") || OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
config.api = api;
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleOpenclawModelsChange = useCallback(
|
||||
(models: OpenClawModel[]) => {
|
||||
setOpenclawModels(models);
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
form.getValues("settingsConfig") || OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
config.models = models;
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const updateOpencodeSettings = useCallback(
|
||||
(updater: (config: Record<string, any>) => void) => {
|
||||
try {
|
||||
@@ -1399,6 +1546,21 @@ export function ProviderForm({
|
||||
formWebsiteUrl: form.watch("websiteUrl") || "",
|
||||
});
|
||||
|
||||
// 使用 API Key 链接 hook (OpenClaw)
|
||||
const {
|
||||
shouldShowApiKeyLink: shouldShowOpenclawApiKeyLink,
|
||||
websiteUrl: openclawWebsiteUrl,
|
||||
isPartner: isOpenclawPartner,
|
||||
partnerPromotionKey: openclawPartnerPromotionKey,
|
||||
} = useApiKeyLink({
|
||||
appId: "openclaw",
|
||||
category,
|
||||
selectedPresetId,
|
||||
presetEntries,
|
||||
formWebsiteUrl: form.watch("websiteUrl") || "",
|
||||
});
|
||||
|
||||
// 使用端点测速候选 hook
|
||||
const speedTestEndpoints = useSpeedTestEndpoints({
|
||||
appId,
|
||||
selectedPresetId,
|
||||
@@ -1430,6 +1592,13 @@ export function ProviderForm({
|
||||
setOpencodeExtraOptions({});
|
||||
resetOmoDraftState();
|
||||
}
|
||||
// OpenClaw 自定义模式:重置为空配置
|
||||
if (appId === "openclaw") {
|
||||
setOpenclawBaseUrl("");
|
||||
setOpenclawApiKey("");
|
||||
setOpenclawApi("openai-completions");
|
||||
setOpenclawModels([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1513,6 +1682,28 @@ export function ProviderForm({
|
||||
return;
|
||||
}
|
||||
|
||||
// OpenClaw preset handling
|
||||
if (appId === "openclaw") {
|
||||
const preset = entry.preset as OpenClawProviderPreset;
|
||||
const config = preset.settingsConfig;
|
||||
|
||||
// Update OpenClaw-specific states
|
||||
setOpenclawBaseUrl(config.baseUrl || "");
|
||||
setOpenclawApiKey(config.apiKey || "");
|
||||
setOpenclawApi(config.api || "openai-completions");
|
||||
setOpenclawModels(config.models || []);
|
||||
|
||||
// Update form fields
|
||||
form.reset({
|
||||
name: preset.name,
|
||||
websiteUrl: preset.websiteUrl ?? "",
|
||||
settingsConfig: JSON.stringify(config, null, 2),
|
||||
icon: preset.icon ?? "",
|
||||
iconColor: preset.iconColor ?? "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = entry.preset as ProviderPreset;
|
||||
const config = applyTemplateValues(
|
||||
preset.settingsConfig,
|
||||
@@ -1752,6 +1943,26 @@ export function ProviderForm({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* OpenClaw 专属字段 */}
|
||||
{appId === "openclaw" && (
|
||||
<OpenClawFormFields
|
||||
baseUrl={openclawBaseUrl}
|
||||
onBaseUrlChange={handleOpenclawBaseUrlChange}
|
||||
apiKey={openclawApiKey}
|
||||
onApiKeyChange={handleOpenclawApiKeyChange}
|
||||
category={category}
|
||||
shouldShowApiKeyLink={shouldShowOpenclawApiKeyLink}
|
||||
websiteUrl={openclawWebsiteUrl}
|
||||
isPartner={isOpenclawPartner}
|
||||
partnerPromotionKey={openclawPartnerPromotionKey}
|
||||
api={openclawApi}
|
||||
onApiChange={handleOpenclawApiChange}
|
||||
models={openclawModels}
|
||||
onModelsChange={handleOpenclawModelsChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 配置编辑器:Codex、Claude、Gemini 分别使用不同的编辑器 */}
|
||||
{appId === "codex" ? (
|
||||
<>
|
||||
<CodexConfigEditor
|
||||
@@ -1828,6 +2039,34 @@ export function ProviderForm({
|
||||
</div>
|
||||
{settingsConfigErrorField}
|
||||
</>
|
||||
) : appId === "openclaw" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="settingsConfig">{t("provider.configJson")}</Label>
|
||||
<JsonEditor
|
||||
value={form.getValues("settingsConfig")}
|
||||
onChange={(config) => form.setValue("settingsConfig", config)}
|
||||
placeholder={`{
|
||||
"baseUrl": "https://api.example.com/v1",
|
||||
"apiKey": "your-api-key-here",
|
||||
"api": "openai-completions",
|
||||
"models": []
|
||||
}`}
|
||||
rows={14}
|
||||
showValidation={true}
|
||||
language="json"
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="settingsConfig"
|
||||
render={() => (
|
||||
<FormItem className="space-y-0">
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CommonConfigEditor
|
||||
|
||||
9
src/components/ui/collapsible.tsx
Normal file
9
src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
10
src/types.ts
10
src/types.ts
@@ -435,8 +435,16 @@ export interface OpenClawModel {
|
||||
id: string;
|
||||
name: string;
|
||||
alias?: string;
|
||||
cost?: { input: number; output: number };
|
||||
reasoning?: boolean; // 是否支持推理模式(如 o1、DeepSeek R1)
|
||||
input?: string[]; // 支持的输入类型(如 ["text"]、["text", "image"])
|
||||
cost?: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead?: number; // 缓存读取价格
|
||||
cacheWrite?: number; // 缓存写入价格
|
||||
};
|
||||
contextWindow?: number;
|
||||
maxTokens?: number; // 最大输出 token 数
|
||||
}
|
||||
|
||||
// OpenClaw 默认模型配置(agents.defaults.model)
|
||||
|
||||
Reference in New Issue
Block a user