mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-07 06:07:18 +08:00
feat: replace text inputs with dropdown selects for OpenClaw agent model config
- Add useOpenClawModelOptions hook to aggregate models from all configured OpenClaw providers - Replace read-only primary model display with a searchable Select dropdown - Replace comma-separated fallback text input with add/remove Select rows - Filter out already-selected models from fallback options - Show "(not configured)" marker for values whose provider has been deleted - Unify terminology: rename "主模型/Primary Model" to "默认模型/Default Model"
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Save } from "lucide-react";
|
||||
import { Save, Plus, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
useOpenClawAgentsDefaults,
|
||||
@@ -10,14 +10,28 @@ import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { OpenClawAgentsDefaults } from "@/types";
|
||||
import { useOpenClawModelOptions } from "./hooks/useOpenClawModelOptions";
|
||||
|
||||
const UNSET_SENTINEL = "__unset__";
|
||||
|
||||
const AgentsDefaultsPanel: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: agentsData, isLoading } = useOpenClawAgentsDefaults();
|
||||
const saveAgentsMutation = useSaveOpenClawAgentsDefaults();
|
||||
const { options: modelOptions, isLoading: modelsLoading } =
|
||||
useOpenClawModelOptions();
|
||||
|
||||
const [defaults, setDefaults] = useState<OpenClawAgentsDefaults | null>(null);
|
||||
const [fallbacks, setFallbacks] = useState("");
|
||||
const [primaryModel, setPrimaryModel] = useState("");
|
||||
const [fallbacks, setFallbacks] = useState<string[]>([]);
|
||||
|
||||
// Extra known fields from agents.defaults
|
||||
const [workspace, setWorkspace] = useState("");
|
||||
@@ -25,16 +39,14 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
const [contextTokens, setContextTokens] = useState("");
|
||||
const [maxConcurrent, setMaxConcurrent] = useState("");
|
||||
|
||||
// Primary model is read-only — set via the "Set as default model" button on provider cards
|
||||
const primaryModel = agentsData?.model?.primary ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
// agentsData is undefined while loading, null when config section is absent
|
||||
if (agentsData === undefined) return;
|
||||
setDefaults(agentsData);
|
||||
|
||||
if (agentsData) {
|
||||
setFallbacks((agentsData.model?.fallbacks ?? []).join(", "));
|
||||
setPrimaryModel(agentsData.model?.primary ?? "");
|
||||
setFallbacks(agentsData.model?.fallbacks ?? []);
|
||||
|
||||
// Extract known extra fields
|
||||
setWorkspace(String(agentsData.workspace ?? ""));
|
||||
@@ -44,25 +56,82 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
}
|
||||
}, [agentsData]);
|
||||
|
||||
// Build primary options, including a "not in list" entry if current value is missing
|
||||
const primaryOptions = useMemo(() => {
|
||||
const result = [...modelOptions];
|
||||
if (
|
||||
primaryModel &&
|
||||
!modelOptions.some((opt) => opt.value === primaryModel)
|
||||
) {
|
||||
result.unshift({
|
||||
value: primaryModel,
|
||||
label: t("openclaw.agents.notInList", {
|
||||
value: primaryModel,
|
||||
defaultValue: "{{value}} (not configured)",
|
||||
}),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}, [modelOptions, primaryModel, t]);
|
||||
|
||||
// For each fallback row, compute available options (exclude primary + other fallbacks)
|
||||
const getFallbackOptions = (currentIndex: number) => {
|
||||
const usedValues = new Set<string>();
|
||||
if (primaryModel) usedValues.add(primaryModel);
|
||||
fallbacks.forEach((fb, idx) => {
|
||||
if (idx !== currentIndex && fb) usedValues.add(fb);
|
||||
});
|
||||
|
||||
const filtered = modelOptions.filter((opt) => !usedValues.has(opt.value));
|
||||
|
||||
// If current fallback value is not in modelOptions, add a "not in list" entry
|
||||
const currentValue = fallbacks[currentIndex];
|
||||
if (
|
||||
currentValue &&
|
||||
!modelOptions.some((opt) => opt.value === currentValue)
|
||||
) {
|
||||
filtered.unshift({
|
||||
value: currentValue,
|
||||
label: t("openclaw.agents.notInList", {
|
||||
value: currentValue,
|
||||
defaultValue: "{{value}} (not configured)",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const handleAddFallback = () => {
|
||||
setFallbacks((prev) => [...prev, ""]);
|
||||
};
|
||||
|
||||
const handleRemoveFallback = (index: number) => {
|
||||
setFallbacks((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleFallbackChange = (index: number, value: string) => {
|
||||
setFallbacks((prev) => {
|
||||
const next = [...prev];
|
||||
next[index] = value;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// Preserve all unknown fields from original data
|
||||
const updated: OpenClawAgentsDefaults = { ...defaults };
|
||||
|
||||
// Model configuration — primary is read-only, preserve original value
|
||||
const fallbackList = fallbacks
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
// Model configuration
|
||||
const fallbackList = fallbacks.filter(Boolean);
|
||||
|
||||
const origPrimary = defaults?.model?.primary;
|
||||
if (origPrimary) {
|
||||
if (primaryModel) {
|
||||
updated.model = {
|
||||
primary: origPrimary,
|
||||
primary: primaryModel,
|
||||
...(fallbackList.length > 0 ? { fallbacks: fallbackList } : {}),
|
||||
};
|
||||
} else if (fallbackList.length > 0) {
|
||||
// No primary set but user provided fallbacks — keep fallbacks only
|
||||
updated.model = { primary: "", fallbacks: fallbackList };
|
||||
}
|
||||
|
||||
@@ -110,6 +179,8 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const noModels = modelOptions.length === 0 && !modelsLoading;
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8">
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
@@ -123,31 +194,111 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Primary Model */}
|
||||
<div>
|
||||
<Label className="mb-1.5 block">
|
||||
{t("openclaw.agents.primaryModel")}
|
||||
</Label>
|
||||
<div className="h-9 px-3 flex items-center rounded-md border border-input bg-muted/50 font-mono text-xs text-muted-foreground">
|
||||
{primaryModel || t("openclaw.agents.notSet")}
|
||||
</div>
|
||||
{noModels ? (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{t("openclaw.agents.noModels", {
|
||||
defaultValue:
|
||||
"No configured provider models. Please add an OpenClaw provider first.",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<Select
|
||||
value={primaryModel || UNSET_SENTINEL}
|
||||
onValueChange={(v) =>
|
||||
setPrimaryModel(v === UNSET_SENTINEL ? "" : v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="font-mono text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={UNSET_SENTINEL}>
|
||||
{t("openclaw.agents.notSet")}
|
||||
</SelectItem>
|
||||
{primaryOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t("openclaw.agents.primaryModelHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Fallback Models */}
|
||||
<div>
|
||||
<Label className="mb-1.5 block">
|
||||
{t("openclaw.agents.fallbackModels")}
|
||||
</Label>
|
||||
<Input
|
||||
value={fallbacks}
|
||||
onChange={(e) => setFallbacks(e.target.value)}
|
||||
placeholder="provider/model-a, provider/model-b"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t("openclaw.agents.fallbackModelsHint")}
|
||||
</p>
|
||||
|
||||
{fallbacks.length === 0 && !noModels && (
|
||||
<p className="text-xs text-muted-foreground italic mb-2">
|
||||
{t("openclaw.agents.fallbackModelsHint")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{fallbacks.map((fb, index) => {
|
||||
const opts = getFallbackOptions(index);
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Select
|
||||
value={fb || UNSET_SENTINEL}
|
||||
onValueChange={(v) =>
|
||||
handleFallbackChange(
|
||||
index,
|
||||
v === UNSET_SENTINEL ? "" : v,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="font-mono text-xs flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={UNSET_SENTINEL}>
|
||||
{t("openclaw.agents.notSet")}
|
||||
</SelectItem>
|
||||
{opts.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => handleRemoveFallback(index)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!noModels && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={handleAddFallback}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
{t("openclaw.agents.addFallback", {
|
||||
defaultValue: "Add fallback model",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
62
src/components/openclaw/hooks/useOpenClawModelOptions.ts
Normal file
62
src/components/openclaw/hooks/useOpenClawModelOptions.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useMemo } from "react";
|
||||
import { useProvidersQuery } from "@/lib/query/queries";
|
||||
import type { OpenClawProviderConfig } from "@/types";
|
||||
|
||||
export interface ModelOption {
|
||||
value: string; // "providerId/modelId"
|
||||
label: string; // "Provider Name / Model Name"
|
||||
}
|
||||
|
||||
export function useOpenClawModelOptions(): {
|
||||
options: ModelOption[];
|
||||
isLoading: boolean;
|
||||
} {
|
||||
const { data: providersData, isLoading } = useProvidersQuery("openclaw");
|
||||
|
||||
const options = useMemo<ModelOption[]>(() => {
|
||||
const allProviders = providersData?.providers;
|
||||
if (!allProviders) return [];
|
||||
|
||||
const dedupedOptions = new Map<string, string>();
|
||||
|
||||
for (const [providerKey, provider] of Object.entries(allProviders)) {
|
||||
let config: OpenClawProviderConfig;
|
||||
try {
|
||||
config =
|
||||
typeof provider.settingsConfig === "string"
|
||||
? (JSON.parse(provider.settingsConfig) as OpenClawProviderConfig)
|
||||
: (provider.settingsConfig as OpenClawProviderConfig);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const models = config.models;
|
||||
if (!Array.isArray(models)) continue;
|
||||
|
||||
const providerDisplayName =
|
||||
typeof provider.name === "string" && provider.name.trim()
|
||||
? provider.name
|
||||
: providerKey;
|
||||
|
||||
for (const model of models) {
|
||||
if (!model.id) continue;
|
||||
const value = `${providerKey}/${model.id}`;
|
||||
const modelDisplayName =
|
||||
typeof model.name === "string" && model.name.trim()
|
||||
? model.name
|
||||
: model.id;
|
||||
const label = `${providerDisplayName} / ${modelDisplayName}`;
|
||||
|
||||
if (!dedupedOptions.has(value)) {
|
||||
dedupedOptions.set(value, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(dedupedOptions.entries())
|
||||
.map(([value, label]) => ({ value, label }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN"));
|
||||
}, [providersData?.providers]);
|
||||
|
||||
return { options, isLoading };
|
||||
}
|
||||
@@ -1292,11 +1292,14 @@
|
||||
"title": "Agents Config",
|
||||
"description": "Manage agents.defaults in openclaw.json (default model, runtime parameters, etc.)",
|
||||
"modelSection": "Model Configuration",
|
||||
"primaryModel": "Primary Model",
|
||||
"primaryModelHint": "Set via the \"Set as Default Model\" button in the provider list",
|
||||
"primaryModel": "Default Model",
|
||||
"primaryModelHint": "Select from models of configured providers",
|
||||
"notSet": "Not set",
|
||||
"fallbackModels": "Fallback Models",
|
||||
"fallbackModelsHint": "Comma-separated, ordered by priority",
|
||||
"fallbackModelsHint": "When the primary model is unavailable, fallbacks are tried in order",
|
||||
"addFallback": "Add fallback model",
|
||||
"noModels": "No configured provider models. Please add an OpenClaw provider first.",
|
||||
"notInList": "{{value}} (not configured)",
|
||||
"runtimeSection": "Runtime Parameters",
|
||||
"workspace": "Workspace Path",
|
||||
"timeout": "Timeout (seconds)",
|
||||
|
||||
@@ -1292,11 +1292,14 @@
|
||||
"title": "Agents 設定",
|
||||
"description": "openclaw.json の agents.defaults 設定を管理(デフォルトモデル、ランタイムパラメータなど)",
|
||||
"modelSection": "モデル設定",
|
||||
"primaryModel": "プライマリモデル",
|
||||
"primaryModelHint": "プロバイダ一覧の「デフォルトモデルに設定」ボタンで設定します",
|
||||
"primaryModel": "デフォルトモデル",
|
||||
"primaryModelHint": "設定済みプロバイダのモデルから選択してください",
|
||||
"notSet": "未設定",
|
||||
"fallbackModels": "フォールバックモデル",
|
||||
"fallbackModelsHint": "カンマ区切り、優先度順",
|
||||
"fallbackModelsHint": "プライマリモデルが利用不可の場合、優先度順にフォールバックが試行されます",
|
||||
"addFallback": "フォールバックモデルを追加",
|
||||
"noModels": "設定済みのプロバイダモデルがありません。先に OpenClaw プロバイダを追加してください。",
|
||||
"notInList": "{{value}} (未設定)",
|
||||
"runtimeSection": "ランタイムパラメータ",
|
||||
"workspace": "ワークスペースパス",
|
||||
"timeout": "タイムアウト(秒)",
|
||||
|
||||
@@ -1292,11 +1292,14 @@
|
||||
"title": "Agents 配置",
|
||||
"description": "管理 openclaw.json 中的 agents.defaults 配置(默认模型、运行参数等)",
|
||||
"modelSection": "模型配置",
|
||||
"primaryModel": "主模型",
|
||||
"primaryModelHint": "在供应商列表中通过「设为默认模型」按钮设置",
|
||||
"primaryModel": "默认模型",
|
||||
"primaryModelHint": "从已配置供应商的模型中选择默认模型",
|
||||
"notSet": "未设置",
|
||||
"fallbackModels": "回退模型",
|
||||
"fallbackModelsHint": "逗号分隔,按优先级排列",
|
||||
"fallbackModelsHint": "当主模型不可用时,按优先级依次尝试以下回退模型",
|
||||
"addFallback": "添加回退模型",
|
||||
"noModels": "暂无已配置的供应商模型。请先添加 OpenClaw 供应商。",
|
||||
"notInList": "{{value}} (供应商未配置)",
|
||||
"runtimeSection": "运行参数",
|
||||
"workspace": "工作区路径",
|
||||
"timeout": "超时时间(秒)",
|
||||
|
||||
Reference in New Issue
Block a user