mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +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",
|
"@lobehub/icons-static-svg": "^1.73.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
|||||||
Generated
+3
@@ -50,6 +50,9 @@ importers:
|
|||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.3
|
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)
|
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':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.15
|
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)
|
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,
|
onSubmit,
|
||||||
}: AddProviderDialogProps) {
|
}: AddProviderDialogProps) {
|
||||||
const { t } = useTranslation();
|
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">(
|
const [activeTab, setActiveTab] = useState<"app-specific" | "universal">(
|
||||||
"app-specific",
|
"app-specific",
|
||||||
);
|
);
|
||||||
@@ -185,6 +186,11 @@ export function AddProviderDialog({
|
|||||||
if (options?.baseURL) {
|
if (options?.baseURL) {
|
||||||
addUrl(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);
|
const urls = Array.from(urlSet);
|
||||||
|
|||||||
@@ -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,
|
OPENCODE_PRESET_MODEL_VARIANTS,
|
||||||
type OpenCodeProviderPreset,
|
type OpenCodeProviderPreset,
|
||||||
} from "@/config/opencodeProviderPresets";
|
} from "@/config/opencodeProviderPresets";
|
||||||
|
import {
|
||||||
|
openclawProviderPresets,
|
||||||
|
type OpenClawProviderPreset,
|
||||||
|
} from "@/config/openclawProviderPresets";
|
||||||
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
||||||
|
import { OpenClawFormFields } from "./OpenClawFormFields";
|
||||||
|
import type { OpenCodeModel, OpenClawModel } from "@/types";
|
||||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||||
import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
||||||
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
||||||
@@ -172,13 +178,25 @@ function toOpencodeExtraOptions(
|
|||||||
return extra;
|
return extra;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OPENCLAW_DEFAULT_CONFIG = JSON.stringify(
|
||||||
|
{
|
||||||
|
baseUrl: "",
|
||||||
|
apiKey: "",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
type PresetEntry = {
|
type PresetEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
preset:
|
preset:
|
||||||
| ProviderPreset
|
| ProviderPreset
|
||||||
| CodexProviderPreset
|
| CodexProviderPreset
|
||||||
| GeminiProviderPreset
|
| GeminiProviderPreset
|
||||||
| OpenCodeProviderPreset;
|
| OpenCodeProviderPreset
|
||||||
|
| OpenClawProviderPreset;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ProviderFormProps {
|
interface ProviderFormProps {
|
||||||
@@ -337,7 +355,9 @@ export function ProviderForm({
|
|||||||
? GEMINI_DEFAULT_CONFIG
|
? GEMINI_DEFAULT_CONFIG
|
||||||
: appId === "opencode"
|
: appId === "opencode"
|
||||||
? OPENCODE_DEFAULT_CONFIG
|
? OPENCODE_DEFAULT_CONFIG
|
||||||
: CLAUDE_DEFAULT_CONFIG,
|
: appId === "openclaw"
|
||||||
|
? OPENCLAW_DEFAULT_CONFIG
|
||||||
|
: CLAUDE_DEFAULT_CONFIG,
|
||||||
icon: initialData?.icon ?? "",
|
icon: initialData?.icon ?? "",
|
||||||
iconColor: initialData?.iconColor ?? "",
|
iconColor: initialData?.iconColor ?? "",
|
||||||
}),
|
}),
|
||||||
@@ -464,6 +484,11 @@ export function ProviderForm({
|
|||||||
id: `opencode-${index}`,
|
id: `opencode-${index}`,
|
||||||
preset,
|
preset,
|
||||||
}));
|
}));
|
||||||
|
} else if (appId === "openclaw") {
|
||||||
|
return openclawProviderPresets.map<PresetEntry>((preset, index) => ({
|
||||||
|
id: `openclaw-${index}`,
|
||||||
|
preset,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return providerPresets.map<PresetEntry>((preset, index) => ({
|
return providerPresets.map<PresetEntry>((preset, index) => ({
|
||||||
id: `claude-${index}`,
|
id: `claude-${index}`,
|
||||||
@@ -987,6 +1012,128 @@ export function ProviderForm({
|
|||||||
setUseOmoCommonConfig(useCommonConfig);
|
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(
|
const updateOpencodeSettings = useCallback(
|
||||||
(updater: (config: Record<string, any>) => void) => {
|
(updater: (config: Record<string, any>) => void) => {
|
||||||
try {
|
try {
|
||||||
@@ -1399,6 +1546,21 @@ export function ProviderForm({
|
|||||||
formWebsiteUrl: form.watch("websiteUrl") || "",
|
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({
|
const speedTestEndpoints = useSpeedTestEndpoints({
|
||||||
appId,
|
appId,
|
||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
@@ -1430,6 +1592,13 @@ export function ProviderForm({
|
|||||||
setOpencodeExtraOptions({});
|
setOpencodeExtraOptions({});
|
||||||
resetOmoDraftState();
|
resetOmoDraftState();
|
||||||
}
|
}
|
||||||
|
// OpenClaw 自定义模式:重置为空配置
|
||||||
|
if (appId === "openclaw") {
|
||||||
|
setOpenclawBaseUrl("");
|
||||||
|
setOpenclawApiKey("");
|
||||||
|
setOpenclawApi("openai-completions");
|
||||||
|
setOpenclawModels([]);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1513,6 +1682,28 @@ export function ProviderForm({
|
|||||||
return;
|
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 preset = entry.preset as ProviderPreset;
|
||||||
const config = applyTemplateValues(
|
const config = applyTemplateValues(
|
||||||
preset.settingsConfig,
|
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" ? (
|
{appId === "codex" ? (
|
||||||
<>
|
<>
|
||||||
<CodexConfigEditor
|
<CodexConfigEditor
|
||||||
@@ -1828,6 +2039,34 @@ export function ProviderForm({
|
|||||||
</div>
|
</div>
|
||||||
{settingsConfigErrorField}
|
{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
|
<CommonConfigEditor
|
||||||
|
|||||||
@@ -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 }
|
||||||
+9
-1
@@ -435,8 +435,16 @@ export interface OpenClawModel {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
alias?: 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;
|
contextWindow?: number;
|
||||||
|
maxTokens?: number; // 最大输出 token 数
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenClaw 默认模型配置(agents.defaults.model)
|
// OpenClaw 默认模型配置(agents.defaults.model)
|
||||||
|
|||||||
Reference in New Issue
Block a user