mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-06 19:26:48 +08:00
feat(opencode): Phase 9 - Frontend UI components for OpenCode
- Create OpenCodeFormFields.tsx with: - NPM package selector (from AI SDK ecosystem) - API Key input using shared ApiKeySection component - Base URL input (shown for openai-compatible) - Dynamic models editor (add/remove models) - Update ProviderForm.tsx: - Import OpenCode presets and form fields - Add OPENCODE_DEFAULT_CONFIG constant - Add OpenCode to PresetEntry type union - Add OpenCode preset entries in useMemo - Add OpenCode state hooks (npm, apiKey, baseUrl, models) - Add OpenCode change handlers syncing to form - Add OpenCodeFormFields rendering section - Add OpenCode config editor using CommonConfigEditor - Update ProviderActions.tsx for OpenCode additive mode: - Add appId and isInConfig props - Implement "Add to Config" / "Remove from Config" buttons - Disable failover mode for OpenCode - Update delete button logic for additive mode - Update ProviderCard.tsx: - Pass appId and isInConfig to ProviderActions - Update AddProviderDialog.tsx: - Add OpenCode base URL extraction from options.baseURL
This commit is contained in:
@@ -17,6 +17,7 @@ import { UniversalProviderPanel } from "@/components/universal";
|
||||
import { providerPresets } from "@/config/claudeProviderPresets";
|
||||
import { codexProviderPresets } from "@/config/codexProviderPresets";
|
||||
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
|
||||
// Note: opencodeProviderPresets is loaded via ProviderForm, not needed here
|
||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||
|
||||
interface AddProviderDialogProps {
|
||||
@@ -153,6 +154,7 @@ export function AddProviderDialog({
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: OpenCode doesn't use endpointCandidates - it handles endpoints internally
|
||||
}
|
||||
|
||||
if (appId === "claude") {
|
||||
@@ -175,6 +177,12 @@ export function AddProviderDialog({
|
||||
if (env?.GOOGLE_GEMINI_BASE_URL) {
|
||||
addUrl(env.GOOGLE_GEMINI_BASE_URL);
|
||||
}
|
||||
} else if (appId === "opencode") {
|
||||
// OpenCode uses options.baseURL
|
||||
const options = parsedConfig.options as Record<string, any> | undefined;
|
||||
if (options?.baseURL) {
|
||||
addUrl(options.baseURL);
|
||||
}
|
||||
}
|
||||
|
||||
const urls = Array.from(urlSet);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Copy,
|
||||
Edit,
|
||||
Loader2,
|
||||
Minus,
|
||||
Play,
|
||||
Plus,
|
||||
Terminal,
|
||||
@@ -13,9 +14,13 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AppId } from "@/lib/api";
|
||||
|
||||
interface ProviderActionsProps {
|
||||
appId?: AppId;
|
||||
isCurrent: boolean;
|
||||
/** OpenCode: 是否已添加到配置 */
|
||||
isInConfig?: boolean;
|
||||
isTesting?: boolean;
|
||||
isProxyTakeover?: boolean;
|
||||
onSwitch: () => void;
|
||||
@@ -32,7 +37,9 @@ interface ProviderActionsProps {
|
||||
}
|
||||
|
||||
export function ProviderActions({
|
||||
appId,
|
||||
isCurrent,
|
||||
isInConfig = false,
|
||||
isTesting,
|
||||
isProxyTakeover = false,
|
||||
onSwitch,
|
||||
@@ -50,12 +57,22 @@ export function ProviderActions({
|
||||
const { t } = useTranslation();
|
||||
const iconButtonClass = "h-8 w-8 p-1";
|
||||
|
||||
// 故障转移模式下的按钮逻辑
|
||||
const isFailoverMode = isAutoFailoverEnabled && onToggleFailover;
|
||||
// OpenCode 使用累加模式
|
||||
const isOpenCodeMode = appId === "opencode";
|
||||
|
||||
// 故障转移模式下的按钮逻辑(OpenCode 不支持故障转移)
|
||||
const isFailoverMode = !isOpenCodeMode && isAutoFailoverEnabled && onToggleFailover;
|
||||
|
||||
// 处理主按钮点击
|
||||
const handleMainButtonClick = () => {
|
||||
if (isFailoverMode) {
|
||||
if (isOpenCodeMode) {
|
||||
// OpenCode 模式:切换配置状态(添加/移除)
|
||||
if (isInConfig) {
|
||||
onDelete(); // 从配置移除
|
||||
} else {
|
||||
onSwitch(); // 添加到配置
|
||||
}
|
||||
} else if (isFailoverMode) {
|
||||
// 故障转移模式:切换队列状态
|
||||
onToggleFailover(!isInFailoverQueue);
|
||||
} else {
|
||||
@@ -66,8 +83,30 @@ export function ProviderActions({
|
||||
|
||||
// 主按钮的状态和样式
|
||||
const getMainButtonState = () => {
|
||||
// OpenCode 累加模式
|
||||
if (isOpenCodeMode) {
|
||||
if (isInConfig) {
|
||||
return {
|
||||
disabled: false,
|
||||
variant: "secondary" as const,
|
||||
className:
|
||||
"bg-orange-100 text-orange-600 hover:bg-orange-200 dark:bg-orange-900/50 dark:text-orange-400 dark:hover:bg-orange-900/70",
|
||||
icon: <Minus className="h-4 w-4" />,
|
||||
text: t("provider.removeFromConfig", { defaultValue: "移除" }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
disabled: false,
|
||||
variant: "default" as const,
|
||||
className:
|
||||
"bg-emerald-500 hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700",
|
||||
icon: <Plus className="h-4 w-4" />,
|
||||
text: t("provider.addToConfig", { defaultValue: "添加" }),
|
||||
};
|
||||
}
|
||||
|
||||
// 故障转移模式
|
||||
if (isFailoverMode) {
|
||||
// 故障转移模式
|
||||
if (isInFailoverQueue) {
|
||||
return {
|
||||
disabled: false,
|
||||
@@ -113,6 +152,9 @@ export function ProviderActions({
|
||||
|
||||
const buttonState = getMainButtonState();
|
||||
|
||||
// OpenCode 模式下删除按钮的行为不同(主按钮已处理移除功能)
|
||||
const canDelete = isOpenCodeMode ? !isInConfig : !isCurrent;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button
|
||||
@@ -192,12 +234,12 @@ export function ProviderActions({
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={isCurrent ? undefined : onDelete}
|
||||
onClick={canDelete ? onDelete : undefined}
|
||||
title={t("common.delete")}
|
||||
className={cn(
|
||||
iconButtonClass,
|
||||
!isCurrent && "hover:text-red-500 dark:hover:text-red-400",
|
||||
isCurrent && "opacity-40 cursor-not-allowed text-muted-foreground",
|
||||
canDelete && "hover:text-red-500 dark:hover:text-red-400",
|
||||
!canDelete && "opacity-40 cursor-not-allowed text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
|
||||
@@ -360,7 +360,9 @@ export function ProviderCard({
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pl-3 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0"
|
||||
>
|
||||
<ProviderActions
|
||||
appId={appId}
|
||||
isCurrent={isCurrent}
|
||||
isInConfig={true}
|
||||
isTesting={isTesting}
|
||||
isProxyTakeover={isProxyTakeover}
|
||||
onSwitch={() => onSwitch(provider)}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { ApiKeySection } from "./shared";
|
||||
import { opencodeNpmPackages } from "@/config/opencodeProviderPresets";
|
||||
import type { ProviderCategory, OpenCodeModel } from "@/types";
|
||||
|
||||
interface OpenCodeFormFieldsProps {
|
||||
// NPM Package
|
||||
npm: string;
|
||||
onNpmChange: (value: string) => void;
|
||||
|
||||
// API Key
|
||||
apiKey: string;
|
||||
onApiKeyChange: (value: string) => void;
|
||||
category?: ProviderCategory;
|
||||
shouldShowApiKeyLink: boolean;
|
||||
websiteUrl: string;
|
||||
|
||||
// Base URL
|
||||
baseUrl: string;
|
||||
onBaseUrlChange: (value: string) => void;
|
||||
|
||||
// Models
|
||||
models: Record<string, OpenCodeModel>;
|
||||
onModelsChange: (models: Record<string, OpenCodeModel>) => void;
|
||||
}
|
||||
|
||||
export function OpenCodeFormFields({
|
||||
npm,
|
||||
onNpmChange,
|
||||
apiKey,
|
||||
onApiKeyChange,
|
||||
category,
|
||||
shouldShowApiKeyLink,
|
||||
websiteUrl,
|
||||
baseUrl,
|
||||
onBaseUrlChange,
|
||||
models,
|
||||
onModelsChange,
|
||||
}: OpenCodeFormFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Add a new model entry
|
||||
const handleAddModel = () => {
|
||||
const newKey = `model-${Date.now()}`;
|
||||
onModelsChange({
|
||||
...models,
|
||||
[newKey]: { name: "" },
|
||||
});
|
||||
};
|
||||
|
||||
// Remove a model entry
|
||||
const handleRemoveModel = (key: string) => {
|
||||
const newModels = { ...models };
|
||||
delete newModels[key];
|
||||
onModelsChange(newModels);
|
||||
};
|
||||
|
||||
// Update model ID (key)
|
||||
const handleModelIdChange = (oldKey: string, newKey: string) => {
|
||||
if (oldKey === newKey || !newKey.trim()) return;
|
||||
const newModels: Record<string, OpenCodeModel> = {};
|
||||
for (const [k, v] of Object.entries(models)) {
|
||||
if (k === oldKey) {
|
||||
newModels[newKey] = v;
|
||||
} else {
|
||||
newModels[k] = v;
|
||||
}
|
||||
}
|
||||
onModelsChange(newModels);
|
||||
};
|
||||
|
||||
// Update model name
|
||||
const handleModelNameChange = (key: string, name: string) => {
|
||||
onModelsChange({
|
||||
...models,
|
||||
[key]: { ...models[key], name },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* NPM Package Selector */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="opencode-npm">
|
||||
{t("provider.form.opencode.npmPackage", {
|
||||
defaultValue: "AI SDK Package",
|
||||
})}
|
||||
</FormLabel>
|
||||
<Select value={npm} onValueChange={onNpmChange}>
|
||||
<SelectTrigger id="opencode-npm">
|
||||
<SelectValue
|
||||
placeholder={t("provider.form.opencode.selectPackage", {
|
||||
defaultValue: "Select a package",
|
||||
})}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{opencodeNpmPackages.map((pkg) => (
|
||||
<SelectItem key={pkg.value} value={pkg.value}>
|
||||
{pkg.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("provider.form.opencode.npmPackageHint", {
|
||||
defaultValue:
|
||||
"Select the AI SDK package that matches your provider.",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<ApiKeySection
|
||||
value={apiKey}
|
||||
onChange={onApiKeyChange}
|
||||
category={category}
|
||||
shouldShowLink={shouldShowApiKeyLink}
|
||||
websiteUrl={websiteUrl}
|
||||
/>
|
||||
|
||||
{/* Base URL (only for compatible providers) */}
|
||||
{npm === "@ai-sdk/openai-compatible" && (
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="opencode-baseurl">
|
||||
{t("provider.form.opencode.baseUrl", { defaultValue: "Base URL" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="opencode-baseurl"
|
||||
value={baseUrl}
|
||||
onChange={(e) => onBaseUrlChange(e.target.value)}
|
||||
placeholder="https://api.example.com/v1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("provider.form.opencode.baseUrlHint", {
|
||||
defaultValue:
|
||||
"The base URL for OpenAI-compatible API endpoints.",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Models Editor */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>
|
||||
{t("provider.form.opencode.models", { defaultValue: "Models" })}
|
||||
</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddModel}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{t("provider.form.opencode.addModel", { defaultValue: "Add" })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{Object.keys(models).length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
{t("provider.form.opencode.noModels", {
|
||||
defaultValue: "No models configured. Click Add to add a model.",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(models).map(([key, model]) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<Input
|
||||
value={key}
|
||||
onChange={(e) => handleModelIdChange(key, e.target.value)}
|
||||
placeholder={t("provider.form.opencode.modelId", {
|
||||
defaultValue: "Model ID",
|
||||
})}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={model.name}
|
||||
onChange={(e) => handleModelNameChange(key, e.target.value)}
|
||||
placeholder={t("provider.form.opencode.modelName", {
|
||||
defaultValue: "Display Name",
|
||||
})}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveModel(key)}
|
||||
className="h-9 w-9 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("provider.form.opencode.modelsHint", {
|
||||
defaultValue:
|
||||
"Configure available models. Model ID is the API identifier, Display Name is shown in the UI.",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,12 @@ import {
|
||||
geminiProviderPresets,
|
||||
type GeminiProviderPreset,
|
||||
} from "@/config/geminiProviderPresets";
|
||||
import {
|
||||
opencodeProviderPresets,
|
||||
type OpenCodeProviderPreset,
|
||||
} from "@/config/opencodeProviderPresets";
|
||||
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
||||
import type { OpenCodeModel } from "@/types";
|
||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||
import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
||||
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
||||
@@ -62,9 +68,22 @@ const GEMINI_DEFAULT_CONFIG = JSON.stringify(
|
||||
2,
|
||||
);
|
||||
|
||||
const OPENCODE_DEFAULT_CONFIG = JSON.stringify(
|
||||
{
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
baseURL: "",
|
||||
apiKey: "",
|
||||
},
|
||||
models: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
type PresetEntry = {
|
||||
id: string;
|
||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset;
|
||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset | OpenCodeProviderPreset;
|
||||
};
|
||||
|
||||
interface ProviderFormProps {
|
||||
@@ -158,7 +177,9 @@ export function ProviderForm({
|
||||
? CODEX_DEFAULT_CONFIG
|
||||
: appId === "gemini"
|
||||
? GEMINI_DEFAULT_CONFIG
|
||||
: CLAUDE_DEFAULT_CONFIG,
|
||||
: appId === "opencode"
|
||||
? OPENCODE_DEFAULT_CONFIG
|
||||
: CLAUDE_DEFAULT_CONFIG,
|
||||
icon: initialData?.icon ?? "",
|
||||
iconColor: initialData?.iconColor ?? "",
|
||||
}),
|
||||
@@ -328,6 +349,11 @@ export function ProviderForm({
|
||||
id: `gemini-${index}`,
|
||||
preset,
|
||||
}));
|
||||
} else if (appId === "opencode") {
|
||||
return opencodeProviderPresets.map<PresetEntry>((preset, index) => ({
|
||||
id: `opencode-${index}`,
|
||||
preset,
|
||||
}));
|
||||
}
|
||||
return providerPresets.map<PresetEntry>((preset, index) => ({
|
||||
id: `claude-${index}`,
|
||||
@@ -469,6 +495,106 @@ export function ProviderForm({
|
||||
selectedPresetId: selectedPresetId ?? undefined,
|
||||
});
|
||||
|
||||
// OpenCode 配置状态
|
||||
const [opencodeNpm, setOpencodeNpm] = useState<string>(() => {
|
||||
if (appId !== "opencode") return "@ai-sdk/openai-compatible";
|
||||
try {
|
||||
const config = JSON.parse(initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig) : OPENCODE_DEFAULT_CONFIG);
|
||||
return config.npm || "@ai-sdk/openai-compatible";
|
||||
} catch {
|
||||
return "@ai-sdk/openai-compatible";
|
||||
}
|
||||
});
|
||||
|
||||
const [opencodeApiKey, setOpencodeApiKey] = useState<string>(() => {
|
||||
if (appId !== "opencode") return "";
|
||||
try {
|
||||
const config = JSON.parse(initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig) : OPENCODE_DEFAULT_CONFIG);
|
||||
return config.options?.apiKey || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const [opencodeBaseUrl, setOpencodeBaseUrl] = useState<string>(() => {
|
||||
if (appId !== "opencode") return "";
|
||||
try {
|
||||
const config = JSON.parse(initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig) : OPENCODE_DEFAULT_CONFIG);
|
||||
return config.options?.baseURL || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const [opencodeModels, setOpencodeModels] = useState<Record<string, OpenCodeModel>>(() => {
|
||||
if (appId !== "opencode") return {};
|
||||
try {
|
||||
const config = JSON.parse(initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig) : OPENCODE_DEFAULT_CONFIG);
|
||||
return config.models || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
// OpenCode handlers - sync state to form
|
||||
const handleOpencodeNpmChange = useCallback(
|
||||
(npm: string) => {
|
||||
setOpencodeNpm(npm);
|
||||
try {
|
||||
const config = JSON.parse(form.watch("settingsConfig") || OPENCODE_DEFAULT_CONFIG);
|
||||
config.npm = npm;
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleOpencodeApiKeyChange = useCallback(
|
||||
(apiKey: string) => {
|
||||
setOpencodeApiKey(apiKey);
|
||||
try {
|
||||
const config = JSON.parse(form.watch("settingsConfig") || OPENCODE_DEFAULT_CONFIG);
|
||||
if (!config.options) config.options = {};
|
||||
config.options.apiKey = apiKey;
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleOpencodeBaseUrlChange = useCallback(
|
||||
(baseUrl: string) => {
|
||||
setOpencodeBaseUrl(baseUrl);
|
||||
try {
|
||||
const config = JSON.parse(form.watch("settingsConfig") || OPENCODE_DEFAULT_CONFIG);
|
||||
if (!config.options) config.options = {};
|
||||
config.options.baseURL = baseUrl.trim().replace(/\/+$/, "");
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleOpencodeModelsChange = useCallback(
|
||||
(models: Record<string, OpenCodeModel>) => {
|
||||
setOpencodeModels(models);
|
||||
try {
|
||||
const config = JSON.parse(form.watch("settingsConfig") || OPENCODE_DEFAULT_CONFIG);
|
||||
config.models = models;
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||
|
||||
const handleSubmit = (values: ProviderFormData) => {
|
||||
@@ -941,6 +1067,23 @@ export function ProviderForm({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* OpenCode 专属字段 */}
|
||||
{appId === "opencode" && (
|
||||
<OpenCodeFormFields
|
||||
npm={opencodeNpm}
|
||||
onNpmChange={handleOpencodeNpmChange}
|
||||
apiKey={opencodeApiKey}
|
||||
onApiKeyChange={handleOpencodeApiKeyChange}
|
||||
category={category}
|
||||
shouldShowApiKeyLink={false}
|
||||
websiteUrl=""
|
||||
baseUrl={opencodeBaseUrl}
|
||||
onBaseUrlChange={handleOpencodeBaseUrlChange}
|
||||
models={opencodeModels}
|
||||
onModelsChange={handleOpencodeModelsChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 配置编辑器:Codex、Claude、Gemini 分别使用不同的编辑器 */}
|
||||
{appId === "codex" ? (
|
||||
<>
|
||||
@@ -1000,6 +1143,32 @@ export function ProviderForm({
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : appId === "opencode" ? (
|
||||
<>
|
||||
<CommonConfigEditor
|
||||
value={form.watch("settingsConfig")}
|
||||
onChange={(config) => form.setValue("settingsConfig", config)}
|
||||
useCommonConfig={false}
|
||||
onCommonConfigToggle={() => {}}
|
||||
commonConfigSnippet=""
|
||||
onCommonConfigSnippetChange={() => {}}
|
||||
commonConfigError=""
|
||||
onEditClick={() => {}}
|
||||
isModalOpen={false}
|
||||
onModalClose={() => {}}
|
||||
onExtract={() => Promise.resolve()}
|
||||
isExtracting={false}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="settingsConfig"
|
||||
render={() => (
|
||||
<FormItem className="space-y-0">
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CommonConfigEditor
|
||||
|
||||
Reference in New Issue
Block a user