mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 19:45:34 +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:
@@ -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