mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-23 23:59:24 +08:00
feat(opencode): add extra options editor for SDK configuration
Add key-value pair editor for configuring additional SDK options like timeout, setCacheKey, etc. Values are automatically parsed to appropriate types (number, boolean, object) on save. - Add `extra` field with serde flatten in Rust backend - Add index signature to OpenCodeProviderOptions type - Create ExtraOptionKeyInput component with local state pattern - Place extra options section above models configuration
This commit is contained in:
@@ -521,6 +521,11 @@ pub struct OpenCodeProviderOptions {
|
||||
/// 自定义请求头
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
|
||||
/// 额外选项(timeout, setCacheKey 等)
|
||||
/// 使用 flatten 捕获所有未明确定义的字段
|
||||
#[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// OpenCode 模型定义
|
||||
|
||||
@@ -52,6 +52,45 @@ function ModelIdInput({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra option key input with local state to prevent focus loss.
|
||||
* Same pattern as ModelIdInput - use local state during editing,
|
||||
* only commit changes on blur.
|
||||
*/
|
||||
function ExtraOptionKeyInput({
|
||||
optionKey,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
optionKey: string;
|
||||
onChange: (newKey: string) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
// For new options with placeholder keys like "option-123", show empty string
|
||||
const displayValue = optionKey.startsWith("option-") ? "" : optionKey;
|
||||
const [localValue, setLocalValue] = useState(displayValue);
|
||||
|
||||
// Sync when external key changes
|
||||
useEffect(() => {
|
||||
setLocalValue(optionKey.startsWith("option-") ? "" : optionKey);
|
||||
}, [optionKey]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={localValue}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
const trimmed = localValue.trim();
|
||||
if (trimmed && trimmed !== optionKey) {
|
||||
onChange(trimmed);
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
className="flex-1"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface OpenCodeFormFieldsProps {
|
||||
// NPM Package
|
||||
npm: string;
|
||||
@@ -71,6 +110,10 @@ interface OpenCodeFormFieldsProps {
|
||||
// Models
|
||||
models: Record<string, OpenCodeModel>;
|
||||
onModelsChange: (models: Record<string, OpenCodeModel>) => void;
|
||||
|
||||
// Extra Options
|
||||
extraOptions: Record<string, string>;
|
||||
onExtraOptionsChange: (options: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
export function OpenCodeFormFields({
|
||||
@@ -85,6 +128,8 @@ export function OpenCodeFormFields({
|
||||
onBaseUrlChange,
|
||||
models,
|
||||
onModelsChange,
|
||||
extraOptions,
|
||||
onExtraOptionsChange,
|
||||
}: OpenCodeFormFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -126,6 +171,41 @@ export function OpenCodeFormFields({
|
||||
});
|
||||
};
|
||||
|
||||
// Extra Options handlers
|
||||
const handleAddExtraOption = () => {
|
||||
const newKey = `option-${Date.now()}`;
|
||||
onExtraOptionsChange({
|
||||
...extraOptions,
|
||||
[newKey]: "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveExtraOption = (key: string) => {
|
||||
const newOptions = { ...extraOptions };
|
||||
delete newOptions[key];
|
||||
onExtraOptionsChange(newOptions);
|
||||
};
|
||||
|
||||
const handleExtraOptionKeyChange = (oldKey: string, newKey: string) => {
|
||||
if (oldKey === newKey) return;
|
||||
const newOptions: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(extraOptions)) {
|
||||
if (k === oldKey) {
|
||||
newOptions[newKey.trim() || oldKey] = v;
|
||||
} else {
|
||||
newOptions[k] = v;
|
||||
}
|
||||
}
|
||||
onExtraOptionsChange(newOptions);
|
||||
};
|
||||
|
||||
const handleExtraOptionValueChange = (key: string, value: string) => {
|
||||
onExtraOptionsChange({
|
||||
...extraOptions,
|
||||
[key]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* NPM Package Selector */}
|
||||
@@ -187,6 +267,80 @@ export function OpenCodeFormFields({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Extra Options Editor */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>
|
||||
{t("opencode.extraOptions", { defaultValue: "额外选项" })}
|
||||
</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddExtraOption}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{t("opencode.addExtraOption", { defaultValue: "添加" })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{Object.keys(extraOptions).length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
{t("opencode.noExtraOptions", {
|
||||
defaultValue: "暂无额外选项",
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground px-1 mb-1">
|
||||
<span className="flex-1">
|
||||
{t("opencode.extraOptionKey", { defaultValue: "键名" })}
|
||||
</span>
|
||||
<span className="flex-1">
|
||||
{t("opencode.extraOptionValue", { defaultValue: "值" })}
|
||||
</span>
|
||||
<span className="w-9" />
|
||||
</div>
|
||||
{Object.entries(extraOptions).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<ExtraOptionKeyInput
|
||||
optionKey={key}
|
||||
onChange={(newKey) => handleExtraOptionKeyChange(key, newKey)}
|
||||
placeholder={t("opencode.extraOptionKeyPlaceholder", {
|
||||
defaultValue: "timeout",
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => handleExtraOptionValueChange(key, e.target.value)}
|
||||
placeholder={t("opencode.extraOptionValuePlaceholder", {
|
||||
defaultValue: "600000",
|
||||
})}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveExtraOption(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("opencode.extraOptionsHint", {
|
||||
defaultValue:
|
||||
"配置额外的 SDK 选项,如 timeout、setCacheKey 等。值会自动解析类型(数字、布尔值等)。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Models Editor */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -558,6 +558,26 @@ export function ProviderForm({
|
||||
}
|
||||
});
|
||||
|
||||
// OpenCode extra options state (e.g., timeout, setCacheKey)
|
||||
const [opencodeExtraOptions, setOpencodeExtraOptions] = useState<Record<string, string>>(() => {
|
||||
if (appId !== "opencode") return {};
|
||||
try {
|
||||
const config = JSON.parse(initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig) : OPENCODE_DEFAULT_CONFIG);
|
||||
const options = config.options || {};
|
||||
const extra: Record<string, string> = {};
|
||||
const knownKeys = ["baseURL", "apiKey", "headers"];
|
||||
for (const [k, v] of Object.entries(options)) {
|
||||
if (!knownKeys.includes(k)) {
|
||||
// Convert value to string for display
|
||||
extra[k] = typeof v === "string" ? v : JSON.stringify(v);
|
||||
}
|
||||
}
|
||||
return extra;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
// OpenCode handlers - sync state to form
|
||||
const handleOpencodeNpmChange = useCallback(
|
||||
(npm: string) => {
|
||||
@@ -617,6 +637,43 @@ export function ProviderForm({
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleOpencodeExtraOptionsChange = useCallback(
|
||||
(options: Record<string, string>) => {
|
||||
setOpencodeExtraOptions(options);
|
||||
try {
|
||||
const config = JSON.parse(form.getValues("settingsConfig") || OPENCODE_DEFAULT_CONFIG);
|
||||
if (!config.options) config.options = {};
|
||||
|
||||
// Remove old extra options (keep only known keys)
|
||||
const knownKeys = ["baseURL", "apiKey", "headers"];
|
||||
for (const k of Object.keys(config.options)) {
|
||||
if (!knownKeys.includes(k)) {
|
||||
delete config.options[k];
|
||||
}
|
||||
}
|
||||
|
||||
// Add new extra options (auto-parse value types)
|
||||
for (const [k, v] of Object.entries(options)) {
|
||||
const trimmedKey = k.trim();
|
||||
if (trimmedKey && !trimmedKey.startsWith("option-")) {
|
||||
try {
|
||||
// Try to parse as JSON (number, boolean, object, array)
|
||||
config.options[trimmedKey] = JSON.parse(v);
|
||||
} catch {
|
||||
// If parsing fails, keep as string
|
||||
config.options[trimmedKey] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||
|
||||
const handleSubmit = (values: ProviderFormData) => {
|
||||
@@ -925,6 +982,7 @@ export function ProviderForm({
|
||||
setOpencodeBaseUrl("");
|
||||
setOpencodeApiKey("");
|
||||
setOpencodeModels({});
|
||||
setOpencodeExtraOptions({});
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -993,6 +1051,17 @@ export function ProviderForm({
|
||||
setOpencodeApiKey(config.options?.apiKey || "");
|
||||
setOpencodeModels(config.models || {});
|
||||
|
||||
// Extract extra options from preset
|
||||
const options = config.options || {};
|
||||
const extra: Record<string, string> = {};
|
||||
const knownKeys = ["baseURL", "apiKey", "headers"];
|
||||
for (const [k, v] of Object.entries(options)) {
|
||||
if (!knownKeys.includes(k)) {
|
||||
extra[k] = typeof v === "string" ? v : JSON.stringify(v);
|
||||
}
|
||||
}
|
||||
setOpencodeExtraOptions(extra);
|
||||
|
||||
// Update form fields
|
||||
form.reset({
|
||||
name: preset.name,
|
||||
@@ -1199,6 +1268,8 @@ export function ProviderForm({
|
||||
onBaseUrlChange={handleOpencodeBaseUrlChange}
|
||||
models={opencodeModels}
|
||||
onModelsChange={handleOpencodeModelsChange}
|
||||
extraOptions={opencodeExtraOptions}
|
||||
onExtraOptionsChange={handleOpencodeExtraOptionsChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -481,7 +481,15 @@
|
||||
"providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.",
|
||||
"providerKeyRequired": "Provider key is required",
|
||||
"providerKeyDuplicate": "This key is already in use",
|
||||
"providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only."
|
||||
"providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.",
|
||||
"extraOptions": "Extra Options",
|
||||
"extraOptionsHint": "Configure extra SDK options like timeout, setCacheKey, etc. Values are auto-parsed to appropriate types (number, boolean, etc.).",
|
||||
"addExtraOption": "Add",
|
||||
"extraOptionKey": "Key",
|
||||
"extraOptionValue": "Value",
|
||||
"extraOptionKeyPlaceholder": "timeout",
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "No extra options configured"
|
||||
},
|
||||
"providerPreset": {
|
||||
"label": "Provider Preset",
|
||||
|
||||
@@ -481,7 +481,15 @@
|
||||
"providerKeyHint": "設定ファイルの一意の識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用できます。",
|
||||
"providerKeyRequired": "プロバイダーキーを入力してください",
|
||||
"providerKeyDuplicate": "このキーは既に使用されています",
|
||||
"providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用できます。"
|
||||
"providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用できます。",
|
||||
"extraOptions": "追加オプション",
|
||||
"extraOptionsHint": "timeout、setCacheKey などの SDK オプションを設定。値は自動的に適切な型(数値、真偽値など)に変換されます。",
|
||||
"addExtraOption": "追加",
|
||||
"extraOptionKey": "キー名",
|
||||
"extraOptionValue": "値",
|
||||
"extraOptionKeyPlaceholder": "timeout",
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "追加オプションはありません"
|
||||
},
|
||||
"providerPreset": {
|
||||
"label": "プロバイダータイプ",
|
||||
|
||||
@@ -481,7 +481,15 @@
|
||||
"providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符",
|
||||
"providerKeyRequired": "请填写供应商标识",
|
||||
"providerKeyDuplicate": "此标识已被使用,请更换",
|
||||
"providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符"
|
||||
"providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符",
|
||||
"extraOptions": "额外选项",
|
||||
"extraOptionsHint": "配置额外的 SDK 选项,如 timeout、setCacheKey 等。值会自动解析类型(数字、布尔值等)。",
|
||||
"addExtraOption": "添加",
|
||||
"extraOptionKey": "键名",
|
||||
"extraOptionValue": "值",
|
||||
"extraOptionKeyPlaceholder": "timeout",
|
||||
"extraOptionValuePlaceholder": "600000",
|
||||
"noExtraOptions": "暂无额外选项"
|
||||
},
|
||||
"providerPreset": {
|
||||
"label": "预设供应商",
|
||||
|
||||
@@ -269,6 +269,8 @@ export interface OpenCodeProviderOptions {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
// 支持额外选项(timeout, setCacheKey 等)
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// OpenCode 供应商配置(settings_config 结构)
|
||||
|
||||
Reference in New Issue
Block a user