mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-24 08:08:52 +08:00
refactor(forms): extract OpenCode/OMO/OpenClaw state from ProviderForm into dedicated hooks
Split ProviderForm.tsx (2227 → 1526 lines) by extracting: - opencodeFormUtils.ts: pure functions and default config constants - useOmoModelSource: OMO model source collection from OpenCode providers - useOpencodeFormState: OpenCode provider form state and handlers - useOmoDraftState: OMO profile draft editing state - useOpenclawFormState: OpenClaw provider form state and handlers
This commit is contained in:
File diff suppressed because it is too large
Load Diff
161
src/components/providers/forms/helpers/opencodeFormUtils.ts
Normal file
161
src/components/providers/forms/helpers/opencodeFormUtils.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { OpenCodeModel, OpenCodeProviderConfig } from "@/types";
|
||||
import type { OmoGlobalConfig } from "@/types/omo";
|
||||
import { parseOmoOtherFieldsObject } from "@/types/omo";
|
||||
import type { PricingModelSourceOption } from "../ProviderAdvancedConfig";
|
||||
|
||||
// ── Default configs ──────────────────────────────────────────────────
|
||||
|
||||
export const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {} }, null, 2);
|
||||
export const CODEX_DEFAULT_CONFIG = JSON.stringify(
|
||||
{ auth: {}, config: "" },
|
||||
null,
|
||||
2,
|
||||
);
|
||||
export const GEMINI_DEFAULT_CONFIG = JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
GOOGLE_GEMINI_BASE_URL: "",
|
||||
GEMINI_API_KEY: "",
|
||||
GEMINI_MODEL: "gemini-3-pro-preview",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
export const OPENCODE_DEFAULT_NPM = "@ai-sdk/openai-compatible";
|
||||
export const OPENCODE_DEFAULT_CONFIG = JSON.stringify(
|
||||
{
|
||||
npm: OPENCODE_DEFAULT_NPM,
|
||||
options: {
|
||||
baseURL: "",
|
||||
apiKey: "",
|
||||
},
|
||||
models: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
export const OPENCODE_KNOWN_OPTION_KEYS = [
|
||||
"baseURL",
|
||||
"apiKey",
|
||||
"headers",
|
||||
] as const;
|
||||
|
||||
export const OPENCLAW_DEFAULT_CONFIG = JSON.stringify(
|
||||
{
|
||||
baseUrl: "",
|
||||
apiKey: "",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
export const EMPTY_OMO_GLOBAL_CONFIG: OmoGlobalConfig = {
|
||||
id: "global",
|
||||
disabledAgents: [],
|
||||
disabledMcps: [],
|
||||
disabledHooks: [],
|
||||
disabledSkills: [],
|
||||
updatedAt: "",
|
||||
};
|
||||
|
||||
// ── Pure functions ───────────────────────────────────────────────────
|
||||
|
||||
export function isKnownOpencodeOptionKey(key: string): boolean {
|
||||
return OPENCODE_KNOWN_OPTION_KEYS.includes(
|
||||
key as (typeof OPENCODE_KNOWN_OPTION_KEYS)[number],
|
||||
);
|
||||
}
|
||||
|
||||
export function parseOpencodeConfig(
|
||||
settingsConfig?: Record<string, unknown>,
|
||||
): OpenCodeProviderConfig {
|
||||
const normalize = (
|
||||
parsed: Partial<OpenCodeProviderConfig>,
|
||||
): OpenCodeProviderConfig => ({
|
||||
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
|
||||
options:
|
||||
parsed.options && typeof parsed.options === "object"
|
||||
? (parsed.options as OpenCodeProviderConfig["options"])
|
||||
: {},
|
||||
models:
|
||||
parsed.models && typeof parsed.models === "object"
|
||||
? (parsed.models as Record<string, OpenCodeModel>)
|
||||
: {},
|
||||
});
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
|
||||
) as Partial<OpenCodeProviderConfig>;
|
||||
return normalize(parsed);
|
||||
} catch {
|
||||
return {
|
||||
npm: OPENCODE_DEFAULT_NPM,
|
||||
options: {},
|
||||
models: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseOpencodeConfigStrict(
|
||||
settingsConfig?: Record<string, unknown>,
|
||||
): OpenCodeProviderConfig {
|
||||
const parsed = JSON.parse(
|
||||
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
|
||||
) as Partial<OpenCodeProviderConfig>;
|
||||
return {
|
||||
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
|
||||
options:
|
||||
parsed.options && typeof parsed.options === "object"
|
||||
? (parsed.options as OpenCodeProviderConfig["options"])
|
||||
: {},
|
||||
models:
|
||||
parsed.models && typeof parsed.models === "object"
|
||||
? (parsed.models as Record<string, OpenCodeModel>)
|
||||
: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function toOpencodeExtraOptions(
|
||||
options: OpenCodeProviderConfig["options"],
|
||||
): Record<string, string> {
|
||||
const extra: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(options || {})) {
|
||||
if (!isKnownOpencodeOptionKey(k)) {
|
||||
extra[k] = typeof v === "string" ? v : JSON.stringify(v);
|
||||
}
|
||||
}
|
||||
return extra;
|
||||
}
|
||||
|
||||
export function buildOmoProfilePreview(
|
||||
agents: Record<string, Record<string, unknown>>,
|
||||
categories: Record<string, Record<string, unknown>>,
|
||||
otherFieldsStr: string,
|
||||
): Record<string, unknown> {
|
||||
const profileOnly: Record<string, unknown> = {};
|
||||
if (Object.keys(agents).length > 0) {
|
||||
profileOnly.agents = agents;
|
||||
}
|
||||
if (Object.keys(categories).length > 0) {
|
||||
profileOnly.categories = categories;
|
||||
}
|
||||
if (otherFieldsStr.trim()) {
|
||||
try {
|
||||
const other = parseOmoOtherFieldsObject(otherFieldsStr);
|
||||
if (other) {
|
||||
Object.assign(profileOnly, other);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return profileOnly;
|
||||
}
|
||||
|
||||
export const normalizePricingSource = (
|
||||
value?: string,
|
||||
): PricingModelSourceOption =>
|
||||
value === "request" || value === "response" ? value : "inherit";
|
||||
@@ -12,3 +12,7 @@ export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints";
|
||||
export { useCodexTomlValidation } from "./useCodexTomlValidation";
|
||||
export { useGeminiConfigState } from "./useGeminiConfigState";
|
||||
export { useGeminiCommonConfig } from "./useGeminiCommonConfig";
|
||||
export { useOmoModelSource } from "./useOmoModelSource";
|
||||
export { useOpencodeFormState } from "./useOpencodeFormState";
|
||||
export { useOmoDraftState } from "./useOmoDraftState";
|
||||
export { useOpenclawFormState } from "./useOpenclawFormState";
|
||||
|
||||
190
src/components/providers/forms/hooks/useOmoDraftState.ts
Normal file
190
src/components/providers/forms/hooks/useOmoDraftState.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import type { OmoGlobalConfig } from "@/types/omo";
|
||||
import { mergeOmoConfigPreview } from "@/types/omo";
|
||||
import { type OmoGlobalConfigFieldsRef } from "../OmoGlobalConfigFields";
|
||||
import * as configApi from "@/lib/api/config";
|
||||
import {
|
||||
EMPTY_OMO_GLOBAL_CONFIG,
|
||||
buildOmoProfilePreview,
|
||||
} from "../helpers/opencodeFormUtils";
|
||||
|
||||
interface UseOmoDraftStateParams {
|
||||
initialOmoSettings: Record<string, unknown> | undefined;
|
||||
queriedOmoGlobalConfig: OmoGlobalConfig | undefined;
|
||||
isEditMode: boolean;
|
||||
appId: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface OmoDraftState {
|
||||
omoAgents: Record<string, Record<string, unknown>>;
|
||||
setOmoAgents: React.Dispatch<
|
||||
React.SetStateAction<Record<string, Record<string, unknown>>>
|
||||
>;
|
||||
omoCategories: Record<string, Record<string, unknown>>;
|
||||
setOmoCategories: React.Dispatch<
|
||||
React.SetStateAction<Record<string, Record<string, unknown>>>
|
||||
>;
|
||||
omoOtherFieldsStr: string;
|
||||
setOmoOtherFieldsStr: React.Dispatch<React.SetStateAction<string>>;
|
||||
useOmoCommonConfig: boolean;
|
||||
setUseOmoCommonConfig: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isOmoConfigModalOpen: boolean;
|
||||
setIsOmoConfigModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isOmoSaving: boolean;
|
||||
omoGlobalConfigRef: React.RefObject<OmoGlobalConfigFieldsRef | null>;
|
||||
omoFieldsKey: number;
|
||||
effectiveOmoGlobalConfig: OmoGlobalConfig;
|
||||
mergedOmoJsonPreview: string;
|
||||
handleOmoGlobalConfigSave: () => Promise<void>;
|
||||
handleOmoEditClick: () => void;
|
||||
resetOmoDraftState: (useCommonConfig?: boolean) => void;
|
||||
setOmoGlobalState: React.Dispatch<
|
||||
React.SetStateAction<OmoGlobalConfig | null>
|
||||
>;
|
||||
}
|
||||
|
||||
export function useOmoDraftState({
|
||||
initialOmoSettings,
|
||||
queriedOmoGlobalConfig,
|
||||
isEditMode,
|
||||
appId,
|
||||
category,
|
||||
}: UseOmoDraftStateParams): OmoDraftState {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [omoAgents, setOmoAgents] = useState<
|
||||
Record<string, Record<string, unknown>>
|
||||
>(
|
||||
() =>
|
||||
(initialOmoSettings?.agents as Record<string, Record<string, unknown>>) ||
|
||||
{},
|
||||
);
|
||||
const [omoCategories, setOmoCategories] = useState<
|
||||
Record<string, Record<string, unknown>>
|
||||
>(
|
||||
() =>
|
||||
(initialOmoSettings?.categories as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>) || {},
|
||||
);
|
||||
const [omoOtherFieldsStr, setOmoOtherFieldsStr] = useState(() => {
|
||||
const otherFields = initialOmoSettings?.otherFields;
|
||||
return otherFields ? JSON.stringify(otherFields, null, 2) : "";
|
||||
});
|
||||
|
||||
const [omoGlobalState, setOmoGlobalState] = useState<OmoGlobalConfig | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [isOmoConfigModalOpen, setIsOmoConfigModalOpen] = useState(false);
|
||||
const [useOmoCommonConfig, setUseOmoCommonConfig] = useState(() => {
|
||||
const raw = initialOmoSettings?.useCommonConfig;
|
||||
return typeof raw === "boolean" ? raw : true;
|
||||
});
|
||||
const [isOmoSaving, setIsOmoSaving] = useState(false);
|
||||
const omoGlobalConfigRef = useRef<OmoGlobalConfigFieldsRef>(null);
|
||||
const [omoFieldsKey, setOmoFieldsKey] = useState(0);
|
||||
const effectiveOmoGlobalConfig =
|
||||
omoGlobalState ?? queriedOmoGlobalConfig ?? EMPTY_OMO_GLOBAL_CONFIG;
|
||||
|
||||
const mergedOmoJsonPreview = useMemo(() => {
|
||||
if (useOmoCommonConfig) {
|
||||
const merged = mergeOmoConfigPreview(
|
||||
effectiveOmoGlobalConfig,
|
||||
omoAgents,
|
||||
omoCategories,
|
||||
omoOtherFieldsStr,
|
||||
);
|
||||
return JSON.stringify(merged, null, 2);
|
||||
} else {
|
||||
return JSON.stringify(
|
||||
buildOmoProfilePreview(omoAgents, omoCategories, omoOtherFieldsStr),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
useOmoCommonConfig,
|
||||
effectiveOmoGlobalConfig,
|
||||
omoAgents,
|
||||
omoCategories,
|
||||
omoOtherFieldsStr,
|
||||
]);
|
||||
|
||||
// Auto-detect whether common config has content for new OMO profiles
|
||||
useEffect(() => {
|
||||
if (appId !== "opencode" || category !== "omo" || isEditMode) return;
|
||||
let active = true;
|
||||
(async () => {
|
||||
let next = false;
|
||||
try {
|
||||
const raw = await configApi.getCommonConfigSnippet("omo");
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
next = Object.keys(parsed).some(
|
||||
(k) => k !== "id" && k !== "updatedAt",
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
if (active) setUseOmoCommonConfig(next);
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [appId, category, isEditMode]);
|
||||
|
||||
const handleOmoGlobalConfigSave = useCallback(async () => {
|
||||
if (!omoGlobalConfigRef.current) return;
|
||||
setIsOmoSaving(true);
|
||||
try {
|
||||
const config = omoGlobalConfigRef.current.buildCurrentConfigStrict();
|
||||
await configApi.setCommonConfigSnippet("omo", JSON.stringify(config));
|
||||
setIsOmoConfigModalOpen(false);
|
||||
toast.success(
|
||||
t("omo.globalConfigSaved", { defaultValue: "Global config saved" }),
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(String(err));
|
||||
} finally {
|
||||
setIsOmoSaving(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const handleOmoEditClick = useCallback(() => {
|
||||
setOmoFieldsKey((k) => k + 1);
|
||||
setIsOmoConfigModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const resetOmoDraftState = useCallback((useCommonConfig = true) => {
|
||||
setOmoAgents({});
|
||||
setOmoCategories({});
|
||||
setOmoOtherFieldsStr("");
|
||||
setUseOmoCommonConfig(useCommonConfig);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
omoAgents,
|
||||
setOmoAgents,
|
||||
omoCategories,
|
||||
setOmoCategories,
|
||||
omoOtherFieldsStr,
|
||||
setOmoOtherFieldsStr,
|
||||
useOmoCommonConfig,
|
||||
setUseOmoCommonConfig,
|
||||
isOmoConfigModalOpen,
|
||||
setIsOmoConfigModalOpen,
|
||||
isOmoSaving,
|
||||
omoGlobalConfigRef,
|
||||
omoFieldsKey,
|
||||
effectiveOmoGlobalConfig,
|
||||
mergedOmoJsonPreview,
|
||||
handleOmoGlobalConfigSave,
|
||||
handleOmoEditClick,
|
||||
resetOmoDraftState,
|
||||
setOmoGlobalState,
|
||||
};
|
||||
}
|
||||
280
src/components/providers/forms/hooks/useOmoModelSource.ts
Normal file
280
src/components/providers/forms/hooks/useOmoModelSource.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { providersApi } from "@/lib/api";
|
||||
import { useProvidersQuery } from "@/lib/query/queries";
|
||||
import type { OpenCodeProviderConfig } from "@/types";
|
||||
import { OPENCODE_PRESET_MODEL_VARIANTS } from "@/config/opencodeProviderPresets";
|
||||
import { parseOpencodeConfigStrict } from "../helpers/opencodeFormUtils";
|
||||
|
||||
interface UseOmoModelSourceParams {
|
||||
isOmoCategory: boolean;
|
||||
providerId?: string;
|
||||
}
|
||||
|
||||
interface OmoModelBuild {
|
||||
options: Array<{ value: string; label: string }>;
|
||||
variantsMap: Record<string, string[]>;
|
||||
presetMetaMap: Record<
|
||||
string,
|
||||
{
|
||||
options?: Record<string, unknown>;
|
||||
limit?: { context?: number; output?: number };
|
||||
}
|
||||
>;
|
||||
parseFailedProviders: string[];
|
||||
usedFallbackSource: boolean;
|
||||
}
|
||||
|
||||
export interface OmoModelSourceResult {
|
||||
omoModelOptions: Array<{ value: string; label: string }>;
|
||||
omoModelVariantsMap: Record<string, string[]>;
|
||||
omoPresetMetaMap: Record<
|
||||
string,
|
||||
{
|
||||
options?: Record<string, unknown>;
|
||||
limit?: { context?: number; output?: number };
|
||||
}
|
||||
>;
|
||||
existingOpencodeKeys: string[];
|
||||
}
|
||||
|
||||
export function useOmoModelSource({
|
||||
isOmoCategory,
|
||||
providerId,
|
||||
}: UseOmoModelSourceParams): OmoModelSourceResult {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: opencodeProvidersData } = useProvidersQuery("opencode");
|
||||
const existingOpencodeKeys = useMemo(() => {
|
||||
if (!opencodeProvidersData?.providers) return [];
|
||||
return Object.keys(opencodeProvidersData.providers).filter(
|
||||
(k) => k !== providerId,
|
||||
);
|
||||
}, [opencodeProvidersData?.providers, providerId]);
|
||||
|
||||
const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState<
|
||||
string[] | null
|
||||
>(null);
|
||||
const [omoLiveIdsLoadFailed, setOmoLiveIdsLoadFailed] = useState(false);
|
||||
const lastOmoModelSourceWarningRef = useRef<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
if (!isOmoCategory) {
|
||||
setEnabledOpencodeProviderIds(null);
|
||||
setOmoLiveIdsLoadFailed(false);
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}
|
||||
|
||||
setEnabledOpencodeProviderIds(null);
|
||||
setOmoLiveIdsLoadFailed(false);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const ids = await providersApi.getOpenCodeLiveProviderIds();
|
||||
if (active) {
|
||||
setEnabledOpencodeProviderIds(ids);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[OMO_MODEL_SOURCE_LIVE_IDS_FAILED] failed to load live provider ids",
|
||||
error,
|
||||
);
|
||||
if (active) {
|
||||
setOmoLiveIdsLoadFailed(true);
|
||||
setEnabledOpencodeProviderIds(null);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [isOmoCategory]);
|
||||
|
||||
const omoModelBuild = useMemo<OmoModelBuild>(() => {
|
||||
const empty: OmoModelBuild = {
|
||||
options: [],
|
||||
variantsMap: {},
|
||||
presetMetaMap: {},
|
||||
parseFailedProviders: [],
|
||||
usedFallbackSource: false,
|
||||
};
|
||||
if (!isOmoCategory) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
const allProviders = opencodeProvidersData?.providers;
|
||||
if (!allProviders) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
const shouldFilterByLive = !omoLiveIdsLoadFailed;
|
||||
if (shouldFilterByLive && enabledOpencodeProviderIds === null) {
|
||||
return empty;
|
||||
}
|
||||
const liveSet =
|
||||
shouldFilterByLive && enabledOpencodeProviderIds
|
||||
? new Set(enabledOpencodeProviderIds)
|
||||
: null;
|
||||
|
||||
const dedupedOptions = new Map<string, string>();
|
||||
const variantsMap: Record<string, string[]> = {};
|
||||
const presetMetaMap: Record<
|
||||
string,
|
||||
{
|
||||
options?: Record<string, unknown>;
|
||||
limit?: { context?: number; output?: number };
|
||||
}
|
||||
> = {};
|
||||
const parseFailedProviders: string[] = [];
|
||||
|
||||
for (const [providerKey, provider] of Object.entries(allProviders)) {
|
||||
if (provider.category === "omo") {
|
||||
continue;
|
||||
}
|
||||
if (liveSet && !liveSet.has(providerKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsedConfig: OpenCodeProviderConfig;
|
||||
try {
|
||||
parsedConfig = parseOpencodeConfigStrict(provider.settingsConfig);
|
||||
} catch (error) {
|
||||
parseFailedProviders.push(providerKey);
|
||||
console.warn(
|
||||
"[OMO_MODEL_SOURCE_PARSE_FAILED] failed to parse provider settings",
|
||||
{
|
||||
providerKey,
|
||||
error,
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for (const [modelId, model] of Object.entries(
|
||||
parsedConfig.models || {},
|
||||
)) {
|
||||
const modelName =
|
||||
typeof model.name === "string" && model.name.trim()
|
||||
? model.name
|
||||
: modelId;
|
||||
const providerDisplayName =
|
||||
typeof provider.name === "string" && provider.name.trim()
|
||||
? provider.name
|
||||
: providerKey;
|
||||
const value = `${providerKey}/${modelId}`;
|
||||
const label = `${providerDisplayName} / ${modelName} (${modelId})`;
|
||||
if (!dedupedOptions.has(value)) {
|
||||
dedupedOptions.set(value, label);
|
||||
}
|
||||
|
||||
const rawVariants = model.variants;
|
||||
if (
|
||||
rawVariants &&
|
||||
typeof rawVariants === "object" &&
|
||||
!Array.isArray(rawVariants)
|
||||
) {
|
||||
const variantKeys = Object.keys(rawVariants).filter(Boolean);
|
||||
if (variantKeys.length > 0) {
|
||||
variantsMap[value] = variantKeys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preset fallback: for models without config-defined variants,
|
||||
// check if the npm package has preset variant definitions.
|
||||
// Also collect preset metadata (options, limit) for enrichment.
|
||||
const presetModels = OPENCODE_PRESET_MODEL_VARIANTS[parsedConfig.npm];
|
||||
if (presetModels) {
|
||||
for (const modelId of Object.keys(parsedConfig.models || {})) {
|
||||
const fullKey = `${providerKey}/${modelId}`;
|
||||
const preset = presetModels.find((p) => p.id === modelId);
|
||||
if (!preset) continue;
|
||||
|
||||
// Variant fallback
|
||||
if (!variantsMap[fullKey] && preset.variants) {
|
||||
const presetKeys = Object.keys(preset.variants).filter(Boolean);
|
||||
if (presetKeys.length > 0) {
|
||||
variantsMap[fullKey] = presetKeys;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect preset metadata for model enrichment
|
||||
const meta: (typeof presetMetaMap)[string] = {};
|
||||
if (preset.options) meta.options = preset.options;
|
||||
if (preset.contextLimit || preset.outputLimit) {
|
||||
meta.limit = {};
|
||||
if (preset.contextLimit) meta.limit.context = preset.contextLimit;
|
||||
if (preset.outputLimit) meta.limit.output = preset.outputLimit;
|
||||
}
|
||||
if (Object.keys(meta).length > 0) {
|
||||
presetMetaMap[fullKey] = meta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
options: Array.from(dedupedOptions.entries())
|
||||
.map(([value, label]) => ({ value, label }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN")),
|
||||
variantsMap,
|
||||
presetMetaMap,
|
||||
parseFailedProviders,
|
||||
usedFallbackSource: omoLiveIdsLoadFailed,
|
||||
};
|
||||
}, [
|
||||
isOmoCategory,
|
||||
opencodeProvidersData?.providers,
|
||||
enabledOpencodeProviderIds,
|
||||
omoLiveIdsLoadFailed,
|
||||
]);
|
||||
|
||||
// Warning toast for parse failures / fallback
|
||||
useEffect(() => {
|
||||
if (!isOmoCategory) return;
|
||||
const failed = omoModelBuild.parseFailedProviders;
|
||||
const fallback = omoModelBuild.usedFallbackSource;
|
||||
if (failed.length === 0 && !fallback) return;
|
||||
|
||||
const signature = `${fallback ? "fallback:" : ""}${failed
|
||||
.slice()
|
||||
.sort()
|
||||
.join(",")}`;
|
||||
if (lastOmoModelSourceWarningRef.current === signature) return;
|
||||
lastOmoModelSourceWarningRef.current = signature;
|
||||
|
||||
if (failed.length > 0) {
|
||||
toast.warning(
|
||||
t("omo.modelSourcePartialWarning", {
|
||||
count: failed.length,
|
||||
defaultValue:
|
||||
"Some provider model configs are invalid and were skipped.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (fallback) {
|
||||
toast.warning(
|
||||
t("omo.modelSourceFallbackWarning", {
|
||||
defaultValue:
|
||||
"Failed to load live provider state. Falling back to configured providers.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
isOmoCategory,
|
||||
omoModelBuild.parseFailedProviders,
|
||||
omoModelBuild.usedFallbackSource,
|
||||
t,
|
||||
]);
|
||||
|
||||
return {
|
||||
omoModelOptions: omoModelBuild.options,
|
||||
omoModelVariantsMap: omoModelBuild.variantsMap,
|
||||
omoPresetMetaMap: omoModelBuild.presetMetaMap,
|
||||
existingOpencodeKeys,
|
||||
};
|
||||
}
|
||||
180
src/components/providers/forms/hooks/useOpenclawFormState.ts
Normal file
180
src/components/providers/forms/hooks/useOpenclawFormState.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import type { OpenClawModel } from "@/types";
|
||||
import type { AppId } from "@/lib/api";
|
||||
import { useProvidersQuery } from "@/lib/query/queries";
|
||||
import { OPENCLAW_DEFAULT_CONFIG } from "../helpers/opencodeFormUtils";
|
||||
|
||||
interface UseOpenclawFormStateParams {
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
};
|
||||
appId: AppId;
|
||||
providerId?: string;
|
||||
onSettingsConfigChange: (config: string) => void;
|
||||
getSettingsConfig: () => string;
|
||||
}
|
||||
|
||||
export interface OpenclawFormState {
|
||||
openclawProviderKey: string;
|
||||
setOpenclawProviderKey: (key: string) => void;
|
||||
openclawBaseUrl: string;
|
||||
openclawApiKey: string;
|
||||
openclawApi: string;
|
||||
openclawModels: OpenClawModel[];
|
||||
existingOpenclawKeys: string[];
|
||||
handleOpenclawBaseUrlChange: (baseUrl: string) => void;
|
||||
handleOpenclawApiKeyChange: (apiKey: string) => void;
|
||||
handleOpenclawApiChange: (api: string) => void;
|
||||
handleOpenclawModelsChange: (models: OpenClawModel[]) => void;
|
||||
resetOpenclawState: (config?: {
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
api?: string;
|
||||
models?: OpenClawModel[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
function parseOpenclawField<T>(
|
||||
initialData: UseOpenclawFormStateParams["initialData"],
|
||||
field: string,
|
||||
fallback: T,
|
||||
): T {
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
initialData?.settingsConfig
|
||||
? JSON.stringify(initialData.settingsConfig)
|
||||
: OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
return (config[field] as T) || fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function useOpenclawFormState({
|
||||
initialData,
|
||||
appId,
|
||||
providerId,
|
||||
onSettingsConfigChange,
|
||||
getSettingsConfig,
|
||||
}: UseOpenclawFormStateParams): OpenclawFormState {
|
||||
// Query existing providers for duplicate key checking
|
||||
const { data: openclawProvidersData } = useProvidersQuery("openclaw");
|
||||
const existingOpenclawKeys = useMemo(() => {
|
||||
if (!openclawProvidersData?.providers) return [];
|
||||
return Object.keys(openclawProvidersData.providers).filter(
|
||||
(k) => k !== providerId,
|
||||
);
|
||||
}, [openclawProvidersData?.providers, providerId]);
|
||||
|
||||
const [openclawProviderKey, setOpenclawProviderKey] = useState<string>(() => {
|
||||
if (appId !== "openclaw") return "";
|
||||
return providerId || "";
|
||||
});
|
||||
|
||||
const [openclawBaseUrl, setOpenclawBaseUrl] = useState<string>(() => {
|
||||
if (appId !== "openclaw") return "";
|
||||
return parseOpenclawField(initialData, "baseUrl", "");
|
||||
});
|
||||
|
||||
const [openclawApiKey, setOpenclawApiKey] = useState<string>(() => {
|
||||
if (appId !== "openclaw") return "";
|
||||
return parseOpenclawField(initialData, "apiKey", "");
|
||||
});
|
||||
|
||||
const [openclawApi, setOpenclawApi] = useState<string>(() => {
|
||||
if (appId !== "openclaw") return "openai-completions";
|
||||
return parseOpenclawField(initialData, "api", "openai-completions");
|
||||
});
|
||||
|
||||
const [openclawModels, setOpenclawModels] = useState<OpenClawModel[]>(() => {
|
||||
if (appId !== "openclaw") return [];
|
||||
return parseOpenclawField<OpenClawModel[]>(initialData, "models", []);
|
||||
});
|
||||
|
||||
const updateOpenclawConfig = useCallback(
|
||||
(updater: (config: Record<string, any>) => void) => {
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
getSettingsConfig() || OPENCLAW_DEFAULT_CONFIG,
|
||||
);
|
||||
updater(config);
|
||||
onSettingsConfigChange(JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[getSettingsConfig, onSettingsConfigChange],
|
||||
);
|
||||
|
||||
const handleOpenclawBaseUrlChange = useCallback(
|
||||
(baseUrl: string) => {
|
||||
setOpenclawBaseUrl(baseUrl);
|
||||
updateOpenclawConfig((config) => {
|
||||
config.baseUrl = baseUrl.trim().replace(/\/+$/, "");
|
||||
});
|
||||
},
|
||||
[updateOpenclawConfig],
|
||||
);
|
||||
|
||||
const handleOpenclawApiKeyChange = useCallback(
|
||||
(apiKey: string) => {
|
||||
setOpenclawApiKey(apiKey);
|
||||
updateOpenclawConfig((config) => {
|
||||
config.apiKey = apiKey;
|
||||
});
|
||||
},
|
||||
[updateOpenclawConfig],
|
||||
);
|
||||
|
||||
const handleOpenclawApiChange = useCallback(
|
||||
(api: string) => {
|
||||
setOpenclawApi(api);
|
||||
updateOpenclawConfig((config) => {
|
||||
config.api = api;
|
||||
});
|
||||
},
|
||||
[updateOpenclawConfig],
|
||||
);
|
||||
|
||||
const handleOpenclawModelsChange = useCallback(
|
||||
(models: OpenClawModel[]) => {
|
||||
setOpenclawModels(models);
|
||||
updateOpenclawConfig((config) => {
|
||||
config.models = models;
|
||||
});
|
||||
},
|
||||
[updateOpenclawConfig],
|
||||
);
|
||||
|
||||
const resetOpenclawState = useCallback(
|
||||
(config?: {
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
api?: string;
|
||||
models?: OpenClawModel[];
|
||||
}) => {
|
||||
setOpenclawProviderKey("");
|
||||
setOpenclawBaseUrl(config?.baseUrl || "");
|
||||
setOpenclawApiKey(config?.apiKey || "");
|
||||
setOpenclawApi(config?.api || "openai-completions");
|
||||
setOpenclawModels(config?.models || []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
openclawProviderKey,
|
||||
setOpenclawProviderKey,
|
||||
openclawBaseUrl,
|
||||
openclawApiKey,
|
||||
openclawApi,
|
||||
openclawModels,
|
||||
existingOpenclawKeys,
|
||||
handleOpenclawBaseUrlChange,
|
||||
handleOpenclawApiKeyChange,
|
||||
handleOpenclawApiChange,
|
||||
handleOpenclawModelsChange,
|
||||
resetOpenclawState,
|
||||
};
|
||||
}
|
||||
192
src/components/providers/forms/hooks/useOpencodeFormState.ts
Normal file
192
src/components/providers/forms/hooks/useOpencodeFormState.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import type { OpenCodeModel, OpenCodeProviderConfig } from "@/types";
|
||||
import {
|
||||
OPENCODE_DEFAULT_NPM,
|
||||
OPENCODE_DEFAULT_CONFIG,
|
||||
isKnownOpencodeOptionKey,
|
||||
parseOpencodeConfig,
|
||||
toOpencodeExtraOptions,
|
||||
} from "../helpers/opencodeFormUtils";
|
||||
|
||||
interface UseOpencodeFormStateParams {
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
};
|
||||
appId: string;
|
||||
providerId?: string;
|
||||
onSettingsConfigChange: (config: string) => void;
|
||||
getSettingsConfig: () => string;
|
||||
}
|
||||
|
||||
export interface OpencodeFormState {
|
||||
opencodeProviderKey: string;
|
||||
setOpencodeProviderKey: (key: string) => void;
|
||||
opencodeNpm: string;
|
||||
opencodeApiKey: string;
|
||||
opencodeBaseUrl: string;
|
||||
opencodeModels: Record<string, OpenCodeModel>;
|
||||
opencodeExtraOptions: Record<string, string>;
|
||||
handleOpencodeNpmChange: (npm: string) => void;
|
||||
handleOpencodeApiKeyChange: (apiKey: string) => void;
|
||||
handleOpencodeBaseUrlChange: (baseUrl: string) => void;
|
||||
handleOpencodeModelsChange: (models: Record<string, OpenCodeModel>) => void;
|
||||
handleOpencodeExtraOptionsChange: (options: Record<string, string>) => void;
|
||||
resetOpencodeState: (config?: OpenCodeProviderConfig) => void;
|
||||
}
|
||||
|
||||
export function useOpencodeFormState({
|
||||
initialData,
|
||||
appId,
|
||||
providerId,
|
||||
onSettingsConfigChange,
|
||||
getSettingsConfig,
|
||||
}: UseOpencodeFormStateParams): OpencodeFormState {
|
||||
const initialOpencodeConfig =
|
||||
appId === "opencode"
|
||||
? parseOpencodeConfig(initialData?.settingsConfig)
|
||||
: null;
|
||||
const initialOpencodeOptions = initialOpencodeConfig?.options || {};
|
||||
|
||||
const [opencodeProviderKey, setOpencodeProviderKey] = useState<string>(() => {
|
||||
if (appId !== "opencode") return "";
|
||||
return providerId || "";
|
||||
});
|
||||
|
||||
const [opencodeNpm, setOpencodeNpm] = useState<string>(() => {
|
||||
if (appId !== "opencode") return OPENCODE_DEFAULT_NPM;
|
||||
return initialOpencodeConfig?.npm || OPENCODE_DEFAULT_NPM;
|
||||
});
|
||||
|
||||
const [opencodeApiKey, setOpencodeApiKey] = useState<string>(() => {
|
||||
if (appId !== "opencode") return "";
|
||||
const value = initialOpencodeOptions.apiKey;
|
||||
return typeof value === "string" ? value : "";
|
||||
});
|
||||
|
||||
const [opencodeBaseUrl, setOpencodeBaseUrl] = useState<string>(() => {
|
||||
if (appId !== "opencode") return "";
|
||||
const value = initialOpencodeOptions.baseURL;
|
||||
return typeof value === "string" ? value : "";
|
||||
});
|
||||
|
||||
const [opencodeModels, setOpencodeModels] = useState<
|
||||
Record<string, OpenCodeModel>
|
||||
>(() => {
|
||||
if (appId !== "opencode") return {};
|
||||
return initialOpencodeConfig?.models || {};
|
||||
});
|
||||
|
||||
const [opencodeExtraOptions, setOpencodeExtraOptions] = useState<
|
||||
Record<string, string>
|
||||
>(() => {
|
||||
if (appId !== "opencode") return {};
|
||||
return toOpencodeExtraOptions(initialOpencodeOptions);
|
||||
});
|
||||
|
||||
const updateOpencodeSettings = useCallback(
|
||||
(updater: (config: Record<string, any>) => void) => {
|
||||
try {
|
||||
const config = JSON.parse(
|
||||
getSettingsConfig() || OPENCODE_DEFAULT_CONFIG,
|
||||
) as Record<string, any>;
|
||||
updater(config);
|
||||
onSettingsConfigChange(JSON.stringify(config, null, 2));
|
||||
} catch {}
|
||||
},
|
||||
[getSettingsConfig, onSettingsConfigChange],
|
||||
);
|
||||
|
||||
const handleOpencodeNpmChange = useCallback(
|
||||
(npm: string) => {
|
||||
setOpencodeNpm(npm);
|
||||
updateOpencodeSettings((config) => {
|
||||
config.npm = npm;
|
||||
});
|
||||
},
|
||||
[updateOpencodeSettings],
|
||||
);
|
||||
|
||||
const handleOpencodeApiKeyChange = useCallback(
|
||||
(apiKey: string) => {
|
||||
setOpencodeApiKey(apiKey);
|
||||
updateOpencodeSettings((config) => {
|
||||
if (!config.options) config.options = {};
|
||||
config.options.apiKey = apiKey;
|
||||
});
|
||||
},
|
||||
[updateOpencodeSettings],
|
||||
);
|
||||
|
||||
const handleOpencodeBaseUrlChange = useCallback(
|
||||
(baseUrl: string) => {
|
||||
setOpencodeBaseUrl(baseUrl);
|
||||
updateOpencodeSettings((config) => {
|
||||
if (!config.options) config.options = {};
|
||||
config.options.baseURL = baseUrl.trim().replace(/\/+$/, "");
|
||||
});
|
||||
},
|
||||
[updateOpencodeSettings],
|
||||
);
|
||||
|
||||
const handleOpencodeModelsChange = useCallback(
|
||||
(models: Record<string, OpenCodeModel>) => {
|
||||
setOpencodeModels(models);
|
||||
updateOpencodeSettings((config) => {
|
||||
config.models = models;
|
||||
});
|
||||
},
|
||||
[updateOpencodeSettings],
|
||||
);
|
||||
|
||||
const handleOpencodeExtraOptionsChange = useCallback(
|
||||
(options: Record<string, string>) => {
|
||||
setOpencodeExtraOptions(options);
|
||||
updateOpencodeSettings((config) => {
|
||||
if (!config.options) config.options = {};
|
||||
|
||||
for (const k of Object.keys(config.options)) {
|
||||
if (!isKnownOpencodeOptionKey(k)) {
|
||||
delete config.options[k];
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(options)) {
|
||||
const trimmedKey = k.trim();
|
||||
if (trimmedKey && !trimmedKey.startsWith("option-")) {
|
||||
try {
|
||||
config.options[trimmedKey] = JSON.parse(v);
|
||||
} catch {
|
||||
config.options[trimmedKey] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[updateOpencodeSettings],
|
||||
);
|
||||
|
||||
const resetOpencodeState = useCallback((config?: OpenCodeProviderConfig) => {
|
||||
setOpencodeProviderKey("");
|
||||
setOpencodeNpm(config?.npm || OPENCODE_DEFAULT_NPM);
|
||||
setOpencodeBaseUrl(config?.options?.baseURL || "");
|
||||
setOpencodeApiKey(config?.options?.apiKey || "");
|
||||
setOpencodeModels(config?.models || {});
|
||||
setOpencodeExtraOptions(toOpencodeExtraOptions(config?.options || {}));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
opencodeProviderKey,
|
||||
setOpencodeProviderKey,
|
||||
opencodeNpm,
|
||||
opencodeApiKey,
|
||||
opencodeBaseUrl,
|
||||
opencodeModels,
|
||||
opencodeExtraOptions,
|
||||
handleOpencodeNpmChange,
|
||||
handleOpencodeApiKeyChange,
|
||||
handleOpencodeBaseUrlChange,
|
||||
handleOpencodeModelsChange,
|
||||
handleOpencodeExtraOptionsChange,
|
||||
resetOpencodeState,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user