From caba5f51ec383bfa22b4337b749a9c8a03c964f7 Mon Sep 17 00:00:00 2001 From: Dex Miller Date: Thu, 12 Feb 2026 21:30:59 +0800 Subject: [PATCH 01/51] feat(omo): improve agent model selection UX and fix lowercase keys (#1004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(omo): use lowercase keys for builtin agent definitions OMO config schema expects all agent keys to be lowercase. Updated OMO_BUILTIN_AGENTS keys (Sisyphus → sisyphus, Hephaestus → hephaestus, etc.) and aligned Rust test fixtures accordingly. * feat(omo): add i18n support and tooltips for agent/category descriptions * feat(omo): add preset model variants for thinking level support Add OPENCODE_PRESET_MODEL_VARIANTS constant with variant definitions for Google, OpenAI, and Anthropic models. The omoModelVariantsMap builder now falls back to presets when config-defined variants are absent, enabling the variant selector for supported models. * feat(omo): replace model select with searchable combobox and improve fallback handling * feat(omo): enrich preset model defaults and metadata fallback * fix(omo): preserve custom fields and align otherFields import/validation * fix: resolve omo clippy warnings and include app update --- package.json | 1 + pnpm-lock.yaml | 39 ++ src-tauri/src/commands/misc.rs | 2 +- src-tauri/src/services/omo.rs | 86 +++- src/App.tsx | 6 +- .../providers/forms/OmoFormFields.tsx | 356 ++++++++++++---- .../providers/forms/ProviderForm.tsx | 284 +++++++++---- src/components/ui/command.tsx | 103 +++++ src/components/ui/popover.tsx | 28 ++ src/config/opencodeProviderPresets.ts | 383 +++++++++++++++++- src/i18n/locales/en.json | 63 ++- src/i18n/locales/ja.json | 63 ++- src/i18n/locales/zh.json | 65 ++- src/types/omo.ts | 112 ++--- ...mFields.mergeCustomModelsIntoStore.test.ts | 111 +++++ tests/utils/omoConfig.test.ts | 50 +++ 16 files changed, 1511 insertions(+), 241 deletions(-) create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 tests/components/OmoFormFields.mergeCustomModelsIntoStore.test.ts create mode 100644 tests/utils/omoConfig.test.ts diff --git a/package.json b/package.json index 1bf485644..147441196 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65db84933..d82cfdc38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@radix-ui/react-label': specifier: ^2.1.7 version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.2.10 version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1048,6 +1051,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.8': resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} peerDependencies: @@ -3890,6 +3906,29 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index db229776b..e7c21471d 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -323,7 +323,7 @@ fn scan_cli_version(tool: &str) -> (Option, Option) { let mut search_paths: Vec = vec![ home.join(".local/bin"), // Native install (official recommended) home.join(".npm-global/bin"), - home.join("n/bin"), // n version manager + home.join("n/bin"), // n version manager home.join(".volta/bin"), // Volta package manager ]; diff --git a/src-tauri/src/services/omo.rs b/src-tauri/src/services/omo.rs index 45ed9479b..2e9cf4905 100644 --- a/src-tauri/src/services/omo.rs +++ b/src-tauri/src/services/omo.rs @@ -286,10 +286,22 @@ impl OmoService { let obj = Self::read_jsonc_object(&actual_path)?; + Ok(Self::build_local_file_data_from_obj( + &obj, + actual_path.to_string_lossy().to_string(), + last_modified, + )) + } + + fn build_local_file_data_from_obj( + obj: &Map, + file_path: String, + last_modified: Option, + ) -> OmoLocalFileData { let agents = obj.get("agents").cloned(); let categories = obj.get("categories").cloned(); - let other = Self::extract_other_fields(&obj); + let other = Self::extract_other_fields(obj); let other_fields = if other.is_empty() { None } else { @@ -297,16 +309,17 @@ impl OmoService { }; let mut global = OmoGlobalConfig::default(); - Self::merge_global_from_obj(&obj, &mut global); + Self::merge_global_from_obj(obj, &mut global); + global.other_fields = other_fields.clone(); - Ok(OmoLocalFileData { + OmoLocalFileData { agents, categories, other_fields, global, - file_path: actual_path.to_string_lossy().to_string(), + file_path, last_modified, - }) + } } fn strip_jsonc_comments(input: &str) -> String { @@ -400,7 +413,7 @@ mod tests { ..Default::default() }; let agents = Some(serde_json::json!({ - "Sisyphus": { "model": "claude-opus-4-5" } + "sisyphus": { "model": "claude-opus-4-5" } })); let categories = None; let other_fields = None; @@ -411,7 +424,7 @@ mod tests { assert_eq!(obj["$schema"], "https://example.com/schema.json"); assert_eq!(obj["disabled_agents"], serde_json::json!(["explore"])); assert!(obj.contains_key("agents")); - assert_eq!(obj["agents"]["Sisyphus"]["model"], "claude-opus-4-5"); + assert_eq!(obj["agents"]["sisyphus"]["model"], "claude-opus-4-5"); } #[test] @@ -422,7 +435,7 @@ mod tests { ..Default::default() }; let agents = Some(serde_json::json!({ - "Sisyphus": { "model": "claude-opus-4-5" } + "sisyphus": { "model": "claude-opus-4-5" } })); let categories = None; let other_fields = None; @@ -434,4 +447,61 @@ mod tests { assert!(!obj.contains_key("disabled_agents")); assert!(obj.contains_key("agents")); } + + #[test] + fn test_build_local_file_data_keeps_unknown_top_level_fields_in_global() { + let obj = serde_json::json!({ + "$schema": "https://example.com/schema.json", + "disabled_agents": ["oracle"], + "agents": { + "sisyphus": { "model": "claude-opus-4-6" } + }, + "categories": { + "code": { "model": "gpt-5.3" } + }, + "custom_top_level": { + "enabled": true + } + }); + let obj_map = obj.as_object().unwrap().clone(); + + let data = OmoService::build_local_file_data_from_obj( + &obj_map, + "/tmp/oh-my-opencode.jsonc".to_string(), + None, + ); + + assert_eq!( + data.global.schema_url.as_deref(), + Some("https://example.com/schema.json") + ); + assert_eq!(data.global.disabled_agents, vec!["oracle".to_string()]); + + assert_eq!( + data.other_fields, + Some(serde_json::json!({ + "custom_top_level": { "enabled": true } + })) + ); + assert_eq!(data.global.other_fields, data.other_fields); + } + + #[test] + fn test_merge_config_ignores_non_object_other_fields() { + let global = OmoGlobalConfig { + other_fields: Some(serde_json::json!(["global_non_object"])), + ..Default::default() + }; + let agents = None; + let categories = None; + let other_fields = Some(serde_json::json!("profile_non_object")); + let profile_data = (agents, categories, other_fields, true); + + let merged = OmoService::merge_config(&global, Some(&profile_data)); + let obj = merged.as_object().unwrap(); + + assert!(!obj.contains_key("0")); + assert!(!obj.contains_key("global_non_object")); + assert!(!obj.contains_key("profile_non_object")); + } } diff --git a/src/App.tsx b/src/App.tsx index 9f758f8ba..4f0d47785 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -141,7 +141,11 @@ function App() { // Fallback from sessions view when switching to an app without session support useEffect(() => { - if (currentView === "sessions" && activeApp !== "claude" && activeApp !== "codex") { + if ( + currentView === "sessions" && + activeApp !== "claude" && + activeApp !== "codex" + ) { setCurrentView("providers"); } }, [activeApp, currentView]); diff --git a/src/components/providers/forms/OmoFormFields.tsx b/src/components/providers/forms/OmoFormFields.tsx index 2ee356a3c..b4b859ba8 100644 --- a/src/components/providers/forms/OmoFormFields.tsx +++ b/src/components/providers/forms/OmoFormFields.tsx @@ -12,6 +12,19 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; import { Plus, Trash2, @@ -21,6 +34,10 @@ import { Settings, FolderInput, Loader2, + HelpCircle, + Check, + ChevronsUpDown, + X, } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -43,6 +60,13 @@ const ADVANCED_PLACEHOLDER = `{ interface OmoFormFieldsProps { modelOptions: Array<{ value: string; label: string }>; modelVariantsMap?: Record; + presetMetaMap?: Record< + string, + { + options?: Record; + limit?: { context?: number; output?: number }; + } + >; agents: Record>; onAgentsChange: (agents: Record>) => void; categories: Record>; @@ -53,19 +77,149 @@ interface OmoFormFieldsProps { onOtherFieldsStrChange: (value: string) => void; } -type CustomModelItem = { key: string; model: string }; +export type CustomModelItem = { + key: string; + model: string; + sourceKey?: string; +}; type BuiltinModelDef = Pick< OmoAgentDef | OmoCategoryDef, - "key" | "display" | "descZh" | "descEn" | "recommended" + "key" | "display" | "descKey" | "recommended" | "tooltipKey" >; type ModelOption = { value: string; label: string }; +function DeferredKeyInput({ + value, + onCommit, + placeholder, + className, +}: { + value: string; + onCommit: (value: string) => void; + placeholder?: string; + className?: string; +}) { + const [draft, setDraft] = useState(value); + + useEffect(() => { + setDraft(value); + }, [value]); + + return ( + setDraft(e.target.value)} + onBlur={() => { + if (draft !== value) { + onCommit(draft); + } + }} + placeholder={placeholder} + className={className} + /> + ); +} + const BUILTIN_AGENT_KEYS = new Set(OMO_BUILTIN_AGENTS.map((a) => a.key)); const BUILTIN_CATEGORY_KEYS = new Set(OMO_BUILTIN_CATEGORIES.map((c) => c.key)); -const EMPTY_MODEL_VALUE = "__cc_switch_omo_model_empty__"; -const UNAVAILABLE_MODEL_VALUE = "__cc_switch_omo_model_unavailable__"; const EMPTY_VARIANT_VALUE = "__cc_switch_omo_variant_empty__"; -const UNAVAILABLE_VARIANT_VALUE = "__cc_switch_omo_variant_unavailable__"; + +function ModelCombobox({ + value, + options, + recommended, + onChange, +}: { + value: string; + options: ModelOption[]; + recommended?: string; + onChange: (value: string) => void; +}) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const selectedLabel = options.find((o) => o.value === value)?.label; + + const selectModelText = t("omo.selectModel", { + defaultValue: "Select configured model", + }); + const placeholderText = recommended + ? `${selectModelText} (${t("omo.recommendedHint", { model: recommended, defaultValue: "Recommended: {{model}}" })})` + : selectModelText; + + return ( + + + + + + + + + + {t("omo.noEnabledModels", { + defaultValue: "No configured models", + })} + + + {options.map((option) => ( + { + onChange(option.value); + setOpen(false); + }} + > + + {option.label} + + ))} + + + + + + ); +} function getAdvancedStr(config: Record | undefined): string { if (!config) return ""; @@ -86,24 +240,58 @@ function collectCustomModels( customs.push({ key: k, model: ((v as Record).model as string) || "", + sourceKey: k, }); } } return customs; } -function mergeCustomModelsIntoStore( +export function mergeCustomModelsIntoStore( store: Record>, builtinKeys: Set, customs: CustomModelItem[], + modelVariantsMap: Record, ): Record> { - const updated = { ...store }; - for (const key of Object.keys(updated)) { - if (!builtinKeys.has(key)) delete updated[key]; + const updated: Record> = {}; + + for (const [key, value] of Object.entries(store)) { + if (builtinKeys.has(key)) { + updated[key] = { ...value }; + } } + for (const custom of customs) { - if (custom.key.trim()) { - updated[custom.key] = { ...updated[custom.key], model: custom.model }; + const targetKey = custom.key.trim(); + if (!targetKey) continue; + + const sourceKey = (custom.sourceKey || targetKey).trim(); + const sourceEntry = store[sourceKey] ?? store[targetKey]; + const nextEntry = { + ...(updated[targetKey] || {}), + ...(sourceEntry || {}), + }; + + if (custom.model.trim()) { + nextEntry.model = custom.model; + const currentVariant = + typeof nextEntry.variant === "string" ? nextEntry.variant : ""; + if (currentVariant) { + const validVariants = modelVariantsMap[custom.model] || []; + if (!validVariants.includes(currentVariant)) { + delete nextEntry.variant; + } + } + updated[targetKey] = nextEntry; + continue; + } + + delete nextEntry.model; + delete nextEntry.variant; + if (Object.keys(nextEntry).length > 0) { + updated[targetKey] = nextEntry; + } else { + delete updated[targetKey]; } } return updated; @@ -112,6 +300,7 @@ function mergeCustomModelsIntoStore( export function OmoFormFields({ modelOptions, modelVariantsMap = {}, + presetMetaMap: _presetMetaMap = {}, agents, onAgentsChange, categories, @@ -119,8 +308,7 @@ export function OmoFormFields({ otherFieldsStr, onOtherFieldsStrChange, }: OmoFormFieldsProps) { - const { t, i18n } = useTranslation(); - const isZh = i18n.language?.startsWith("zh"); + const { t } = useTranslation(); const [mainAgentsOpen, setMainAgentsOpen] = useState(true); const [subAgentsOpen, setSubAgentsOpen] = useState(true); @@ -159,19 +347,29 @@ export function OmoFormFields({ const syncCustomAgents = useCallback( (customs: CustomModelItem[]) => { onAgentsChange( - mergeCustomModelsIntoStore(agents, BUILTIN_AGENT_KEYS, customs), + mergeCustomModelsIntoStore( + agents, + BUILTIN_AGENT_KEYS, + customs, + modelVariantsMap, + ), ); }, - [agents, onAgentsChange], + [agents, onAgentsChange, modelVariantsMap], ); const syncCustomCategories = useCallback( (customs: CustomModelItem[]) => { onCategoriesChange( - mergeCustomModelsIntoStore(categories, BUILTIN_CATEGORY_KEYS, customs), + mergeCustomModelsIntoStore( + categories, + BUILTIN_CATEGORY_KEYS, + customs, + modelVariantsMap, + ), ); }, - [categories, onCategoriesChange], + [categories, onCategoriesChange, modelVariantsMap], ); const buildEffectiveModelOptions = useCallback( @@ -212,43 +410,16 @@ export function OmoFormFields({ const renderModelSelect = ( currentModel: string, onChange: (value: string) => void, - placeholder?: string, + recommended?: string, ) => { const options = buildEffectiveModelOptions(currentModel); return ( - + ); }; @@ -268,11 +439,21 @@ export function OmoFormFields({ currentVariant: string, onChange: (value: string) => void, ) => { + const hasModel = Boolean(currentModel); + const modelVariantKeys = hasModel + ? modelVariantsMap[currentModel] || [] + : []; + const hasVariants = modelVariantKeys.length > 0; + const shouldShow = hasModel && (hasVariants || Boolean(currentVariant)); + + if (!shouldShow) { + return null; + } + const variantOptions = buildEffectiveVariantOptions( currentModel, currentVariant, ); - const hasModel = Boolean(currentModel); const firstIsUnavailable = Boolean(currentVariant) && !(modelVariantsMap[currentModel] || []).includes(currentVariant); @@ -283,9 +464,8 @@ export function OmoFormFields({ onValueChange={(value) => onChange(value === EMPTY_VARIANT_VALUE ? "" : value) } - disabled={!hasModel} > - + {t("omo.defaultWrapped", { defaultValue: "(Default)" })} - {!hasModel ? ( - - {t("omo.selectModelFirst", { - defaultValue: "Select model first", - })} + {variantOptions.map((variant, index) => ( + + {firstIsUnavailable && index === 0 + ? t("omo.currentValueUnavailable", { + value: variant, + defaultValue: "{{value}} (current value, unavailable)", + }) + : variant} - ) : variantOptions.length === 0 ? ( - - {t("omo.noVariantsForModel", { - defaultValue: "No variants for model", - })} - - ) : ( - variantOptions.map((variant, index) => ( - - {firstIsUnavailable && index === 0 - ? t("omo.currentValueUnavailable", { - value: variant, - defaultValue: "{{value}} (current value, unavailable)", - }) - : variant} - - )) - )} + ))} ); @@ -536,7 +702,7 @@ export function OmoFormFields({ toast.warning( t("omo.noEnabledModelsWarning", { defaultValue: - "No enabled models available. Configure and enable OpenCode models first.", + "No configured models available. Configure OpenCode models first.", }), ); return; @@ -641,9 +807,17 @@ export function OmoFormFields({
-
{def.display}
+
+ {def.display} + + + + {t(def.tooltipKey)} + + +
- {isZh ? def.descZh : def.descEn} + {t(def.descKey)}
{renderModelSelect( @@ -727,16 +901,14 @@ export function OmoFormFields({ className="border-b border-border/30 last:border-b-0" >
- updateCustom({ key: e.target.value })} + onCommit={(value) => updateCustom({ key: value })} placeholder={keyPlaceholder} className="w-32 shrink-0 h-8 text-sm text-primary" /> - {renderModelSelect( - item.model, - (value) => updateCustom({ model: value }), - t("omo.modelNamePlaceholder", { defaultValue: "model-name" }), + {renderModelSelect(item.model, (value) => + updateCustom({ model: value }), )} {renderVariantSelect(item.model, currentVariant, (value) => { if (!item.key) return; @@ -877,11 +1049,17 @@ export function OmoFormFields({ const addCustomModel = (scope: AdvancedScope) => { if (scope === "agent") { - setCustomAgents((prev) => [...prev, { key: "", model: "" }]); + setCustomAgents((prev) => [ + ...prev, + { key: "", model: "", sourceKey: "" }, + ]); setSubAgentsOpen(true); return; } - setCustomCategories((prev) => [...prev, { key: "", model: "" }]); + setCustomCategories((prev) => [ + ...prev, + { key: "", model: "", sourceKey: "" }, + ]); setCategoriesOpen(true); }; @@ -931,7 +1109,7 @@ export function OmoFormFields({ ·{" "} {t("omo.enabledModelsCount", { count: modelOptions.length, - defaultValue: "{{count}} enabled models available", + defaultValue: "{{count}} configured models available", })} {localFilePath && ( diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index b60e18a86..381a7d68c 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -31,6 +31,7 @@ import { } from "@/config/geminiProviderPresets"; import { opencodeProviderPresets, + OPENCODE_PRESET_MODEL_VARIANTS, type OpenCodeProviderPreset, } from "@/config/opencodeProviderPresets"; import { OpenCodeFormFields } from "./OpenCodeFormFields"; @@ -53,7 +54,7 @@ import { type OmoGlobalConfigFieldsRef } from "./OmoGlobalConfigFields"; import { OmoCommonConfigEditor } from "./OmoCommonConfigEditor"; import * as configApi from "@/lib/api/config"; import type { OmoGlobalConfig } from "@/types/omo"; -import { mergeOmoConfigPreview } from "@/types/omo"; +import { mergeOmoConfigPreview, parseOmoOtherFieldsObject } from "@/types/omo"; import { ProviderAdvancedConfig, type PricingModelSourceOption, @@ -112,21 +113,25 @@ const isKnownOpencodeOptionKey = (key: string) => function parseOpencodeConfig( settingsConfig?: Record, ): OpenCodeProviderConfig { + const normalize = ( + parsed: Partial, + ): 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) + : {}, + }); + try { const parsed = JSON.parse( settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG, ) as Partial; - 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) - : {}, - }; + return normalize(parsed); } catch { return { npm: OPENCODE_DEFAULT_NPM, @@ -136,6 +141,25 @@ function parseOpencodeConfig( } } +function parseOpencodeConfigStrict( + settingsConfig?: Record, +): OpenCodeProviderConfig { + const parsed = JSON.parse( + settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG, + ) as Partial; + 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) + : {}, + }; +} + function toOpencodeExtraOptions( options: OpenCodeProviderConfig["options"], ): Record { @@ -195,8 +219,10 @@ function buildOmoProfilePreview( } if (otherFieldsStr.trim()) { try { - const other = JSON.parse(otherFieldsStr); - Object.assign(profileOnly, other); + const other = parseOmoOtherFieldsObject(otherFieldsStr); + if (other) { + Object.assign(profileOnly, other); + } } catch {} } return profileOnly; @@ -580,19 +606,24 @@ export function ProviderForm({ ); }, [opencodeProvidersData?.providers, providerId]); const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState< - string[] - >([]); + string[] | null + >(null); + const [omoLiveIdsLoadFailed, setOmoLiveIdsLoadFailed] = useState(false); + const lastOmoModelSourceWarningRef = useRef(""); useEffect(() => { let active = true; - if (!isOmoCategory) { - setEnabledOpencodeProviderIds([]); + setEnabledOpencodeProviderIds(null); + setOmoLiveIdsLoadFailed(false); return () => { active = false; }; } + setEnabledOpencodeProviderIds(null); + setOmoLiveIdsLoadFailed(false); + (async () => { try { const ids = await providersApi.getOpenCodeLiveProviderIds(); @@ -600,9 +631,13 @@ export function ProviderForm({ setEnabledOpencodeProviderIds(ids); } } catch (error) { - console.error("Failed to load OpenCode live provider ids:", error); + console.warn( + "[OMO_MODEL_SOURCE_LIVE_IDS_FAILED] failed to load live provider ids", + error, + ); if (active) { - setEnabledOpencodeProviderIds([]); + setOmoLiveIdsLoadFailed(true); + setEnabledOpencodeProviderIds(null); } } })(); @@ -612,23 +647,71 @@ export function ProviderForm({ }; }, [isOmoCategory]); - const omoModelOptions = useMemo(() => { - if (!isOmoCategory) return []; + const omoModelBuild = useMemo(() => { + const empty = { + options: [] as Array<{ value: string; label: string }>, + variantsMap: {} as Record, + presetMetaMap: {} as Record< + string, + { + options?: Record; + limit?: { context?: number; output?: number }; + } + >, + parseFailedProviders: [] as string[], + usedFallbackSource: false, + }; + if (!isOmoCategory) { + return empty; + } const allProviders = opencodeProvidersData?.providers; - if (!allProviders) return []; + if (!allProviders) { + return empty; + } - const enabledSet = new Set(enabledOpencodeProviderIds); - if (enabledSet.size === 0) return []; + const shouldFilterByLive = !omoLiveIdsLoadFailed; + if (shouldFilterByLive && enabledOpencodeProviderIds === null) { + return empty; + } + const liveSet = + shouldFilterByLive && enabledOpencodeProviderIds + ? new Set(enabledOpencodeProviderIds) + : null; const dedupedOptions = new Map(); + const variantsMap: Record = {}; + const presetMetaMap: Record< + string, + { + options?: Record; + limit?: { context?: number; output?: number }; + } + > = {}; + const parseFailedProviders: string[] = []; for (const [providerKey, provider] of Object.entries(allProviders)) { - if (provider.category === "omo" || !enabledSet.has(providerKey)) { + if (provider.category === "omo") { + continue; + } + if (liveSet && !liveSet.has(providerKey)) { continue; } - const parsedConfig = parseOpencodeConfig(provider.settingsConfig); + 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 || {}, )) { @@ -645,63 +728,107 @@ export function ProviderForm({ 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")); - }, [ - isOmoCategory, - opencodeProvidersData?.providers, - enabledOpencodeProviderIds, - ]); - const omoModelVariantsMap = useMemo(() => { - const variantsMap: Record = {}; - if (!isOmoCategory) { - return variantsMap; - } - - const allProviders = opencodeProvidersData?.providers; - if (!allProviders) { - return variantsMap; - } - - const enabledSet = new Set(enabledOpencodeProviderIds); - if (enabledSet.size === 0) { - return variantsMap; - } - - for (const [providerKey, provider] of Object.entries(allProviders)) { - if (provider.category === "omo" || !enabledSet.has(providerKey)) { - continue; - } - - const parsedConfig = parseOpencodeConfig(provider.settingsConfig); - for (const [modelId, model] of Object.entries( - parsedConfig.models || {}, - )) { const rawVariants = model.variants; if ( - !rawVariants || - typeof rawVariants !== "object" || - Array.isArray(rawVariants) + rawVariants && + typeof rawVariants === "object" && + !Array.isArray(rawVariants) ) { - continue; + const variantKeys = Object.keys(rawVariants).filter(Boolean); + if (variantKeys.length > 0) { + variantsMap[value] = variantKeys; + } } - const variantKeys = Object.keys(rawVariants).filter(Boolean); - if (variantKeys.length === 0) { - continue; + } + + // 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; + } } - variantsMap[`${providerKey}/${modelId}`] = variantKeys; } } - return variantsMap; + 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, + ]); + const omoModelOptions = omoModelBuild.options; + const omoModelVariantsMap = omoModelBuild.variantsMap; + const omoPresetMetaMap = omoModelBuild.presetMetaMap; + + 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, ]); const initialOmoSettings = @@ -1078,7 +1205,19 @@ export function ProviderForm({ } if (omoOtherFieldsStr.trim()) { try { - omoConfig.otherFields = JSON.parse(omoOtherFieldsStr); + const otherFields = parseOmoOtherFieldsObject(omoOtherFieldsStr); + if (!otherFields) { + toast.error( + t("omo.jsonMustBeObject", { + field: t("omo.otherFields", { + defaultValue: "Other Config", + }), + defaultValue: "{{field}} must be a JSON object", + }), + ); + return; + } + omoConfig.otherFields = otherFields; } catch { toast.error( t("omo.invalidJson", { @@ -1603,6 +1742,7 @@ export function ProviderForm({ , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +const CommandInput = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandItem = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandItem.displayName = CommandPrimitive.Item.displayName; + +export { + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, +}; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 000000000..9f00cbca2 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import { cn } from "@/lib/utils"; + +const Popover = PopoverPrimitive.Root; +const PopoverTrigger = PopoverPrimitive.Trigger; +const PopoverAnchor = PopoverPrimitive.Anchor; + +const PopoverContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, align = "start", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 26e71a2ad..c41ce2b25 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -24,6 +24,384 @@ export const opencodeNpmPackages = [ { value: "@ai-sdk/google", label: "Google (Gemini)" }, ] as const; +export interface PresetModelVariant { + id: string; + name?: string; + contextLimit?: number; + outputLimit?: number; + modalities?: { input: string[]; output: string[] }; + options?: Record; + variants?: Record>; +} + +export const OPENCODE_PRESET_MODEL_VARIANTS: Record< + string, + PresetModelVariant[] +> = { + "@ai-sdk/openai-compatible": [ + { + id: "MiniMax-M2.1", + name: "MiniMax M2.1", + contextLimit: 204800, + outputLimit: 131072, + modalities: { input: ["text"], output: ["text"] }, + }, + { + id: "glm-4.7", + name: "GLM 4.7", + contextLimit: 204800, + outputLimit: 131072, + modalities: { input: ["text"], output: ["text"] }, + }, + { + id: "kimi-k2.5", + name: "Kimi K2.5", + contextLimit: 262144, + outputLimit: 262144, + modalities: { input: ["text", "image", "video"], output: ["text"] }, + }, + ], + "@ai-sdk/google": [ + { + id: "gemini-2.5-flash-lite", + name: "Gemini 2.5 Flash Lite", + contextLimit: 1048576, + outputLimit: 65536, + modalities: { + input: ["text", "image", "pdf", "video", "audio"], + output: ["text"], + }, + variants: { + auto: { + thinkingConfig: { includeThoughts: true, thinkingBudget: -1 }, + }, + "no-thinking": { thinkingConfig: { thinkingBudget: 0 } }, + }, + }, + { + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + contextLimit: 1048576, + outputLimit: 65536, + modalities: { + input: ["text", "image", "pdf", "video", "audio"], + output: ["text"], + }, + variants: { + minimal: { + thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" }, + }, + low: { + thinkingConfig: { includeThoughts: true, thinkingLevel: "low" }, + }, + medium: { + thinkingConfig: { includeThoughts: true, thinkingLevel: "medium" }, + }, + high: { + thinkingConfig: { includeThoughts: true, thinkingLevel: "high" }, + }, + }, + }, + { + id: "gemini-3-pro-preview", + name: "Gemini 3 Pro Preview", + contextLimit: 1048576, + outputLimit: 65536, + modalities: { + input: ["text", "image", "pdf", "video", "audio"], + output: ["text"], + }, + variants: { + low: { + thinkingConfig: { includeThoughts: true, thinkingLevel: "low" }, + }, + high: { + thinkingConfig: { includeThoughts: true, thinkingLevel: "high" }, + }, + }, + }, + ], + "@ai-sdk/openai": [ + { + id: "gpt-5", + name: "GPT-5", + contextLimit: 400000, + outputLimit: 128000, + modalities: { input: ["text", "image"], output: ["text"] }, + variants: { + low: { + reasoningEffort: "low", + reasoningSummary: "auto", + textVerbosity: "low", + }, + medium: { + reasoningEffort: "medium", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + high: { + reasoningEffort: "high", + reasoningSummary: "auto", + textVerbosity: "high", + }, + }, + }, + { + id: "gpt-5.1", + name: "GPT-5.1", + contextLimit: 400000, + outputLimit: 272000, + modalities: { input: ["text", "image"], output: ["text"] }, + variants: { + low: { + reasoningEffort: "low", + reasoningSummary: "auto", + textVerbosity: "low", + }, + medium: { + reasoningEffort: "medium", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + high: { + reasoningEffort: "high", + reasoningSummary: "auto", + textVerbosity: "high", + }, + }, + }, + { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + contextLimit: 400000, + outputLimit: 128000, + modalities: { input: ["text", "image"], output: ["text"] }, + options: { include: ["reasoning.encrypted_content"], store: false }, + variants: { + low: { + reasoningEffort: "low", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + medium: { + reasoningEffort: "medium", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + high: { + reasoningEffort: "high", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + }, + }, + { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + contextLimit: 400000, + outputLimit: 128000, + modalities: { input: ["text", "image"], output: ["text"] }, + options: { include: ["reasoning.encrypted_content"], store: false }, + variants: { + low: { + reasoningEffort: "low", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + medium: { + reasoningEffort: "medium", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + high: { + reasoningEffort: "high", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + xhigh: { + reasoningEffort: "xhigh", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + }, + }, + { + id: "gpt-5.2", + name: "GPT-5.2", + contextLimit: 400000, + outputLimit: 128000, + modalities: { input: ["text", "image"], output: ["text"] }, + variants: { + low: { + reasoningEffort: "low", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + medium: { + reasoningEffort: "medium", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + high: { + reasoningEffort: "high", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + xhigh: { + reasoningEffort: "xhigh", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + }, + }, + { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + contextLimit: 400000, + outputLimit: 128000, + modalities: { input: ["text", "image"], output: ["text"] }, + options: { include: ["reasoning.encrypted_content"], store: false }, + variants: { + low: { + reasoningEffort: "low", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + medium: { + reasoningEffort: "medium", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + high: { + reasoningEffort: "high", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + xhigh: { + reasoningEffort: "xhigh", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + }, + }, + { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + contextLimit: 400000, + outputLimit: 128000, + modalities: { input: ["text", "image"], output: ["text"] }, + options: { include: ["reasoning.encrypted_content"], store: false }, + variants: { + low: { + reasoningEffort: "low", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + medium: { + reasoningEffort: "medium", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + high: { + reasoningEffort: "high", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + xhigh: { + reasoningEffort: "xhigh", + reasoningSummary: "auto", + textVerbosity: "medium", + }, + }, + }, + ], + "@ai-sdk/anthropic": [ + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextLimit: 200000, + outputLimit: 64000, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + variants: { + low: { effort: "low" }, + medium: { effort: "medium" }, + high: { effort: "high" }, + }, + }, + { + id: "claude-opus-4-5-20251101", + name: "Claude Opus 4.5", + contextLimit: 200000, + outputLimit: 64000, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + variants: { + low: { thinking: { budgetTokens: 5000, type: "enabled" } }, + medium: { thinking: { budgetTokens: 13000, type: "enabled" } }, + high: { thinking: { budgetTokens: 18000, type: "enabled" } }, + }, + }, + { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + contextLimit: 1000000, + outputLimit: 128000, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + variants: { + low: { effort: "low" }, + medium: { effort: "medium" }, + high: { effort: "high" }, + max: { effort: "max" }, + }, + }, + { + id: "claude-haiku-4-5-20251001", + name: "Claude Haiku 4.5", + contextLimit: 200000, + outputLimit: 64000, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + }, + { + id: "gemini-claude-opus-4-5-thinking", + name: "Antigravity - Claude Opus 4.5", + contextLimit: 200000, + outputLimit: 64000, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + variants: { + low: { effort: "low" }, + medium: { effort: "medium" }, + high: { effort: "high" }, + }, + }, + { + id: "gemini-claude-sonnet-4-5-thinking", + name: "Antigravity - Claude Sonnet 4.5", + contextLimit: 200000, + outputLimit: 64000, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + variants: { + low: { thinking: { budgetTokens: 5000, type: "enabled" } }, + medium: { thinking: { budgetTokens: 13000, type: "enabled" } }, + high: { thinking: { budgetTokens: 18000, type: "enabled" } }, + }, + }, + ], +}; + +/** + * Look up preset metadata for a model by npm package and model ID. + * Returns enrichment fields (options, limit, modalities) that can be + * merged into a model definition when the user's config doesn't already + * provide them. + */ +export function getPresetModelDefaults( + npm: string, + modelId: string, +): PresetModelVariant | undefined { + const models = OPENCODE_PRESET_MODEL_VARIANTS[npm]; + if (!models) return undefined; + return models.find((m) => m.id === modelId); +} + export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ { name: "DeepSeek", @@ -664,7 +1042,10 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, models: { "gpt-5.2": { name: "GPT-5.2" }, - "gpt-5.2-codex": { name: "GPT-5.2 Codex" }, + "gpt-5.2-codex": { + name: "GPT-5.2 Codex", + options: { include: ["reasoning.encrypted_content"], store: false }, + }, }, }, category: "third_party", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 05489ed9f..d5bd4632a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1572,16 +1572,21 @@ "clearWrapped": "(Clear)", "defaultWrapped": "(Default)", "variantPlaceholder": "variant", - "selectEnabledModel": "Select enabled model", + "selectEnabledModel": "Select configured model", + "selectModel": "Select configured model", + "recommendedHint": "Recommended: {{model}}", + "searchModel": "Search model...", "selectModelFirst": "Select model first", - "noEnabledModels": "No enabled models", + "noEnabledModels": "No configured models", "noVariantsForModel": "No variants for model", - "currentValueNotEnabled": "{{value}} (current value, not enabled)", + "currentValueNotEnabled": "{{value}} (current value, not configured)", "currentValueUnavailable": "{{value}} (current value, unavailable)", "advancedLabel": "Advanced", "advancedJsonInvalid": "Advanced JSON is invalid", "advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission, etc. Leave empty for defaults", - "noEnabledModelsWarning": "No enabled models available. Configure and enable OpenCode models first.", + "noEnabledModelsWarning": "No configured models available. Configure OpenCode models first.", + "modelSourcePartialWarning": "Some provider model configs are invalid and were skipped.", + "modelSourceFallbackWarning": "Failed to load live provider state. Falling back to configured providers.", "importLocalReplaceSuccess": "Imported local file and replaced Agents/Categories/Other Fields", "importLocalFailed": "Failed to read local file: {{error}}", "agentKeyPlaceholder": "agent key", @@ -1592,7 +1597,7 @@ "modelConfiguration": "Model Configuration", "fillRecommended": "Fill Recommended", "configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params", - "enabledModelsCount": "{{count}} enabled models available", + "enabledModelsCount": "{{count}} configured models available", "source": "from:", "otherFieldsJson": "Other Fields (JSON)", "searchOrType": "Search or type custom value...", @@ -1618,6 +1623,52 @@ "advancedExperimental": "Experimental Features", "advancedBackgroundTask": "Background Tasks", "advancedBrowserAutomation": "Browser Automation", - "advancedClaudeCode": "Claude Code" + "advancedClaudeCode": "Claude Code", + "agentDesc": { + "sisyphus": "Main orchestrator", + "hephaestus": "Autonomous deep worker", + "prometheus": "Strategic planner", + "atlas": "Task manager", + "oracle": "Strategic advisor", + "librarian": "Multi-repo researcher", + "explore": "Fast code search", + "multimodalLooker": "Media analyzer", + "metis": "Pre-plan analysis advisor", + "momus": "Plan reviewer", + "sisyphusJunior": "Delegated task executor" + }, + "agentTooltip": { + "sisyphus": "Main orchestrator responsible for task planning, delegation and parallel execution. Uses extended thinking (32k budget) and drives workflows through TODO lists to ensure task completion.", + "atlas": "Main orchestrator (holds TODO list) responsible for task distribution and coordination during execution phase. Delegates to specialized agents rather than completing all work directly.", + "prometheus": "Strategic planner that collects requirements through interview mode and creates detailed work plans. Can only read/write Markdown files in .sisyphus/ directory, never writes code directly.", + "hephaestus": "Autonomous deep worker (\"legitimate craftsman\") inspired by AmpCode deep mode. Goal-oriented execution that launches 2-5 explore/librarian agents in parallel for research before taking action.", + "oracle": "Architecture decision and debugging advisor. Read-only consulting agent providing excellent logical reasoning and deep analysis. Cannot write files or delegate tasks.", + "librarian": "Multi-repository analysis and documentation retrieval expert. Deeply understands codebases and provides evidence-based answers. Excels at finding official documentation and open-source implementation examples.", + "explore": "Fast codebase exploration and context grep expert. Uses lightweight models for high-speed search. The scout for understanding code structure.", + "multimodalLooker": "Visual content expert that analyzes PDFs, images, charts and other non-text media, extracting information and insights from them.", + "metis": "Plan consultant that performs pre-analysis before planning. Identifies hidden intents, ambiguities and AI failure points to prevent over-engineering.", + "momus": "Plan reviewer that validates plan clarity, verifiability and completeness with high precision. Rejects and requests revisions until plans are perfect.", + "sisyphusJunior": "Category-generated executor, automatically created via category parameters. Focuses on executing assigned tasks and cannot re-delegate, preventing infinite delegation loops." + }, + "categoryDesc": { + "visualEngineering": "Visual/frontend engineering", + "ultrabrain": "Ultra thinking", + "deep": "Deep work", + "artistry": "Creative/artistic", + "quick": "Quick response", + "unspecifiedLow": "General low tier", + "unspecifiedHigh": "General high tier", + "writing": "Writing" + }, + "categoryTooltip": { + "visualEngineering": "Frontend and visual engineering category for UI/UX design, styling, animation and interface implementation. Defaults to Gemini 3 Pro model.", + "ultrabrain": "Deep logical reasoning category for complex architectural decisions requiring extensive analysis. Defaults to GPT-5.3 Codex ultra-high reasoning variant.", + "deep": "Deep autonomous problem-solving category with goal-oriented execution. Conducts thorough research before action, suitable for difficult problems requiring deep understanding.", + "artistry": "Highly creative and artistic task category that inspires novel ideas and creative solutions. Defaults to Gemini 3 Pro max variant.", + "quick": "Lightweight task category for single-file modifications, typo fixes, and simple adjustments. Defaults to Claude Haiku 4.5 fast model.", + "unspecifiedLow": "Uncategorized low-effort task category for tasks that don't fit other categories with small workload. Defaults to Claude Sonnet 4.5.", + "unspecifiedHigh": "Uncategorized high-effort task category for tasks that don't fit other categories with large workload. Defaults to Claude Opus 4.6 max variant.", + "writing": "Writing category for documentation, prose and technical writing. Defaults to Gemini 3 Flash fast generation model." + } } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 8675570cd..908d45f4f 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1553,16 +1553,21 @@ "clearWrapped": "(クリア)", "defaultWrapped": "(デフォルト)", "variantPlaceholder": "variant", - "selectEnabledModel": "有効なモデルを選択", + "selectEnabledModel": "設定済みモデルを選択", + "selectModel": "設定済みモデルを選択", + "recommendedHint": "推奨: {{model}}", + "searchModel": "モデルを検索...", "selectModelFirst": "先にモデルを選択", - "noEnabledModels": "有効なモデルがありません", + "noEnabledModels": "設定済みモデルがありません", "noVariantsForModel": "このモデルには思考レベルがありません", - "currentValueNotEnabled": "{{value}} (現在値・未有効)", + "currentValueNotEnabled": "{{value}} (現在値・未設定)", "currentValueUnavailable": "{{value}} (現在値・利用不可)", "advancedLabel": "詳細", "advancedJsonInvalid": "詳細 JSON が不正です", "advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission など。空欄でデフォルトを使用します", - "noEnabledModelsWarning": "利用可能な有効モデルがありません。先に OpenCode モデルを有効化してください。", + "noEnabledModelsWarning": "利用可能な設定済みモデルがありません。先に OpenCode モデルを設定してください。", + "modelSourcePartialWarning": "一部プロバイダーのモデル設定が不正なため、候補から除外しました。", + "modelSourceFallbackWarning": "live プロバイダー状態の取得に失敗したため、設定済みプロバイダーへフォールバックしました。", "importLocalReplaceSuccess": "ローカルファイルから読み込み、Agents/Categories/Other Fields を置き換えました", "importLocalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}", "agentKeyPlaceholder": "agent キー", @@ -1573,7 +1578,7 @@ "modelConfiguration": "モデル設定", "fillRecommended": "推奨を入力", "configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開", - "enabledModelsCount": "有効モデル {{count}} 件", + "enabledModelsCount": "設定済みモデル {{count}} 件", "source": "出典:", "otherFieldsJson": "その他のフィールド (JSON)", "searchOrType": "検索またはカスタム値を入力...", @@ -1599,6 +1604,52 @@ "advancedExperimental": "実験的機能", "advancedBackgroundTask": "バックグラウンドタスク", "advancedBrowserAutomation": "ブラウザ自動化", - "advancedClaudeCode": "Claude Code" + "advancedClaudeCode": "Claude Code", + "agentDesc": { + "sisyphus": "メインオーケストレーター", + "hephaestus": "自律型ディープワーカー", + "prometheus": "戦略プランナー", + "atlas": "タスクマネージャー", + "oracle": "戦略アドバイザー", + "librarian": "マルチリポジトリ研究員", + "explore": "高速コード検索", + "multimodalLooker": "メディアアナライザー", + "metis": "計画前分析アドバイザー", + "momus": "プランレビュアー", + "sisyphusJunior": "委任タスクエグゼキューター" + }, + "agentTooltip": { + "sisyphus": "メインオーケストレーター。タスクの計画、委任、並列実行を担当。拡張思考(32k バジェット)を使用し、TODO リストでワークフローを駆動してタスク完了を確保します。", + "atlas": "メインオーケストレーター(TODO リスト保持)。実行フェーズでのタスク配分と調整を担当。すべての作業を直接行うのではなく、専門エージェントに委任します。", + "prometheus": "戦略プランナー。インタビューモードで要件を収集し、詳細な作業計画を策定。.sisyphus/ ディレクトリ内の Markdown ファイルの読み書きのみ可能で、直接コードを書くことはありません。", + "hephaestus": "自律型ディープワーカー(「正当な職人」)。AmpCode ディープモードに着想を得た目標指向の実行を行い、行動前に 2-5 個の探索/ライブラリアンエージェントを並列起動して調査します。", + "oracle": "アーキテクチャ決定とデバッグのアドバイザー。読み取り専用のコンサルティングエージェントで、優れた論理的推論と深い分析を提供。ファイルの書き込みやタスクの委任はできません。", + "librarian": "マルチリポジトリ分析とドキュメント検索の専門家。コードベースを深く理解し、エビデンスに基づく回答を提供。公式ドキュメントやオープンソース実装例の検索に長けています。", + "explore": "高速コードベース探索とコンテキスト grep の専門家。軽量モデルを使用した高速検索で、コード構造を理解するための先鋒です。", + "multimodalLooker": "ビジュアルコンテンツの専門家。PDF、画像、グラフなどの非テキストメディアを分析し、情報とインサイトを抽出します。", + "metis": "プランコンサルタント。計画前に事前分析を行い、隠れた意図、曖昧な点、AI の失敗ポイントを特定して、過剰エンジニアリングを防止します。", + "momus": "プランレビュアー。計画の明確さ、検証可能性、完全性を高精度で検証。計画が完璧になるまで却下と修正を要求します。", + "sisyphusJunior": "カテゴリ生成のエグゼキューター。category パラメータにより自動生成され、割り当てられたタスクの実行に専念し、再委任はできません。無限委任ループを防止します。" + }, + "categoryDesc": { + "visualEngineering": "ビジュアル/フロントエンド工学", + "ultrabrain": "超深度思考", + "deep": "ディープワーク", + "artistry": "クリエイティブ/芸術", + "quick": "クイックレスポンス", + "unspecifiedLow": "汎用ロースペック", + "unspecifiedHigh": "汎用ハイスペック", + "writing": "ライティング" + }, + "categoryTooltip": { + "visualEngineering": "フロントエンドとビジュアルエンジニアリングカテゴリ。UI/UX デザイン、スタイリング、アニメーション、インターフェース実装に特化。デフォルトで Gemini 3 Pro モデルを使用。", + "ultrabrain": "ディープロジック推論カテゴリ。広範な分析が必要な複雑なアーキテクチャ決定に使用。デフォルトで GPT-5.3 Codex の超高推論バリアントを使用。", + "deep": "ディープ自律問題解決カテゴリ。目標指向の実行で、行動前に徹底的な調査を実施。深い理解が必要な難問に適しています。", + "artistry": "高度にクリエイティブで芸術的なタスクカテゴリ。斬新なアイデアとクリエイティブなソリューションを促進。デフォルトで Gemini 3 Pro の max バリアントを使用。", + "quick": "軽量タスクカテゴリ。単一ファイルの修正、タイポ修正、簡単な調整などの些細な作業に使用。デフォルトで Claude Haiku 4.5 高速モデルを使用。", + "unspecifiedLow": "未分類の低作業量タスクカテゴリ。他のカテゴリに該当せず作業量が小さいタスクに適用。デフォルトで Claude Sonnet 4.5 を使用。", + "unspecifiedHigh": "未分類の高作業量タスクカテゴリ。他のカテゴリに該当せず作業量が大きいタスクに適用。デフォルトで Claude Opus 4.6 の max バリアントを使用。", + "writing": "ライティングカテゴリ。ドキュメント、散文、技術文書に特化。デフォルトで Gemini 3 Flash 高速生成モデルを使用。" + } } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 350ff83e6..e325e9bbe 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1572,16 +1572,21 @@ "clearWrapped": "(清空)", "defaultWrapped": "(默认)", "variantPlaceholder": "思考等级", - "selectEnabledModel": "选择已启用模型", + "selectEnabledModel": "选择已配置模型", + "selectModel": "选择已配置模型", + "recommendedHint": "推荐: {{model}}", + "searchModel": "搜索模型...", "selectModelFirst": "先选择模型", - "noEnabledModels": "暂无已启用模型", + "noEnabledModels": "暂无已配置模型", "noVariantsForModel": "该模型无思考等级", - "currentValueNotEnabled": "{{value}}(当前值,未启用)", - "currentValueUnavailable": "{{value}}(当前值,未启用)", + "currentValueNotEnabled": "{{value}}(当前值,未配置)", + "currentValueUnavailable": "{{value}}(当前值,不可用)", "advancedLabel": "高级参数", "advancedJsonInvalid": "高级参数 JSON 无效", "advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission 等,留空使用默认值", - "noEnabledModelsWarning": "当前没有可用的已启用模型,请先启用并配置 OpenCode 模型", + "noEnabledModelsWarning": "当前没有可用的已配置模型,请先配置 OpenCode 模型", + "modelSourcePartialWarning": "部分供应商模型配置无效,已自动跳过。", + "modelSourceFallbackWarning": "读取 live 供应商状态失败,已回退到已配置供应商列表。", "importLocalReplaceSuccess": "已从本地文件导入并覆盖 Agent/Category/Other Fields", "importLocalFailed": "读取本地文件失败: {{error}}", "agentKeyPlaceholder": "agent 键名", @@ -1592,7 +1597,7 @@ "modelConfiguration": "模型配置", "fillRecommended": "填充推荐", "configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数", - "enabledModelsCount": "可选已启用模型 {{count}} 个", + "enabledModelsCount": "可选已配置模型 {{count}} 个", "source": "来源:", "otherFieldsJson": "其他字段 (JSON)", "searchOrType": "搜索或输入自定义值...", @@ -1618,6 +1623,52 @@ "advancedExperimental": "实验性功能", "advancedBackgroundTask": "后台任务", "advancedBrowserAutomation": "浏览器自动化", - "advancedClaudeCode": "Claude Code" + "advancedClaudeCode": "Claude Code", + "agentDesc": { + "sisyphus": "主编排者", + "hephaestus": "自主深度工作者", + "prometheus": "战略规划者", + "atlas": "任务管理者", + "oracle": "战略顾问", + "librarian": "多仓库研究员", + "explore": "快速代码搜索", + "multimodalLooker": "媒体分析器", + "metis": "规划前分析顾问", + "momus": "计划审查者", + "sisyphusJunior": "委托任务执行器" + }, + "agentTooltip": { + "sisyphus": "主编排器,负责任务规划、委派与并行执行,使用扩展思考(32k 预算),通过 TODO 驱动工作流确保任务完成。", + "atlas": "主编排器(持有 TODO 列表),负责执行阶段的任务分发与协调,不直接完成所有工作而是委派给专业代理。", + "prometheus": "战略规划师,通过访谈模式收集需求并制定详细工作计划,仅能在 .sisyphus/ 目录读写 Markdown 文件,从不直接写代码。", + "hephaestus": "自主深度工作者(「合法工匠」),受 AmpCode 深度模式启发,目标导向执行,行动前会并行启动 2-5 个探索/图书管理员代理进行研究。", + "oracle": "架构决策与调试顾问,只读咨询代理,提供出色的逻辑推理和深度分析,不能写文件或委派任务。", + "librarian": "多仓库分析与文档检索专家,深度理解代码库并提供基于证据的答案,擅长查找官方文档和开源实现示例。", + "explore": "快速代码库探索与上下文 grep 专家,使用轻量级模型进行高速搜索,是理解代码结构的先锋。", + "multimodalLooker": "视觉内容专家,分析 PDF、图片、图表等非文本媒体,提取其中的信息与洞察。", + "metis": "计划顾问,在规划前进行预分析,识别隐藏意图、模糊点和 AI 失败点,防止过度工程化。", + "momus": "计划评审员,高精度验证计划的清晰度、可验证性和完整性,拒绝并要求修订直到计划完美。", + "sisyphusJunior": "类别生成的执行器,由 category 参数自动生成,专注于执行分配的任务且不能再委派,防止无限委派循环。" + }, + "categoryDesc": { + "visualEngineering": "视觉/前端工程", + "ultrabrain": "超级思考", + "deep": "深度工作", + "artistry": "创意/文艺", + "quick": "快速响应", + "unspecifiedLow": "通用低配", + "unspecifiedHigh": "通用高配", + "writing": "写作" + }, + "categoryTooltip": { + "visualEngineering": "前端与视觉工程类别,专注于 UI/UX 设计、样式、动画和界面实现,默认使用 Gemini 3 Pro 模型。", + "ultrabrain": "深度逻辑推理类别,用于需要广泛分析的复杂架构决策,默认使用 GPT-5.3 Codex 的超高推理变体。", + "deep": "深度自主问题解决类别,目标导向执行,行动前进行彻底研究,适用于需要深度理解的棘手问题。", + "artistry": "高度创意与艺术性任务类别,激发新颖想法和创造性解决方案,默认使用 Gemini 3 Pro 的最大变体。", + "quick": "轻量任务类别,用于单文件修改、错别字修复、简单调整等琐碎工作,默认使用 Claude Haiku 4.5 快速模型。", + "unspecifiedLow": "未归类低工作量任务类别,适用于不适合其他类别且工作量较小的任务,默认使用 Claude Sonnet 4.5。", + "unspecifiedHigh": "未归类高工作量任务类别,适用于不适合其他类别且工作量较大的任务,默认使用 Claude Opus 4.6 的最大变体。", + "writing": "写作类别,专注于文档、散文和技术写作,默认使用 Gemini 3 Flash 快速生成模型。" + } } } diff --git a/src/types/omo.ts b/src/types/omo.ts index df9102d65..8b6059223 100644 --- a/src/types/omo.ts +++ b/src/types/omo.ts @@ -27,8 +27,8 @@ export interface OmoLocalFileData { export interface OmoAgentDef { key: string; display: string; - descZh: string; - descEn: string; + descKey: string; + tooltipKey: string; recommended?: string; group: "main" | "sub"; } @@ -36,97 +36,97 @@ export interface OmoAgentDef { export interface OmoCategoryDef { key: string; display: string; - descZh: string; - descEn: string; + descKey: string; + tooltipKey: string; recommended?: string; } export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [ { - key: "Sisyphus", + key: "sisyphus", display: "Sisyphus", - descZh: "主编排者", - descEn: "Main orchestrator", + descKey: "omo.agentDesc.sisyphus", + tooltipKey: "omo.agentTooltip.sisyphus", recommended: "claude-opus-4-6", group: "main", }, { - key: "Hephaestus", + key: "hephaestus", display: "Hephaestus", - descZh: "自主深度工作者", - descEn: "Autonomous deep worker", + descKey: "omo.agentDesc.hephaestus", + tooltipKey: "omo.agentTooltip.hephaestus", recommended: "gpt-5.3-codex", group: "main", }, { - key: "Prometheus", + key: "prometheus", display: "Prometheus", - descZh: "战略规划者", - descEn: "Strategic planner", + descKey: "omo.agentDesc.prometheus", + tooltipKey: "omo.agentTooltip.prometheus", recommended: "claude-opus-4-6", group: "main", }, { - key: "Atlas", + key: "atlas", display: "Atlas", - descZh: "任务管理者", - descEn: "Task manager", + descKey: "omo.agentDesc.atlas", + tooltipKey: "omo.agentTooltip.atlas", recommended: "kimi-k2.5", group: "main", }, { key: "oracle", display: "Oracle", - descZh: "战略顾问", - descEn: "Strategic advisor", + descKey: "omo.agentDesc.oracle", + tooltipKey: "omo.agentTooltip.oracle", recommended: "gpt-5.3", group: "sub", }, { key: "librarian", display: "Librarian", - descZh: "多仓库研究员", - descEn: "Multi-repo researcher", + descKey: "omo.agentDesc.librarian", + tooltipKey: "omo.agentTooltip.librarian", recommended: "glm-4.7", group: "sub", }, { key: "explore", display: "Explore", - descZh: "快速代码搜索", - descEn: "Fast code search", + descKey: "omo.agentDesc.explore", + tooltipKey: "omo.agentTooltip.explore", recommended: "grok-code-fast-1", group: "sub", }, { key: "multimodal-looker", display: "Multimodal-Looker", - descZh: "媒体分析器", - descEn: "Media analyzer", + descKey: "omo.agentDesc.multimodalLooker", + tooltipKey: "omo.agentTooltip.multimodalLooker", recommended: "gemini-3-flash", group: "sub", }, { - key: "Metis", + key: "metis", display: "Metis", - descZh: "规划前分析顾问", - descEn: "Pre-plan analysis advisor", + descKey: "omo.agentDesc.metis", + tooltipKey: "omo.agentTooltip.metis", recommended: "claude-opus-4-6", group: "sub", }, { - key: "Momus", + key: "momus", display: "Momus", - descZh: "计划审查者", - descEn: "Plan reviewer", + descKey: "omo.agentDesc.momus", + tooltipKey: "omo.agentTooltip.momus", recommended: "gpt-5.3", group: "sub", }, { - key: "Sisyphus-Junior", + key: "sisyphus-junior", display: "Sisyphus-Junior", - descZh: "委托任务执行器", - descEn: "Delegated task executor", + descKey: "omo.agentDesc.sisyphusJunior", + tooltipKey: "omo.agentTooltip.sisyphusJunior", group: "sub", }, ]; @@ -135,57 +135,57 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [ { key: "visual-engineering", display: "Visual Engineering", - descZh: "视觉/前端工程", - descEn: "Visual/frontend engineering", + descKey: "omo.categoryDesc.visualEngineering", + tooltipKey: "omo.categoryTooltip.visualEngineering", recommended: "gemini-3-pro", }, { key: "ultrabrain", display: "Ultrabrain", - descZh: "超级思考", - descEn: "Ultra thinking", + descKey: "omo.categoryDesc.ultrabrain", + tooltipKey: "omo.categoryTooltip.ultrabrain", recommended: "claude-opus-4-6", }, { key: "deep", display: "Deep", - descZh: "深度工作", - descEn: "Deep work", + descKey: "omo.categoryDesc.deep", + tooltipKey: "omo.categoryTooltip.deep", recommended: "gpt-5.3-codex", }, { key: "artistry", display: "Artistry", - descZh: "创意/文艺", - descEn: "Creative/artistic", + descKey: "omo.categoryDesc.artistry", + tooltipKey: "omo.categoryTooltip.artistry", recommended: "claude-opus-4-6", }, { key: "quick", display: "Quick", - descZh: "快速响应", - descEn: "Quick response", + descKey: "omo.categoryDesc.quick", + tooltipKey: "omo.categoryTooltip.quick", recommended: "gemini-3-flash", }, { key: "unspecified-low", display: "Unspecified Low", - descZh: "通用低配", - descEn: "General low tier", + descKey: "omo.categoryDesc.unspecifiedLow", + tooltipKey: "omo.categoryTooltip.unspecifiedLow", recommended: "gemini-3-flash", }, { key: "unspecified-high", display: "Unspecified High", - descZh: "通用高配", - descEn: "General high tier", + descKey: "omo.categoryDesc.unspecifiedHigh", + tooltipKey: "omo.categoryTooltip.unspecifiedHigh", recommended: "gpt-5.3-codex", }, { key: "writing", display: "Writing", - descZh: "写作", - descEn: "Writing", + descKey: "omo.categoryDesc.writing", + tooltipKey: "omo.categoryTooltip.writing", recommended: "claude-opus-4-6", }, ]; @@ -316,6 +316,17 @@ export const OMO_CLAUDE_CODE_PLACEHOLDER = `{ "plugins": true }`; +export function parseOmoOtherFieldsObject( + raw: string, +): Record | undefined { + if (!raw.trim()) return undefined; + const parsed: unknown = JSON.parse(raw); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + return undefined; + } + return parsed as Record; +} + export function mergeOmoConfigPreview( global: OmoGlobalConfig, agents: Record>, @@ -351,7 +362,8 @@ export function mergeOmoConfigPreview( if (Object.keys(agents).length > 0) result["agents"] = agents; if (Object.keys(categories).length > 0) result["categories"] = categories; try { - const other = JSON.parse(otherFieldsStr || "{}"); + const other = parseOmoOtherFieldsObject(otherFieldsStr); + if (!other) return result; for (const [k, v] of Object.entries(other)) { result[k] = v; } diff --git a/tests/components/OmoFormFields.mergeCustomModelsIntoStore.test.ts b/tests/components/OmoFormFields.mergeCustomModelsIntoStore.test.ts new file mode 100644 index 000000000..b77b79e60 --- /dev/null +++ b/tests/components/OmoFormFields.mergeCustomModelsIntoStore.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from "vitest"; +import { + mergeCustomModelsIntoStore, + type CustomModelItem, +} from "@/components/providers/forms/OmoFormFields"; + +describe("mergeCustomModelsIntoStore", () => { + it("保留自定义项高级字段,并在模型变更时仅按需清理非法 variant", () => { + const store = { + sisyphus: { model: "builtin-model" }, + "custom-agent": { + model: "model-a", + variant: "fast", + temperature: 0.2, + permission: { edit: "allow" }, + }, + }; + const customs: CustomModelItem[] = [ + { key: "custom-agent", model: "model-b", sourceKey: "custom-agent" }, + ]; + + const merged = mergeCustomModelsIntoStore( + store, + new Set(["sisyphus"]), + customs, + { "model-b": ["precise"] }, + ); + + expect(merged.sisyphus).toEqual({ model: "builtin-model" }); + expect(merged["custom-agent"]).toEqual({ + model: "model-b", + temperature: 0.2, + permission: { edit: "allow" }, + }); + }); + + it("重命名自定义 key 时迁移原有 variant 和高级字段", () => { + const store = { + sisyphus: { model: "builtin-model" }, + "custom-agent-old": { + model: "model-a", + variant: "fast", + maxTokens: 8192, + }, + }; + const customs: CustomModelItem[] = [ + { + key: "custom-agent-new", + sourceKey: "custom-agent-old", + model: "model-a", + }, + ]; + + const merged = mergeCustomModelsIntoStore( + store, + new Set(["sisyphus"]), + customs, + { "model-a": ["fast", "balanced"] }, + ); + + expect(merged["custom-agent-old"]).toBeUndefined(); + expect(merged["custom-agent-new"]).toEqual({ + model: "model-a", + variant: "fast", + maxTokens: 8192, + }); + }); + + it("custom 列表为空时移除旧自定义项但保留内置项", () => { + const store = { + sisyphus: { model: "builtin-model" }, + hephaestus: { model: "builtin-model-2" }, + "custom-agent": { model: "model-a", temperature: 0.3 }, + }; + + const merged = mergeCustomModelsIntoStore( + store, + new Set(["sisyphus", "hephaestus"]), + [], + {}, + ); + + expect(merged).toEqual({ + sisyphus: { model: "builtin-model" }, + hephaestus: { model: "builtin-model-2" }, + }); + }); + + it("清空 model 时保留高级字段并移除 model/variant", () => { + const store = { + sisyphus: { model: "builtin-model" }, + "custom-agent": { + model: "model-a", + variant: "fast", + temperature: 0.7, + }, + }; + const customs: CustomModelItem[] = [ + { key: "custom-agent", model: "", sourceKey: "custom-agent" }, + ]; + + const merged = mergeCustomModelsIntoStore( + store, + new Set(["sisyphus"]), + customs, + { "model-a": ["fast"] }, + ); + + expect(merged["custom-agent"]).toEqual({ temperature: 0.7 }); + }); +}); diff --git a/tests/utils/omoConfig.test.ts b/tests/utils/omoConfig.test.ts new file mode 100644 index 000000000..5a5de6c91 --- /dev/null +++ b/tests/utils/omoConfig.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { + mergeOmoConfigPreview, + parseOmoOtherFieldsObject, + type OmoGlobalConfig, +} from "@/types/omo"; + +const EMPTY_GLOBAL: OmoGlobalConfig = { + id: "global", + disabledAgents: [], + disabledMcps: [], + disabledHooks: [], + disabledSkills: [], + updatedAt: "2026-01-01T00:00:00.000Z", +}; + +describe("parseOmoOtherFieldsObject", () => { + it("解析对象 JSON", () => { + expect(parseOmoOtherFieldsObject('{ "foo": 1 }')).toEqual({ foo: 1 }); + }); + + it("数组/字符串返回 undefined", () => { + expect(parseOmoOtherFieldsObject('["a"]')).toBeUndefined(); + expect(parseOmoOtherFieldsObject('"hello"')).toBeUndefined(); + }); + + it("非法 JSON 抛出异常", () => { + expect(() => parseOmoOtherFieldsObject("{")).toThrow(); + }); +}); + +describe("mergeOmoConfigPreview", () => { + it("只合并 otherFields 的对象值,忽略数组", () => { + const mergedFromArray = mergeOmoConfigPreview( + EMPTY_GLOBAL, + {}, + {}, + '["a", "b"]', + ); + expect(mergedFromArray).toEqual({}); + + const mergedFromObject = mergeOmoConfigPreview( + EMPTY_GLOBAL, + {}, + {}, + '{ "foo": "bar" }', + ); + expect(mergedFromObject).toEqual({ foo: "bar" }); + }); +}); From 62fa5213bfb90ca5f64442a59689e74cb06090ee Mon Sep 17 00:00:00 2001 From: Dex Miller Date: Thu, 12 Feb 2026 22:28:05 +0800 Subject: [PATCH 02/51] feat(proxy): fix thinking rectifiers and resolve clippy warnings (#1005) * feat(proxy): align thinking rectifiers and resolve clippy warnings - add thinking budget rectifier flow with single retry on anthropic budget errors - align thinking signature rectification behavior with adaptive-safe handling - expose requestThinkingBudget in settings/ui/i18n and default rectifier config to disabled - fix clippy warnings in model_mapper format args and RectifierConfig default derive * fix(proxy): thinking rectifiers --- src-tauri/src/database/dao/settings.rs | 2 +- src-tauri/src/proxy/forwarder.rs | 235 ++++++++++-- src-tauri/src/proxy/mod.rs | 1 + src-tauri/src/proxy/model_mapper.rs | 38 +- .../src/proxy/thinking_budget_rectifier.rs | 359 ++++++++++++++++++ src-tauri/src/proxy/thinking_rectifier.rs | 274 ++++++++++++- src-tauri/src/proxy/types.rs | 60 ++- .../settings/RectifierConfigPanel.tsx | 16 + src/i18n/locales/en.json | 4 +- src/i18n/locales/ja.json | 4 +- src/i18n/locales/zh.json | 4 +- src/lib/api/settings.ts | 1 + 12 files changed, 953 insertions(+), 45 deletions(-) create mode 100644 src-tauri/src/proxy/thinking_budget_rectifier.rs diff --git a/src-tauri/src/database/dao/settings.rs b/src-tauri/src/database/dao/settings.rs index 109e972dd..8056a01cb 100644 --- a/src-tauri/src/database/dao/settings.rs +++ b/src-tauri/src/database/dao/settings.rs @@ -168,7 +168,7 @@ impl Database { /// 获取整流器配置 /// - /// 返回整流器配置,如果不存在则返回默认值(全部启用) + /// 返回整流器配置,如果不存在则返回默认值(全部开启) pub fn get_rectifier_config(&self) -> Result { match self.get_setting("rectifier_config")? { Some(json) => serde_json::from_str(&json) diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index 2c741b8a6..c26ceabf4 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -8,7 +8,10 @@ use super::{ failover_switch::FailoverSwitchManager, provider_router::ProviderRouter, providers::{get_adapter, ProviderAdapter, ProviderType}, - thinking_rectifier::{rectify_anthropic_request, should_rectify_thinking_signature}, + thinking_budget_rectifier::{rectify_thinking_budget, should_rectify_thinking_budget}, + thinking_rectifier::{ + normalize_thinking_type, rectify_anthropic_request, should_rectify_thinking_signature, + }, types::{ProxyStatus, RectifierConfig}, ProxyError, }; @@ -157,6 +160,7 @@ impl RequestForwarder { // 整流器重试标记:确保整流最多触发一次 let mut rectifier_retried = false; + let mut budget_rectifier_retried = false; // 单 Provider 场景下跳过熔断器检查(故障转移关闭时) let bypass_circuit_breaker = providers.len() == 1; @@ -258,6 +262,7 @@ impl RequestForwarder { provider_type, ProviderType::Claude | ProviderType::ClaudeAuth ); + let mut signature_rectifier_non_retryable_client_error = false; if is_anthropic_provider { let error_message = extract_error_message(&e); @@ -293,12 +298,185 @@ impl RequestForwarder { // 首次触发:整流请求体 let rectified = rectify_anthropic_request(&mut body); - // 整流未生效:直接返回错误(不可重试客户端错误) + // 整流未生效:继续尝试 budget 整流路径,避免误判后短路 if !rectified.applied { log::warn!( - "[{app_type_str}] [RECT-006] 整流器触发但无可整流内容,不做无意义重试" + "[{app_type_str}] [RECT-006] thinking 签名整流器触发但无可整流内容,继续检查 budget;若 budget 也未命中则按客户端错误返回" + ); + signature_rectifier_non_retryable_client_error = true; + } else { + log::info!( + "[{}] [RECT-001] thinking 签名整流器触发, 移除 {} thinking blocks, {} redacted_thinking blocks, {} signature fields", + app_type_str, + rectified.removed_thinking_blocks, + rectified.removed_redacted_thinking_blocks, + rectified.removed_signature_fields + ); + + // 标记已重试(当前逻辑下重试后必定 return,保留标记以备将来扩展) + let _ = std::mem::replace(&mut rectifier_retried, true); + + // 使用同一供应商重试(不计入熔断器) + match self + .forward(provider, endpoint, &body, &headers, adapter.as_ref()) + .await + { + Ok(response) => { + log::info!("[{app_type_str}] [RECT-002] 整流重试成功"); + // 记录成功 + let _ = self + .router + .record_result( + &provider.id, + app_type_str, + used_half_open_permit, + true, + None, + ) + .await; + + // 更新当前应用类型使用的 provider + { + let mut current_providers = + self.current_providers.write().await; + current_providers.insert( + app_type_str.to_string(), + (provider.id.clone(), provider.name.clone()), + ); + } + + // 更新成功统计 + { + let mut status = self.status.write().await; + status.success_requests += 1; + status.last_error = None; + let should_switch = + self.current_provider_id_at_start.as_str() + != provider.id.as_str(); + if should_switch { + status.failover_count += 1; + + // 异步触发供应商切换,更新 UI/托盘 + let fm = self.failover_manager.clone(); + let ah = self.app_handle.clone(); + let pid = provider.id.clone(); + let pname = provider.name.clone(); + let at = app_type_str.to_string(); + + tokio::spawn(async move { + let _ = fm + .try_switch(ah.as_ref(), &at, &pid, &pname) + .await; + }); + } + if status.total_requests > 0 { + status.success_rate = (status.success_requests + as f32 + / status.total_requests as f32) + * 100.0; + } + } + + return Ok(ForwardResult { + response, + provider: provider.clone(), + }); + } + Err(retry_err) => { + // 整流重试仍失败:区分错误类型决定是否记录熔断器 + log::warn!( + "[{app_type_str}] [RECT-003] 整流重试仍失败: {retry_err}" + ); + + // 区分错误类型:Provider 问题记录失败,客户端问题仅释放 permit + let is_provider_error = match &retry_err { + ProxyError::Timeout(_) + | ProxyError::ForwardFailed(_) => true, + ProxyError::UpstreamError { status, .. } => { + *status >= 500 + } + _ => false, + }; + + if is_provider_error { + // Provider 问题:记录失败到熔断器 + let _ = self + .router + .record_result( + &provider.id, + app_type_str, + used_half_open_permit, + false, + Some(retry_err.to_string()), + ) + .await; + } else { + // 客户端问题:仅释放 permit,不记录熔断器 + self.router + .release_permit_neutral( + &provider.id, + app_type_str, + used_half_open_permit, + ) + .await; + } + + let mut status = self.status.write().await; + status.failed_requests += 1; + status.last_error = Some(retry_err.to_string()); + if status.total_requests > 0 { + status.success_rate = (status.success_requests as f32 + / status.total_requests as f32) + * 100.0; + } + return Err(ForwardError { + error: retry_err, + provider: Some(provider.clone()), + }); + } + } + } + } + } + + // 检测是否需要触发 budget 整流器(仅 Claude/ClaudeAuth 供应商) + if is_anthropic_provider { + let error_message = extract_error_message(&e); + if should_rectify_thinking_budget( + error_message.as_deref(), + &self.rectifier_config, + ) { + // 已经重试过:直接返回错误(不可重试客户端错误) + if budget_rectifier_retried { + log::warn!( + "[{app_type_str}] [RECT-013] budget 整流器已触发过,不再重试" + ); + self.router + .release_permit_neutral( + &provider.id, + app_type_str, + used_half_open_permit, + ) + .await; + let mut status = self.status.write().await; + status.failed_requests += 1; + status.last_error = Some(e.to_string()); + if status.total_requests > 0 { + status.success_rate = (status.success_requests as f32 + / status.total_requests as f32) + * 100.0; + } + return Err(ForwardError { + error: e, + provider: Some(provider.clone()), + }); + } + + let budget_rectified = rectify_thinking_budget(&mut body); + if !budget_rectified.applied { + log::warn!( + "[{app_type_str}] [RECT-014] budget 整流器触发但无可整流内容,不做无意义重试" ); - // 释放 HalfOpen permit(不记录熔断器,这是客户端兼容性问题) self.router .release_permit_neutral( &provider.id, @@ -321,15 +499,13 @@ impl RequestForwarder { } log::info!( - "[{}] [RECT-001] thinking 签名整流器触发, 移除 {} thinking blocks, {} redacted_thinking blocks, {} signature fields", + "[{}] [RECT-010] thinking budget 整流器触发, before={:?}, after={:?}", app_type_str, - rectified.removed_thinking_blocks, - rectified.removed_redacted_thinking_blocks, - rectified.removed_signature_fields + budget_rectified.before, + budget_rectified.after ); - // 标记已重试(当前逻辑下重试后必定 return,保留标记以备将来扩展) - let _ = std::mem::replace(&mut rectifier_retried, true); + let _ = std::mem::replace(&mut budget_rectifier_retried, true); // 使用同一供应商重试(不计入熔断器) match self @@ -337,8 +513,7 @@ impl RequestForwarder { .await { Ok(response) => { - log::info!("[{app_type_str}] [RECT-002] 整流重试成功"); - // 记录成功 + log::info!("[{app_type_str}] [RECT-011] budget 整流重试成功"); let _ = self .router .record_result( @@ -350,7 +525,6 @@ impl RequestForwarder { ) .await; - // 更新当前应用类型使用的 provider { let mut current_providers = self.current_providers.write().await; @@ -360,7 +534,6 @@ impl RequestForwarder { ); } - // 更新成功统计 { let mut status = self.status.write().await; status.success_requests += 1; @@ -370,14 +543,11 @@ impl RequestForwarder { != provider.id.as_str(); if should_switch { status.failover_count += 1; - - // 异步触发供应商切换,更新 UI/托盘 let fm = self.failover_manager.clone(); let ah = self.app_handle.clone(); let pid = provider.id.clone(); let pname = provider.name.clone(); let at = app_type_str.to_string(); - tokio::spawn(async move { let _ = fm .try_switch(ah.as_ref(), &at, &pid, &pname) @@ -397,12 +567,10 @@ impl RequestForwarder { }); } Err(retry_err) => { - // 整流重试仍失败:区分错误类型决定是否记录熔断器 log::warn!( - "[{app_type_str}] [RECT-003] 整流重试仍失败: {retry_err}" + "[{app_type_str}] [RECT-012] budget 整流重试仍失败: {retry_err}" ); - // 区分错误类型:Provider 问题记录失败,客户端问题仅释放 permit let is_provider_error = match &retry_err { ProxyError::Timeout(_) | ProxyError::ForwardFailed(_) => { true @@ -412,7 +580,6 @@ impl RequestForwarder { }; if is_provider_error { - // Provider 问题:记录失败到熔断器 let _ = self .router .record_result( @@ -424,7 +591,6 @@ impl RequestForwarder { ) .await; } else { - // 客户端问题:仅释放 permit,不记录熔断器 self.router .release_permit_neutral( &provider.id, @@ -451,6 +617,28 @@ impl RequestForwarder { } } + if signature_rectifier_non_retryable_client_error { + self.router + .release_permit_neutral( + &provider.id, + app_type_str, + used_half_open_permit, + ) + .await; + let mut status = self.status.write().await; + status.failed_requests += 1; + status.last_error = Some(e.to_string()); + if status.total_requests > 0 { + status.success_rate = (status.success_requests as f32 + / status.total_requests as f32) + * 100.0; + } + return Err(ForwardError { + error: e, + provider: Some(provider.clone()), + }); + } + // 失败:记录失败并更新熔断器 let _ = self .router @@ -575,6 +763,9 @@ impl RequestForwarder { let (mapped_body, _original_model, _mapped_model) = super::model_mapper::apply_model_mapping(body.clone(), provider); + // 与 CCH 对齐:请求前不做 thinking 主动改写(仅保留兼容入口) + let mapped_body = normalize_thinking_type(mapped_body); + // 转换请求体(如果需要) let request_body = if needs_transform { adapter.transform_request(mapped_body, provider)? diff --git a/src-tauri/src/proxy/mod.rs b/src-tauri/src/proxy/mod.rs index 338b845f6..fd6e0ba88 100644 --- a/src-tauri/src/proxy/mod.rs +++ b/src-tauri/src/proxy/mod.rs @@ -21,6 +21,7 @@ pub mod response_handler; pub mod response_processor; pub(crate) mod server; pub mod session; +pub mod thinking_budget_rectifier; pub mod thinking_rectifier; pub(crate) mod types; pub mod usage; diff --git a/src-tauri/src/proxy/model_mapper.rs b/src-tauri/src/proxy/model_mapper.rs index a0eca7d1e..4f0f94d81 100644 --- a/src-tauri/src/proxy/model_mapper.rs +++ b/src-tauri/src/proxy/model_mapper.rs @@ -97,11 +97,21 @@ impl ModelMapping { /// 检测请求是否启用了 thinking 模式 pub fn has_thinking_enabled(body: &Value) -> bool { - body.get("thinking") + match body + .get("thinking") .and_then(|v| v.as_object()) .and_then(|o| o.get("type")) .and_then(|t| t.as_str()) - == Some("enabled") + { + Some("enabled") | Some("adaptive") => true, + Some("disabled") | None => false, + Some(other) => { + log::warn!( + "[ModelMapper] 未知 thinking.type='{other}',按 disabled 处理以避免误路由 reasoning 模型" + ); + false + } + } } /// 对请求体应用模型映射 @@ -300,6 +310,30 @@ mod tests { assert!(mapped.is_none()); } + #[test] + fn test_thinking_adaptive() { + let provider = create_provider_with_mapping(); + let body = json!({ + "model": "claude-sonnet-4-5", + "thinking": {"type": "adaptive"} + }); + let (result, _, mapped) = apply_model_mapping(body, &provider); + assert_eq!(result["model"], "reasoning-model"); + assert_eq!(mapped, Some("reasoning-model".to_string())); + } + + #[test] + fn test_thinking_unknown_type() { + let provider = create_provider_with_mapping(); + let body = json!({ + "model": "claude-sonnet-4-5", + "thinking": {"type": "some_future_type"} + }); + let (result, _, mapped) = apply_model_mapping(body, &provider); + assert_eq!(result["model"], "sonnet-mapped"); + assert_eq!(mapped, Some("sonnet-mapped".to_string())); + } + #[test] fn test_case_insensitive() { let provider = create_provider_with_mapping(); diff --git a/src-tauri/src/proxy/thinking_budget_rectifier.rs b/src-tauri/src/proxy/thinking_budget_rectifier.rs new file mode 100644 index 000000000..aa0836878 --- /dev/null +++ b/src-tauri/src/proxy/thinking_budget_rectifier.rs @@ -0,0 +1,359 @@ +//! Thinking Budget 整流器 +//! +//! 用于自动修复 Anthropic API 中因 thinking budget 约束导致的请求错误。 +//! 当上游 API 返回 budget_tokens 相关错误时,系统会自动调整 budget 参数并重试。 + +use super::types::RectifierConfig; +use serde_json::Value; + +/// 最大 thinking budget tokens +const MAX_THINKING_BUDGET: u64 = 32000; + +/// 最大 max_tokens 值 +const MAX_TOKENS_VALUE: u64 = 64000; + +/// max_tokens 必须大于 budget_tokens +const MIN_MAX_TOKENS_FOR_BUDGET: u64 = MAX_THINKING_BUDGET + 1; + +/// Budget 整流结果 +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct BudgetRectifySnapshot { + /// max_tokens + pub max_tokens: Option, + /// thinking.type + pub thinking_type: Option, + /// thinking.budget_tokens + pub thinking_budget_tokens: Option, +} + +/// Budget 整流结果 +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct BudgetRectifyResult { + /// 是否应用了整流 + pub applied: bool, + /// 整流前快照 + pub before: BudgetRectifySnapshot, + /// 整流后快照 + pub after: BudgetRectifySnapshot, +} + +/// 检测是否需要触发 thinking budget 整流器 +/// +/// 检测条件:error message 同时包含 `budget_tokens` + `thinking` 相关约束 +pub fn should_rectify_thinking_budget( + error_message: Option<&str>, + config: &RectifierConfig, +) -> bool { + // 检查总开关 + if !config.enabled { + return false; + } + // 检查子开关 + if !config.request_thinking_budget { + return false; + } + + let Some(msg) = error_message else { + return false; + }; + let lower = msg.to_lowercase(); + + // 与 CCH 对齐:仅在包含 budget_tokens + thinking + 1024 约束时触发 + let has_budget_tokens_reference = + lower.contains("budget_tokens") || lower.contains("budget tokens"); + let has_thinking_reference = lower.contains("thinking"); + let has_1024_constraint = lower.contains("greater than or equal to 1024") + || lower.contains(">= 1024") + || (lower.contains("1024") && lower.contains("input should be")); + if has_budget_tokens_reference && has_thinking_reference && has_1024_constraint { + return true; + } + + false +} + +/// 对请求体执行 budget 整流 +/// +/// 整流动作: +/// - `thinking.type = "enabled"` +/// - `thinking.budget_tokens = 32000` +/// - 如果 `max_tokens < 32001`,设为 `64000` +pub fn rectify_thinking_budget(body: &mut Value) -> BudgetRectifyResult { + let before = snapshot_budget(body); + + // 与 CCH 对齐:adaptive 请求不改写 + if before.thinking_type.as_deref() == Some("adaptive") { + return BudgetRectifyResult { + applied: false, + before: before.clone(), + after: before, + }; + } + + // 与 CCH 对齐:缺少/非法 thinking 时自动创建后再整流 + if !body.get("thinking").is_some_and(Value::is_object) { + body["thinking"] = Value::Object(serde_json::Map::new()); + } + + let Some(thinking) = body.get_mut("thinking").and_then(|t| t.as_object_mut()) else { + return BudgetRectifyResult { + applied: false, + before: before.clone(), + after: before, + }; + }; + + thinking.insert("type".to_string(), Value::String("enabled".to_string())); + thinking.insert( + "budget_tokens".to_string(), + Value::Number(MAX_THINKING_BUDGET.into()), + ); + + if before.max_tokens.is_none() || before.max_tokens < Some(MIN_MAX_TOKENS_FOR_BUDGET) { + body["max_tokens"] = Value::Number(MAX_TOKENS_VALUE.into()); + } + + let after = snapshot_budget(body); + BudgetRectifyResult { + applied: before != after, + before, + after, + } +} + +fn snapshot_budget(body: &Value) -> BudgetRectifySnapshot { + let max_tokens = body.get("max_tokens").and_then(|v| v.as_u64()); + let thinking = body.get("thinking").and_then(|t| t.as_object()); + let thinking_type = thinking + .and_then(|t| t.get("type")) + .and_then(|v| v.as_str()) + .map(ToString::to_string); + let thinking_budget_tokens = thinking + .and_then(|t| t.get("budget_tokens")) + .and_then(|v| v.as_u64()); + BudgetRectifySnapshot { + max_tokens, + thinking_type, + thinking_budget_tokens, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn enabled_config() -> RectifierConfig { + RectifierConfig { + enabled: true, + request_thinking_signature: true, + request_thinking_budget: true, + } + } + + fn budget_disabled_config() -> RectifierConfig { + RectifierConfig { + enabled: true, + request_thinking_signature: true, + request_thinking_budget: false, + } + } + + fn master_disabled_config() -> RectifierConfig { + RectifierConfig { + enabled: false, + request_thinking_signature: true, + request_thinking_budget: true, + } + } + + // ==================== should_rectify_thinking_budget 测试 ==================== + + #[test] + fn test_detect_budget_tokens_thinking_error() { + assert!(should_rectify_thinking_budget( + Some("thinking.budget_tokens: Input should be greater than or equal to 1024"), + &enabled_config() + )); + } + + #[test] + fn test_detect_budget_tokens_max_tokens_error() { + assert!(!should_rectify_thinking_budget( + Some("budget_tokens must be less than max_tokens"), + &enabled_config() + )); + } + + #[test] + fn test_detect_budget_tokens_1024_error() { + assert!(!should_rectify_thinking_budget( + Some("budget_tokens: value must be at least 1024"), + &enabled_config() + )); + } + + #[test] + fn test_detect_budget_tokens_with_thinking_and_1024_error() { + assert!(should_rectify_thinking_budget( + Some("thinking budget_tokens must be >= 1024"), + &enabled_config() + )); + } + + #[test] + fn test_no_trigger_for_unrelated_error() { + assert!(!should_rectify_thinking_budget( + Some("Request timeout"), + &enabled_config() + )); + assert!(!should_rectify_thinking_budget(None, &enabled_config())); + } + + #[test] + fn test_disabled_budget_config() { + assert!(!should_rectify_thinking_budget( + Some("thinking.budget_tokens: Input should be greater than or equal to 1024"), + &budget_disabled_config() + )); + } + + #[test] + fn test_master_disabled() { + assert!(!should_rectify_thinking_budget( + Some("thinking.budget_tokens: Input should be greater than or equal to 1024"), + &master_disabled_config() + )); + } + + // ==================== rectify_thinking_budget 测试 ==================== + + #[test] + fn test_rectify_budget_basic() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "enabled", "budget_tokens": 512 }, + "max_tokens": 1024 + }); + + let result = rectify_thinking_budget(&mut body); + + assert!(result.applied); + assert_eq!(result.before.thinking_type.as_deref(), Some("enabled")); + assert_eq!(result.after.thinking_type.as_deref(), Some("enabled")); + assert_eq!(result.before.thinking_budget_tokens, Some(512)); + assert_eq!( + result.after.thinking_budget_tokens, + Some(MAX_THINKING_BUDGET) + ); + assert_eq!(result.before.max_tokens, Some(1024)); + assert_eq!(result.after.max_tokens, Some(MAX_TOKENS_VALUE)); + assert_eq!(body["thinking"]["type"], "enabled"); + assert_eq!(body["thinking"]["budget_tokens"], MAX_THINKING_BUDGET); + assert_eq!(body["max_tokens"], MAX_TOKENS_VALUE); + } + + #[test] + fn test_rectify_budget_skips_adaptive() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "adaptive", "budget_tokens": 512 }, + "max_tokens": 1024 + }); + + let result = rectify_thinking_budget(&mut body); + + assert!(!result.applied); + assert_eq!(result.before, result.after); + assert_eq!(body["thinking"]["type"], "adaptive"); + assert_eq!(body["thinking"]["budget_tokens"], 512); + assert_eq!(body["max_tokens"], 1024); + } + + #[test] + fn test_rectify_budget_preserves_large_max_tokens() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "enabled", "budget_tokens": 512 }, + "max_tokens": 100000 + }); + + let result = rectify_thinking_budget(&mut body); + + assert!(result.applied); + assert_eq!(result.before.max_tokens, Some(100000)); + assert_eq!(result.after.max_tokens, Some(100000)); + assert_eq!(body["max_tokens"], 100000); + } + + #[test] + fn test_rectify_budget_creates_thinking_object_when_missing() { + let mut body = json!({ + "model": "claude-test", + "max_tokens": 1024 + }); + + let result = rectify_thinking_budget(&mut body); + + assert!(result.applied); + assert_eq!(result.before.thinking_type, None); + assert_eq!(result.after.thinking_type.as_deref(), Some("enabled")); + assert_eq!( + result.after.thinking_budget_tokens, + Some(MAX_THINKING_BUDGET) + ); + assert_eq!(result.after.max_tokens, Some(MAX_TOKENS_VALUE)); + assert_eq!(body["thinking"]["type"], "enabled"); + assert_eq!(body["thinking"]["budget_tokens"], MAX_THINKING_BUDGET); + assert_eq!(body["max_tokens"], MAX_TOKENS_VALUE); + } + + #[test] + fn test_rectify_budget_no_max_tokens() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "enabled", "budget_tokens": 512 } + }); + + let result = rectify_thinking_budget(&mut body); + + assert!(result.applied); + assert_eq!(result.before.max_tokens, None); + assert_eq!(result.after.max_tokens, Some(MAX_TOKENS_VALUE)); + assert_eq!(body["max_tokens"], MAX_TOKENS_VALUE); + } + + #[test] + fn test_rectify_budget_normalizes_non_enabled_type() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "disabled", "budget_tokens": 512 }, + "max_tokens": 1024 + }); + + let result = rectify_thinking_budget(&mut body); + + assert!(result.applied); + assert_eq!(result.before.thinking_type.as_deref(), Some("disabled")); + assert_eq!(result.after.thinking_type.as_deref(), Some("enabled")); + assert_eq!(body["thinking"]["type"], "enabled"); + assert_eq!(body["thinking"]["budget_tokens"], MAX_THINKING_BUDGET); + assert_eq!(body["max_tokens"], MAX_TOKENS_VALUE); + } + + #[test] + fn test_rectify_budget_no_change_when_already_valid() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "enabled", "budget_tokens": 32000 }, + "max_tokens": 64001 + }); + + let result = rectify_thinking_budget(&mut body); + + assert!(!result.applied); + assert_eq!(result.before, result.after); + assert_eq!(body["thinking"]["budget_tokens"], 32000); + assert_eq!(body["max_tokens"], 64001); + } +} diff --git a/src-tauri/src/proxy/thinking_rectifier.rs b/src-tauri/src/proxy/thinking_rectifier.rs index 827ce5618..ce71503c8 100644 --- a/src-tauri/src/proxy/thinking_rectifier.rs +++ b/src-tauri/src/proxy/thinking_rectifier.rs @@ -59,10 +59,12 @@ pub fn should_rectify_thinking_signature( } // 场景3: expected thinking or redacted_thinking, found tool_use + // 与 CCH 对齐:要求明确包含 tool_use,避免过宽匹配。 // 错误示例: "Expected `thinking` or `redacted_thinking`, but found `tool_use`" if lower.contains("expected") && (lower.contains("thinking") || lower.contains("redacted_thinking")) && lower.contains("found") + && lower.contains("tool_use") { return true; } @@ -73,6 +75,28 @@ pub fn should_rectify_thinking_signature( return true; } + // 场景5: signature 字段不被接受(第三方渠道) + // 错误示例: "xxx.signature: Extra inputs are not permitted" + if lower.contains("signature") && lower.contains("extra inputs are not permitted") { + return true; + } + + // 场景6: thinking/redacted_thinking 块被修改 + // 错误示例: "thinking or redacted_thinking blocks ... cannot be modified" + if (lower.contains("thinking") || lower.contains("redacted_thinking")) + && lower.contains("cannot be modified") + { + return true; + } + + // 场景7: 非法请求(与 CCH 对齐,按 invalid request 统一兜底) + if lower.contains("非法请求") + || lower.contains("illegal request") + || lower.contains("invalid request") + { + return true; + } + false } @@ -159,11 +183,13 @@ pub fn rectify_anthropic_request(body: &mut Value) -> RectifyResult { /// 判断是否需要删除顶层 thinking 字段 fn should_remove_top_level_thinking(body: &Value, messages: &[Value]) -> bool { // 检查 thinking 是否启用 - let thinking_enabled = body + let thinking_type = body .get("thinking") .and_then(|t| t.get("type")) - .and_then(|t| t.as_str()) - == Some("enabled"); + .and_then(|t| t.as_str()); + + // 与 CCH 对齐:仅 type=enabled 视为开启 + let thinking_enabled = thinking_type == Some("enabled"); if !thinking_enabled { return false; @@ -202,6 +228,11 @@ fn should_remove_top_level_thinking(body: &Value, messages: &[Value]) -> bool { .any(|b| b.get("type").and_then(|t| t.as_str()) == Some("tool_use")) } +/// 与 CCH 对齐:请求前不做 thinking type 主动改写。 +pub fn normalize_thinking_type(body: Value) -> Value { + body +} + #[cfg(test)] mod tests { use super::*; @@ -211,6 +242,7 @@ mod tests { RectifierConfig { enabled: true, request_thinking_signature: true, + request_thinking_budget: true, } } @@ -218,6 +250,7 @@ mod tests { RectifierConfig { enabled: true, request_thinking_signature: false, + request_thinking_budget: false, } } @@ -225,6 +258,7 @@ mod tests { RectifierConfig { enabled: false, request_thinking_signature: true, + request_thinking_budget: true, } } @@ -264,6 +298,14 @@ mod tests { )); } + #[test] + fn test_no_detect_thinking_expected_without_tool_use() { + assert!(!should_rectify_thinking_signature( + Some("messages.69.content.0.type: Expected `thinking` or `redacted_thinking`, but found `text`."), + &enabled_config() + )); + } + #[test] fn test_detect_must_start_with_thinking() { assert!(should_rectify_thinking_signature( @@ -418,4 +460,230 @@ mod tests { // 此时会触发删除顶层 thinking 的逻辑 // 这是预期行为:整流后如果仍然不符合要求,就删除顶层 thinking } + + // ==================== 新增错误场景检测测试 ==================== + + #[test] + fn test_detect_signature_extra_inputs() { + // 场景5: signature 字段不被接受 + assert!(should_rectify_thinking_signature( + Some("xxx.signature: Extra inputs are not permitted"), + &enabled_config() + )); + } + + #[test] + fn test_detect_thinking_cannot_be_modified() { + // 场景6: thinking blocks cannot be modified + assert!(should_rectify_thinking_signature( + Some("thinking or redacted_thinking blocks in the response cannot be modified"), + &enabled_config() + )); + } + + #[test] + fn test_detect_invalid_request() { + // 场景7: 非法请求(与 CCH 对齐,统一触发) + assert!(should_rectify_thinking_signature( + Some("非法请求:thinking signature 不合法"), + &enabled_config() + )); + assert!(should_rectify_thinking_signature( + Some("illegal request: tool_use block mismatch"), + &enabled_config() + )); + assert!(should_rectify_thinking_signature( + Some("invalid request: malformed JSON"), + &enabled_config() + )); + } + + #[test] + fn test_do_not_detect_thinking_type_tag_mismatch() { + // 与 CCH 对齐:adaptive tag mismatch 不触发签名整流器 + assert!(!should_rectify_thinking_signature( + Some("Input tag 'adaptive' found using 'type' does not match expected tags"), + &enabled_config() + )); + } + + // ==================== adaptive thinking type 测试 ==================== + + #[test] + fn test_rectify_keeps_adaptive_when_no_legacy_blocks() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "adaptive" }, + "messages": [{ + "role": "user", + "content": [{ "type": "text", "text": "hello" }] + }] + }); + + let result = rectify_anthropic_request(&mut body); + + assert!(!result.applied); + assert_eq!(body["thinking"]["type"], "adaptive"); + assert!(body["thinking"].get("budget_tokens").is_none()); + } + + #[test] + fn test_rectify_adaptive_preserves_existing_budget_tokens() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "adaptive", "budget_tokens": 5000 }, + "messages": [{ + "role": "user", + "content": [{ "type": "text", "text": "hello" }] + }] + }); + + let result = rectify_anthropic_request(&mut body); + + assert!(!result.applied); + assert_eq!(body["thinking"]["type"], "adaptive"); + assert_eq!(body["thinking"]["budget_tokens"], 5000); + } + + #[test] + fn test_rectify_does_not_change_enabled_type() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "enabled", "budget_tokens": 1024 }, + "messages": [{ + "role": "user", + "content": [{ "type": "text", "text": "hello" }] + }] + }); + + let result = rectify_anthropic_request(&mut body); + + assert!(!result.applied); + assert_eq!(body["thinking"]["type"], "enabled"); + } + + #[test] + fn test_rectify_removes_top_level_thinking_adaptive() { + // 顶层 thinking 仅在 type=enabled 且 tool_use 场景才会删除,adaptive 不删除 + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "adaptive" }, + "messages": [{ + "role": "assistant", + "content": [ + { "type": "tool_use", "id": "toolu_1", "name": "WebSearch", "input": {} } + ] + }, { + "role": "user", + "content": [{ "type": "tool_result", "tool_use_id": "toolu_1", "content": "ok" }] + }] + }); + + let result = rectify_anthropic_request(&mut body); + + assert!(!result.applied); + assert_eq!(body["thinking"]["type"], "adaptive"); + } + + #[test] + fn test_rectify_adaptive_still_cleans_legacy_signature_blocks() { + let mut body = json!({ + "model": "claude-test", + "thinking": { "type": "adaptive" }, + "messages": [{ + "role": "assistant", + "content": [ + { "type": "thinking", "thinking": "t", "signature": "sig_thinking" }, + { "type": "text", "text": "hello", "signature": "sig_text" } + ] + }] + }); + + let result = rectify_anthropic_request(&mut body); + + assert!(result.applied); + assert_eq!(result.removed_thinking_blocks, 1); + let content = body["messages"][0]["content"].as_array().unwrap(); + assert_eq!(content.len(), 1); + assert_eq!(content[0]["type"], "text"); + assert!(content[0].get("signature").is_none()); + assert_eq!(body["thinking"]["type"], "adaptive"); + } + + // ==================== normalize_thinking_type 测试 ==================== + + #[test] + fn test_normalize_thinking_type_adaptive_unchanged() { + let body = json!({ + "model": "claude-test", + "thinking": { "type": "adaptive" } + }); + + let result = normalize_thinking_type(body); + + assert_eq!(result["thinking"]["type"], "adaptive"); + assert!(result["thinking"].get("budget_tokens").is_none()); + } + + #[test] + fn test_normalize_thinking_type_enabled_unchanged() { + let body = json!({ + "model": "claude-test", + "thinking": { "type": "enabled", "budget_tokens": 2048 } + }); + + let result = normalize_thinking_type(body); + + assert_eq!(result["thinking"]["type"], "enabled"); + assert_eq!(result["thinking"]["budget_tokens"], 2048); + } + + #[test] + fn test_normalize_thinking_type_disabled_unchanged() { + let body = json!({ + "model": "claude-test", + "thinking": { "type": "disabled" } + }); + + let result = normalize_thinking_type(body); + + assert_eq!(result["thinking"]["type"], "disabled"); + } + + #[test] + fn test_normalize_thinking_type_preserves_budget() { + let body = json!({ + "model": "claude-test", + "thinking": { "type": "adaptive", "budget_tokens": 5000 } + }); + + let result = normalize_thinking_type(body); + + assert_eq!(result["thinking"]["type"], "adaptive"); + assert_eq!(result["thinking"]["budget_tokens"], 5000); + } + + #[test] + fn test_normalize_thinking_type_no_thinking() { + let body = json!({ + "model": "claude-test" + }); + + let result = normalize_thinking_type(body); + + assert!(result.get("thinking").is_none()); + } + + #[test] + fn test_normalize_thinking_type_unknown_unchanged() { + let body = json!({ + "model": "claude-test", + "thinking": { "type": "unexpected", "budget_tokens": 100 } + }); + + let result = normalize_thinking_type(body); + + assert_eq!(result["thinking"]["type"], "unexpected"); + assert_eq!(result["thinking"]["budget_tokens"], 100); + } } diff --git a/src-tauri/src/proxy/types.rs b/src-tauri/src/proxy/types.rs index 82ad47cb6..cf8dd7c8c 100644 --- a/src-tauri/src/proxy/types.rs +++ b/src-tauri/src/proxy/types.rs @@ -195,17 +195,22 @@ pub struct AppProxyConfig { /// 整流器配置 /// /// 存储在 settings 表中 -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RectifierConfig { - /// 总开关:是否启用整流器 - #[serde(default)] + /// 总开关:是否启用整流器(默认开启) + #[serde(default = "default_true")] pub enabled: bool, - /// 请求整流:启用 thinking 签名整流器 + /// 请求整流:启用 thinking 签名整流器(默认开启) /// /// 处理错误:Invalid 'signature' in 'thinking' block - #[serde(default)] + #[serde(default = "default_true")] pub request_thinking_signature: bool, + /// 请求整流:启用 thinking budget 整流器(默认开启) + /// + /// 处理错误:budget_tokens + thinking 相关约束 + #[serde(default = "default_true")] + pub request_thinking_budget: bool, } fn default_true() -> bool { @@ -216,6 +221,16 @@ fn default_log_level() -> String { "info".to_string() } +impl Default for RectifierConfig { + fn default() -> Self { + Self { + enabled: true, + request_thinking_signature: true, + request_thinking_budget: true, + } + } +} + /// 日志配置 /// /// 存储在 settings 表的 log_config 字段中(JSON 格式) @@ -261,32 +276,49 @@ mod tests { use super::*; #[test] - fn test_rectifier_config_default_disabled() { - // 验证 RectifierConfig::default() 返回全禁用状态 + fn test_rectifier_config_default_enabled() { + // 验证 RectifierConfig::default() 返回全开启状态 let config = RectifierConfig::default(); - assert!(!config.enabled, "整流器总开关默认应为 false"); + assert!(config.enabled, "整流器总开关默认应为 true"); assert!( - !config.request_thinking_signature, - "thinking 签名整流器默认应为 false" + config.request_thinking_signature, + "thinking 签名整流器默认应为 true" + ); + assert!( + config.request_thinking_budget, + "thinking budget 整流器默认应为 true" ); } #[test] fn test_rectifier_config_serde_default() { - // 验证反序列化缺字段时使用默认值 false + // 验证反序列化缺字段时使用默认值 true let json = "{}"; let config: RectifierConfig = serde_json::from_str(json).unwrap(); - assert!(!config.enabled); - assert!(!config.request_thinking_signature); + assert!(config.enabled); + assert!(config.request_thinking_signature); + assert!(config.request_thinking_budget); } #[test] fn test_rectifier_config_serde_explicit_true() { // 验证显式设置 true 时正确反序列化 - let json = r#"{"enabled": true, "requestThinkingSignature": true}"#; + let json = + r#"{"enabled": true, "requestThinkingSignature": true, "requestThinkingBudget": true}"#; let config: RectifierConfig = serde_json::from_str(json).unwrap(); assert!(config.enabled); assert!(config.request_thinking_signature); + assert!(config.request_thinking_budget); + } + + #[test] + fn test_rectifier_config_serde_partial_fields() { + // 验证只设置部分字段时,缺失字段使用默认值 true + let json = r#"{"enabled": true, "requestThinkingSignature": false}"#; + let config: RectifierConfig = serde_json::from_str(json).unwrap(); + assert!(config.enabled); + assert!(!config.request_thinking_signature); + assert!(config.request_thinking_budget); } #[test] diff --git a/src/components/settings/RectifierConfigPanel.tsx b/src/components/settings/RectifierConfigPanel.tsx index eeb6e10b4..be75a2282 100644 --- a/src/components/settings/RectifierConfigPanel.tsx +++ b/src/components/settings/RectifierConfigPanel.tsx @@ -10,6 +10,7 @@ export function RectifierConfigPanel() { const [config, setConfig] = useState({ enabled: true, requestThinkingSignature: true, + requestThinkingBudget: true, }); const [isLoading, setIsLoading] = useState(true); @@ -69,6 +70,21 @@ export function RectifierConfigPanel() { } />
+
+
+ +

+ {t("settings.advanced.rectifier.thinkingBudgetDescription")} +

+
+ + handleChange({ requestThinkingBudget: checked }) + } + /> +
); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index d5bd4632a..c697a891b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -214,7 +214,9 @@ "requestGroup": "Request Rectification", "responseGroup": "Response Rectification", "thinkingSignature": "Thinking Signature Rectification", - "thinkingSignatureDescription": "Automatically fix Claude API errors caused by thinking signature validation failures" + "thinkingSignatureDescription": "When an Anthropic-type provider returns thinking signature incompatibility or illegal request errors, automatically removes incompatible thinking-related blocks and retries once with the same provider", + "thinkingBudget": "Thinking Budget Rectification", + "thinkingBudgetDescription": "When an Anthropic-type provider returns budget_tokens constraint errors (such as at least 1024), automatically normalizes thinking to enabled, sets thinking budget to 32000, and raises max_tokens to 64000 if needed, then retries once" }, "logConfig": { "title": "Log Management", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 908d45f4f..d2fdfa44e 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -214,7 +214,9 @@ "requestGroup": "リクエスト整流", "responseGroup": "レスポンス整流", "thinkingSignature": "Thinking 署名整流", - "thinkingSignatureDescription": "Claude API の thinking 署名検証エラーを自動修正" + "thinkingSignatureDescription": "Anthropic タイプのプロバイダーが thinking 署名の非互換性や不正なリクエストエラーを返した場合、互換性のない thinking 関連ブロックを自動削除し、同じプロバイダーで 1 回リトライします", + "thinkingBudget": "Thinking Budget 整流", + "thinkingBudgetDescription": "Anthropic タイプのプロバイダーが budget_tokens 制約エラー(例: 1024 以上)を返した場合、thinking を enabled に正規化し、thinking 予算を 32000 に設定し、必要に応じて max_tokens を 64000 に引き上げて 1 回リトライします" }, "logConfig": { "title": "ログ管理", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index e325e9bbe..cbe8eb0ee 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -214,7 +214,9 @@ "requestGroup": "请求整流", "responseGroup": "响应整流", "thinkingSignature": "Thinking 签名整流", - "thinkingSignatureDescription": "自动修复 Claude API 中因 thinking 签名校验失败导致的请求错误" + "thinkingSignatureDescription": "当 Anthropic 类型供应商返回 thinking 签名不兼容或非法请求等错误时,自动移除不兼容的 thinking 相关块并对同一供应商重试一次", + "thinkingBudget": "Thinking Budget 整流", + "thinkingBudgetDescription": "当 Anthropic 类型供应商返回 budget_tokens 约束错误(如至少 1024)时,自动将 thinking 规范为 enabled 并将 budget 设为 32000,同时在需要时将 max_tokens 设为 64000,然后重试一次" }, "logConfig": { "title": "日志管理", diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index 43daeed37..cd8de3666 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -155,6 +155,7 @@ export const settingsApi = { export interface RectifierConfig { enabled: boolean; requestThinkingSignature: boolean; + requestThinkingBudget: boolean; } export interface LogConfig { From 2edee3163890528ad77af8caf06cc6a1f25575f4 Mon Sep 17 00:00:00 2001 From: ThendCN Date: Thu, 12 Feb 2026 22:53:33 +0800 Subject: [PATCH 03/51] fix(linux): disable WebKitGTK hardware acceleration to prevent white screen (#986) On some Linux systems with AMD GPUs (e.g., AMD Cezanne/Radeon Vega), WebKitGTK's GPU process fails to initialize EGL, causing the app to show a white screen on startup. Root cause: `amdgpu_query_info(ACCEL_WORKING)` returns EACCES (-13), which causes EGL display creation to fail with EGL_BAD_PARAMETER, crashing the GPU process. Fix: Use WebKitGTK's `set_hardware_acceleration_policy(Never)` API to disable GPU acceleration on Linux, falling back to software rendering. This is applied at startup via Tauri's `with_webview` API. Co-authored-by: Naozhong AI Co-authored-by: Claude Opus 4.6 --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 3 +++ src-tauri/src/lib.rs | 15 +++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d09f0ad7b..49e1759f0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -746,6 +746,7 @@ dependencies = [ "tower-http 0.5.2", "url", "uuid", + "webkit2gtk", "winreg 0.52.0", "zip 2.4.2", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 420d6436a..7346899a6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -65,6 +65,9 @@ uuid = { version = "1.11", features = ["v4"] } [target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] tauri-plugin-single-instance = "2" +[target.'cfg(target_os = "linux")'.dependencies] +webkit2gtk = { version = "2.0.1", features = ["v2_16"] } + [target.'cfg(target_os = "windows")'.dependencies] winreg = "0.52" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9c246ff90..e2657745a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -768,6 +768,21 @@ pub fn run() { restore_proxy_state_on_startup(&state).await; }); + // Linux: 禁用 WebKitGTK 硬件加速,防止 EGL 初始化失败导致白屏 + #[cfg(target_os = "linux")] + { + if let Some(window) = app.get_webview_window("main") { + let _ = window.with_webview(|webview| { + use webkit2gtk::{WebViewExt, SettingsExt, HardwareAccelerationPolicy}; + let wk_webview = webview.inner(); + if let Some(settings) = WebViewExt::settings(&wk_webview) { + SettingsExt::set_hardware_acceleration_policy(&settings, HardwareAccelerationPolicy::Never); + log::info!("已禁用 WebKitGTK 硬件加速"); + } + }); + } + } + // 静默启动:根据设置决定是否显示主窗口 let settings = crate::settings::get_settings(); if let Some(window) = app.get_webview_window("main") { From b0a177725dc343cb16156370c3a5f9155d97ca35 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 9 Feb 2026 23:04:45 +0800 Subject: [PATCH 04/51] fix: remove partner status from Zhipu GLM presets --- src/config/claudeProviderPresets.ts | 4 ---- src/config/opencodeProviderPresets.ts | 4 ---- src/i18n/locales/en.json | 1 - src/i18n/locales/ja.json | 1 - src/i18n/locales/zh.json | 1 - 5 files changed, 11 deletions(-) diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 01b9b19dd..52d03b50c 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -99,8 +99,6 @@ export const providerPresets: ProviderPreset[] = [ }, }, category: "cn_official", - isPartner: true, // 合作伙伴 - partnerPromotionKey: "zhipu", // 促销信息 i18n key icon: "zhipu", iconColor: "#0F62FE", }, @@ -119,8 +117,6 @@ export const providerPresets: ProviderPreset[] = [ }, }, category: "cn_official", - isPartner: true, // 合作伙伴 - partnerPromotionKey: "zhipu", // 促销信息 i18n key icon: "zhipu", iconColor: "#0F62FE", }, diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index c41ce2b25..c01c1448d 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -445,8 +445,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, category: "cn_official", - isPartner: true, - partnerPromotionKey: "zhipu", icon: "zhipu", iconColor: "#0F62FE", templateValues: { @@ -479,8 +477,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, category: "cn_official", - isPartner: true, - partnerPromotionKey: "zhipu", icon: "zhipu", iconColor: "#0F62FE", templateValues: { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index c697a891b..7c65a6857 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -490,7 +490,6 @@ "officialHint": "💡 Official provider uses browser login, no API Key needed", "getApiKey": "Get API Key", "partnerPromotion": { - "zhipu": "Zhipu GLM is an official partner of CC Switch. Use this link to top up and get a 10% discount", "packycode": "PackyCode is an official partner of CC Switch. Register using this link and enter \"cc-switch\" promo code during recharge to get 10% off", "minimax_cn": "MiniMax Coding Plan Special Offer, Starter from ¥9.9", "minimax_en": "MiniMax Coding Plan Black Friday, Starter is now $2/mo (80% OFF!)", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index d2fdfa44e..f8c9243fe 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -490,7 +490,6 @@ "officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です", "getApiKey": "API Key を取得", "partnerPromotion": { - "zhipu": "Zhipu GLM は CC Switch の公式パートナーです。リンク経由でチャージすると 10% 割引", "packycode": "PackyCode は CC Switch の公式パートナーです。登録後チャージ時に \"cc-switch\" を入力すると 10% オフ", "minimax_cn": "MiniMax Coding Plan 特別価格、Starter ¥9.9 から", "minimax_en": "MiniMax Coding Plan Black Friday、Starter が月額 $2(80% OFF)", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index cbe8eb0ee..a5f54bd4e 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -490,7 +490,6 @@ "officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key", "getApiKey": "获取 API Key", "partnerPromotion": { - "zhipu": "智谱 GLM 是 CC Switch 的官方合作伙伴,使用此链接充值可以获得9折优惠", "packycode": "PackyCode 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"cc-switch\" 优惠码,可以享受9折优惠", "minimax_cn": "MiniMax Coding Plan 特惠,Starter 套餐 9.9 元起", "minimax_en": "MiniMax Coding Plan 黑五特惠,Starter 套餐现仅 $2/月(2折优惠!)", From 1ecc0fff303cc02e2f16809634ddda9746687cf1 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 13 Feb 2026 22:47:26 +0800 Subject: [PATCH 05/51] docs: update MiniMax sponsor to M2.5 across all READMEs --- README.md | 6 +++--- README_JA.md | 6 +++--- README_ZH.md | 2 +- assets/partners/banners/minimax-en.jpeg | Bin 0 -> 211827 bytes assets/partners/banners/minimax-en.jpg | Bin 155773 -> 0 bytes assets/partners/banners/minimax-zh.jpeg | Bin 185791 -> 197994 bytes 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 assets/partners/banners/minimax-en.jpeg delete mode 100644 assets/partners/banners/minimax-en.jpg diff --git a/README.md b/README.md index fd3c0205b..4553aa825 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ English | [中文](README_ZH.md) | [日本語](README_JA.md) | [Changelog](CHANG ## ❤️Sponsor -[![MiniMax](assets/partners/banners/minimax-en.jpg)](https://bit.ly/3Nue8mA) +[![MiniMax](assets/partners/banners/minimax-en.jpeg)](https://platform.minimax.io/subscribe/coding-plan?code=ClLhgxr2je&source=link) -MiniMax M2.1 is an open-source, SOTA model built for real-world development and agentic workflows. It delivers top-tier performance on major coding benchmarks such as SWE, VIBE, and Multi-SWE. Powered by a 10B active / 230B total MoE architecture, M2.1 enables faster inference, easier deployment, and even local execution. It excels at coding, navigating digital environments, and handling long, multi-step tasks at scale. +MiniMax-M2.5 is a SOTA large language model designed for real-world productivity. Trained in a diverse range of complex real-world digital working environments, M2.5 builds upon the coding expertise of M2.1 to extend into general office work, reaching fluency in generating and operating Word, Excel, and Powerpoint files, context switching between diverse software environments, and working across different agent and human teams. Scoring 80.2% on SWE-Bench Verified, 51.3% on Multi-SWE-Bench, and 76.3% on BrowseComp, M2.5 is also more token efficient than previous generations, having been trained to optimize its actions and output through planning. -[Click](https://bit.ly/3Nue8mA) to get an exclusive 12% off the MiniMax Coding Plan! +[Click](https://platform.minimax.io/subscribe/coding-plan?code=ClLhgxr2je&source=link) to get an exclusive 12% off the MiniMax Coding Plan! --- diff --git a/README_JA.md b/README_JA.md index 65396cebf..c3a2a80b2 100644 --- a/README_JA.md +++ b/README_JA.md @@ -15,11 +15,11 @@ ## ❤️スポンサー -[![MiniMax](assets/partners/banners/minimax-en.jpg)](https://bit.ly/3Nue8mA) +[![MiniMax](assets/partners/banners/minimax-en.jpeg)](https://platform.minimax.io/subscribe/coding-plan?code=ClLhgxr2je&source=link) -MiniMax M2.1 は、実務開発とエージェントワークフロー向けに構築されたオープンソースの最先端モデルです。100 億のアクティブパラメータ / 2,300 億の総パラメータを持つ MoE アーキテクチャにより、高速な推論、簡単なデプロイ、ローカル実行にも対応します。SWE、VIBE、Multi-SWE などの主要コーディングベンチマークでトップクラスの性能を発揮し、コーディング、デジタル環境のナビゲーション、大規模な多段階タスクの処理に優れています。 +MiniMax-M2.5 は、実際の生産性向上のために設計された最先端の大規模言語モデルです。多様で複雑な実環境のデジタルワークスペースでトレーニングされた M2.5 は、M2.1 のコーディング能力をベースに一般的なオフィス業務へと拡張し、Word・Excel・PowerPoint ファイルの生成と操作、多様なソフトウェア環境間のコンテキスト切り替え、異なるエージェントや人間チーム間での協働を流暢にこなします。SWE-Bench Verified で 80.2%、Multi-SWE-Bench で 51.3%、BrowseComp で 76.3% を達成し、計画的な行動と出力の最適化トレーニングにより、前世代よりもトークン効率に優れています。 -[こちら](https://bit.ly/3Nue8mA)から MiniMax Coding Plan の限定 12% オフを入手! +[こちら](https://platform.minimax.io/subscribe/coding-plan?code=ClLhgxr2je&source=link)から MiniMax Coding Plan の限定 12% オフを入手! --- diff --git a/README_ZH.md b/README_ZH.md index 587365fe1..e762dc6ec 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -17,7 +17,7 @@ [![MiniMax](assets/partners/banners/minimax-zh.jpeg)](https://platform.minimaxi.com/subscribe/coding-plan?code=7kYF2VoaCn&source=link) -MiniMax M2.x 系列模型是面向实际开发与智能体工作流打造的编码模型,M2.1 基于 100 亿激活 / 2300 亿总参的混合专家架构打造,推理更快、部署更便捷且支持本地运行,在 SWE、VIBE、Multi-SWE 等主流代码评测基准中均表现顶尖,擅长代码开发、数字环境适配及规模化处理长链路多步骤任务。 +MiniMax M2.5 在编程、工具调用与搜索、办公等核心生产力场景均达到或刷新行业 SOTA,拥有架构师级代码能力与高效任务拆解能力,推理速度较上一代提升 37%、token 消耗更优;100 token/s 连续工作一小时仅需 1 美金,让复杂 Agent 规模化部署经济可行,已在企业多职能场景深度落地,加速全民 Agent 时代到来。 [点击](https://platform.minimaxi.com/subscribe/coding-plan?code=7kYF2VoaCn&source=link)即可领取 MiniMax Coding Plan 专属 88 折优惠! diff --git a/assets/partners/banners/minimax-en.jpeg b/assets/partners/banners/minimax-en.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..fc0884385ec36646e2e6366aa5a2807c95760a43 GIT binary patch literal 211827 zcmeFZ1z23mvM4+R2^u6wkU(&U;K75#U}10%GWg&UJPB^W26tz0cS(Y~y9IX$9{iEJ zPxjvTy>tHi-Shr;?z{iLYrdIP)2q9xy1JxSuhr{*`u-aL3n(cg34ntG0N@@zfcq7= zX&Esw{Wl;*Nf|kb-%fM`9suDb006Oevh z6Sr3Zi9O)P4>+CapK!xJ;YPOhKkJ7+)DeJKIsN4I6MjmJ4z^ZRemEjOd`SQf01!Y5 zAo{ca5C0#)CKCX7bp`;yNBmJ{m;eCO`T+pXX8$OoN(TTieF1=)p+Cz0(21>qy}`ec zLwNXoWMTpU9OnT57@7dU(_sJrRr}x0J^cIw*RCG*qRJ12(C@2`% z7*8;i#Jg+^^080FRN8P~Z{a5KtdIoOi?m01zKUM?}VXf`s%K34jTJdjyYw zi1Zj48;2a_DVvBA?lTH?11D$&9swacH_s~-RXYdAnDWZ1k#T%VPA*Y3d*=^P(Xpdr z93aEUj5R8akJVqkiit}o8-X*aX=r)dmyKO~eEqUkztfwT=47uNJ#Zy?@C-P_pYHKH zAU^2rG4i9IGAm*KlmQ+A9qsWWLlh6qj;rqvjdAp9P>lDBuw`JT^cW(ES-; zo>lXUFZ@R<6}eBjBsVN#TSAcm-3Hx5^l>2L?9|lhfi6w0Txe(AdGz^Y-­ivJEX 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�g{|Aql+d zgHl#VrmFXV*+}P!Le%z%5J6%NoT^ow?lip2XJDGyW{1o8t!LU`zEew3RCObTNl%iU zL5{dWuRa^hv;s;XlieVBXj_VARrIW!Uc)Zn)Oou~ON?rcnbs|~^sAl4I%wQ|K&q)< z{S!t}X?dIuybq{&EMj5ZEu*0t+(Ek^S_3Y6*N}QlMXr_Lt!sIJX2xCA)WoYH?}I@X zXfK~*)>WOG#>-yqK3O=Q+AjBW8URpddj0nX{9joH|0R+xjDdokeYm?rzW*M8bdF#` zjk7y!9$0045jd`@Wohgy(9Fpqn7OAu&{QReto;&ThDmtblhBkjzNqT;Y2SVX;>M-s z@5hhQzFi9bA(xumaKI8dsrds2SuhuO5_pqGdO*HzUgbR_Bu4gnf(XJ_j|re-rIiSf)uOp0{M*-Z}^#Jw8kfj>yPmkkD3ZFaAJ@Go0-7ti7~a{ln8RRa>!{3fqU(;%HHn7Bu_uW0UME=oV`+GzY1i z*JH%J2aKBYv0ruq!inU&K-CK7MZdC?{Y^&SAfF)mFhj{%a2uq#pb*akQRyc6LR!fOS8nXbuUF&ZhglBG; zYO&w#&__skld{g`uJn$e)(;&EQ7KpN)l%%dOUV0I>ly*{7|ij<1YJ1?zPV#$Yb`&>a`N9_U%GbB)D^WA!y@^ z;phWCLzni(h`k%Zx&z}va4&>gY%k#+uo#6I!aABI^JMh{8Ag)HWV?q!>=)SO$?9DB zb0r%fp;ekk;N^MPIoHjHcwHG|b@6yT^rzKc{s}YAqiUjYmGLd0!l`8+cXg%8Q5mWr zdI}1P;lTQU2)M*V&OZ;ue<_Y`eb<&mj0>6JPJJ3D zcG!4=wtF!Bqt9ZTK87xM+A+Hf((-9L@<7u?TDmIumb8_S9fpTwoFP&Aws?I%jvixZ zRZZum| zEwf4W0o5>e+}~J>{32v6cMv8uOFg+>G}+vq_=x@Zrdklpb0#hDH$oUaDI@vU;tliF ziBomauO;>LWAln~?_iIA5vu2C;36}N(~hmwy1+3*?)xSAFG4&tE|OXMIJ@?fmU=7l zaD=}kC;b~Cq_x4V@xN5?e?$eOMK@NuZxgdMaS5Xt#_&3%Y=XJ+$32qh6wb(O-N4~| z7nB}EdAyRQ#)ZYKC?n2uy5jw_1*PRU3H>!Ut=rD3g0gnzZtlJw;)a47o(x;kl}#iG zI7KU5U6`EDuH}@*h+PUGSDZC1`)a_)HGV7v>LxkAJ_;>dJY7&Ao^9eJJsl@ml zXZ2@6j@Jfbvm6hRas9+hs(ZxIhFHSDpYr9rS${^XrVRI-nG7|Z48E_pL|tG=yonk( z?#U3?q1mByL@s6(2^ zJyW2fUCdt0^9nQ#Im>Uo$(*M=hzzV`Jqb<=Q?v3T^)0AGzZ*Um-XOU4??aBmSuZ>4 zMNnR`VAH87P{4Mkcg>;yF?*rn*Uf64NijjnTX#JYtFkM$y@7t0HdkCozZJ`mKK4R z=cMJ=>`#6n7@cV&bW6oc>sGr5v>}#h*Dy>?)!fGgyV`e~Q4$3+jwuEbdGtBjZWo+{c#9wSsR<1_46~k(xAt=p zts0wOKbeoR>d$s=;N<%9l&{($?~9#cIN_-tJXzar%aa$=&I^}vV=xw=IIt2ys(aoa zv?F^RD^^iFxsAU-0rqMIg)8Qc&8P%{vR_F)Oi1p zCq1 zNlCq_;qO&*uyylOG>MU_v3R-MrO+o=(F^h=U|BhDM=Zp*ABp?U9B0q$P>d1e2)1&s zHG_25M3W>iK;^PK-u=MCW;|l*MqQuk@+WPZ6gCn0g@_UUwE84ruUhYz%F?>>bjCP+ z(r50N8n+3Le<4V#id#^d$;0Csrwe>eMEl*v=B>pMEOW2dHREbWUn4H6o zMa!Ma2bZMzj3uRqMCy*Gd@K5-30>fvA@9nU@j&GuOm^XRp+)1Nhc0jkPTveR%T!5iA z>_1MF!ea^s!|pPW))ltKZ4UO2JDo0`)*(~=Du4V-9*o|HB)_yUw;uWsI&A-roWwB? z(~Q=}+CIXmd4+E;KK+va`MYPXd#Z|O$UF=WS>D^lCYGbLF6Fbx@8x{oU!F`Cr0rl? zKXg>`?qG@OA$;+_|M+|P?;W0pEf9tMrHJtte<|W`F7THk{!+xh#g6}bV+2!J=8uwh+ow1TF_NbN@KPL&kEw!F%Nsz=ooJZufm6{)2;9&-h>EDW_GMpS3%tCLQSCn!l z&AIA2*uy|!Tz)pI&m3d*zcEt+JL9e=8C{1HqN0GzKg2#^Z5_l=Lsb|$ZIBbkz#Zb6rjVS} z703H51_bIZH!YqCD<;R#+q-L5;8Oh}MC}8oQMSsD%O6}aKyjX)8=SyEMWM}-6q+IF zhQHX@-PrVXkXlAd#!QKxfmyoyjFKhtD3TbPVS-^!-D{-r2!xS=p;4H`o_WHcil0my zf!+Di$gkA*Wg|c>S8SJgxgxl0-gg4~7;=bca6d;FOB zawK}AH-RAu;_jgV4@>H6_Fyhswz{&kM-*R!gFi`RW^PI-AOZkF(9i#qs{R#!mt%04 zHt>k7OtRUVdm-aEqh2$l%kSqCTs{{z4XBgi%#NX7%BxI{nqGm#sa^v_tc`8x4vOX- z0Q<_IaMv^T*1&hW1ih?gjOt$|ZbY2`2zZmMP`zQbQlG*fK1KR;&FRJZkJXP*#sOIB z8ZAohB3dm^N>`qgdt{z}toC@W-%k4mAfjt{DY1`e@q@25kgwx7WEOSeyOTFo%4;}8 zr;v4mD!jsPJ9jVsV)MVLwC8vi&acagLi{faJiBZ_ITOY212ARv5;?F&?FowX)1yR7c#~b4x@JJt@m3JN0fwV)<(IE z@~!259MIFktU;P)16r}Sy$f~v*QP#bH0NVsb?;0X%4=JJ&?!=CcQ>mY+D0;@?|B%6 zkPu8vGfq*nD$x~DQ61KM0DbNh@5y`4%>V%s-l+cK-ia9(ORsG#u;dD7#R zEbPQ^kg$MzJr^-spO9D?C6g78&|Ib~m;IIw#5osQ{{kv;4-nT0Bd`-*WHS5BNgq8O zRD%3>nGwfXBK!_bE<6OJz*!dNn^$9xE(&W*<~*fUjmCu zwBu)lv?oMGWeSN=Qc{wiiE|cz?SoEzuYEd1%Cw^uJkh+wHU6XO9IK!UR;nwoqsP+U zT3w?HK;!wc@R3)g|A_|kZt&WdLai(ZC0(}=9oJ43>sQyniCTgg`{EeREFlLjG>$(U z(!clzH3w-jO_$Wm+~q`h?debQ?N*b;I2}sOZYjD06`(|+Xb>OL;p_inzUrsHOx>%e zU$#r{!h8~-p)OXt%NaD@k!j2gjeAu~UDF#0OE_S6xk2iX5Lz}Ytd{TLn!)HWD=3zX z#(qA5JDK_;;U$v!mnm(x)~F_iNznGndOhusuti_RP$Q*CUC8HM_dHg-(jFv8F>2O%r{xN zK@N2|R`VuyAcU|kxe%3_DE4Wqs^O41jzy!aZ=Jp3I)E=fe)u>l534ty;lV0YRygeu z@UWTV8!xg?*|qwvIwgKkY`uR`@<&j3{znv5NC$ju_#=}35k<-Q-akV9Ba({zVH>_X z0A$8L0`}uSg7wpUz+Y_sQrcgZ_LtxM6>NT=>;4rw|8c%K%Uz?TtxQ~5`b?}-ONbNE zJ8C|a-XrP-KxAltf+CQwhb~Zn$X;$Kr%A}YhvJ_*CzML>9v+>oyr=T5qyJoA&+Fi` zcJo|G0y^}vE4YQmfGYHYn8uwYHMvoc(*CnRb68*k3orwOZWeVY9g$pYZ=#~bQYraN zJtRBs}q_;!d8zwBeqJgYv?u}tx4mC2tk;If%4f%Cc z3b;2q50}ibj>{#EwVz+jxWdqDDfnS_c{sA7qi#`A?%my&-S+5oi*(f;|I3x%`;*db zx{_;&AFC0c_Z-u!Rhz)Dy3+y_BgwObz!}wat?hn9JqzKlTaQR# z?EC(SQRc&{%oxHm)HclRc@SMb3)B7h#@dT??eJy}Aqmk8W7?5{o<*ofg~YQUbuQdl z9!bXalImho)V0=jU@jW4!1M=f##9OeLaZ6$wOO-`uAD9Jxm6MI)%ndf4k)`&L6m(_ zyWZ&l4I{3%s9_Ra%VG1Cwct2HPyUcHJH!IQm&UvG5gXsV8D)0i$bHQ>BUWF!P;qg{ zqw%9JXHTL%ErH$fY#5I9O3%;dhocyy3WLYGzP2Ka`x$oR-vdlFVXBRqt{viD<&WYr z72Te(Luh2Srp%@G6G9$`z2grkHlTiZDy9|(ZCryf273d$q6$_qB_^ayNilr3jrF{5 zr_1sr_i^ZO&8vxR8JCMx#Xv(LbZ&9uugwhAAg%9%{XJ6U`{g}#^lw=Q)!N<9`p3L-2<+8?g23TbX|?!LWkAO3p zoF^uJM2gvgXXKFpcqLu4guCv`ffDe_vBZe=(}E-h1cd$Qu1x9iHPu6w>Z(W##%OJW*D-{b%@ME)pn- zx?N?SvicuKm8h$aV;2+cVB6F-2k&X!oFvoKKXxd@@CHS!r6+^WNvLa96eC76=Zys; z^X3?zaxi`|4Lq{FAfW7Ru#L@oF@_^XlT%%$QzD_&nx!S(J>Iij%Hv}B4icoMz%m2f zy$4`gj_jNQv)rv@z&bar13Y)`kUy9>g| zs|k!#Jaz+xzi6puMtO9Kvqu0tHaD`E-UUS8XshD5m^03b21#t{ zibO(ap`q2xvlv6hirgw56L0tUs_LzHT!)jy`9Gn`1&JTw2Sq%*(P*JTqDuF1RjL7< zQ7VGLpuryfJNFphPbn`i^?~Lp>gz;@;xO+Tn6A1n<=88i5vMTZLu5TGz)GELD@MhFbJNs!GhB;(VCDhz(Mg=jK zI_ECtowpOFuCiJ}Lp9$2)^#L|Fb!Vg)kWfovPP^ICGw;tzWYfQGfr_>nV{zo{g2uC zA>K7QtGocDe7n*QiFpz=7BO;|?XYTd=Ik_!0A;I0dQ_Rq_PXfdiQHVt%ma+rw;ecn zR^QG-Ad^`Pf}$}H4-c>k=bp!|k^>5OEspV_Y+l*} z4++LRJ4lfbJ2CTB=??EeY0AUyCuU>-r)l)O8cfd6`ibt1rK$ze5{O2GNM|LYIPRGe z>22=Bz)0hFiDWEO6g?ur*rKI3N23$|R!WBwmn^Fg{9aYI9O|cuTi1qaerZnYC>7|_ z6Ld;DFBfQydd)F8E7m}`;^yq<-QAXbJrq%u^msult+fG7TeqS67qc}My;52&7*A6Q z*LE`V*vSV0@XxuW|DDbe9;bNANVv?hwwITCVH_&4;9s=p4`pLGn(B`#>7IuiF+S}TXJc}#DvuIaCxrEqMJdvQ+8e3PV!|O5^lsz`x!g`s7g6A z*fc{gk)t5%FZT)-UX0)#X1)?c<26FBzi2p#FqU29{bYVy%R?t$xwa_KdH>GCS2atg zxR_(4EOA{ijLu1!Bfwdh*9NrzPW~&fmb?Q7oY|O=s0)>k7YS35Qwu%8ka~ru5lG#N z4IB@nZG=1?OmT6HJ&5e^sliAe#2qHqZADy#jS)uETUW%uCK^71syUdw(Oh1a)+v02 zSoOI+mIX!QbE#!1P$gV$fE2O=uRL}X=P2;$Im|Ria#o~*V!I0hVR93>5U}*Q=TdD> zUb^OS9}-oeYWGgTI;EvnUQLQqDV)P+B(hNZb>;8DKT?pyLS@1rm#K3-l$E=2Hc|W3$>xj)OjXe87w=gys7{^IzGpm~_wZOccUJ?ilQAxAC zuptxCKnqigefzz@FF4ltqY6Wly>dTM;Cv7{e~N*dJw57pV@}2~A%F^tC}~-HaKI_# zE2+M%*It}FqvvRXy^48-%2;8n`L&LCoew2d1_KvUNDNTM3nk+G9Wfg#)?p~HU0OZm zP^YAvC;)p>JAcStR9mx~or@~gG-JEb7T)qLXZhFCkat;TCbORS_~JipI*}F2d4RI9~*G ztL^5{iuY71v!kIqd*ZESCX#iB1ctkeTOfR(%=K&ZZ%DbXjLK=Tt*ng74QFZ>+yaPH zD{7w`rU_1OY4s}m)foFQE6SrOZ;@Nqy)1DLKI|EQ=`<8Z7n~88s$#9xm%=vHp#4tt zlv<(vrxX;dW^k6TJx&aq`Bznn24+<*UzIvF<6R%XKY7_uxdVX|r2E2^}0 z7UaF39u`d4_m>ixSNp1%)W68c0Wt?k&vCGGPc)sb%M_-GG`l*@a-vdBG4&>!n9E7< zmJBHS%H{G>TFrCWTkRcArNbS*A7-&VnF-QZ4s+7>!DMwiYc^AEJ$HJ-MEI#=)hI*i zE@JsbpK4tem(2-`GcUe#u#VxNF!88Yg0Iq9eFraSQwPRrpC_$6n259DCIiUk-H4&#ZF4SP7eQ2ifuMl?zbasLF-tF{`~f^|gl z0aHD@LCO_ovq}jGlM*}Tz@lCM_YkCNw{_GmU;hQL5zScmzoW8~#9;_u1q-{r}F z?H@SbRs1K@8oLH6wwu`b#pC8&xP&$ow3Kxc(@mrGZ>my{+0Vm49OQ*)sC(^;s5;$+ z7_2=}yRSr?n-pv5={Q8HvS#QZd6BFM($pdYZJU5$z;|#WA%h`!$Mxv|6tEw(YK?=H5WwkO7WikAZRSqPZ~;I128S zl#~?wQk;q^*FMkLq1&$av?#-1x1{~JjsK%Ekkii5tS>5B$v$=lzsC_-sJYF^$Bs;ylZ3uu zGrgYFZjuPtDpIHy@z^H*2?{aJb`ifPcxN~Itq#>rb|%g2AR(VyYB4jJm8rL6Q`6T+ zwH%&FazU%|PmwD{^d)+%j+mvPK3dd05`t}Z$gXiwZaYAkgdDAeDD?Vv7_tBE6O6Sp zJv1HP$>zvfW*@bb0oC-WT>DCRVvcwB0KE=3W5PG)%7j5UDL6`z4Q=RUzKN<8CR$0! zhPB=raHm`b`jh^&vyQuq9VH!wBL$&3KSqs;pbr~^w!9~l>tb!Rz!nk>4c=EP-7>^IC|?uh0v2(5zd)t1;6C87;*gK`-!DUgSmFi~D%JyWbu z`s3(;`6rvII1~gEarYpJlAOMLc&=Y#0eDCH`gi=lAA+my&xpCIbE#r-OoO~WQ;WcB z?iivhi-W%UD$F%-a+h{L-OsF#+0zC8vDWBP> z3%H2Yq+xLI5uGur3N`d>0gzhxsQ#S>AHff{W@k3;6Rc=Vw<2BS$Dz3|v2KncHE{gh z)t|D1uJ}9f70Gf%US9M--VmnzCMhXo}rMT%P4qN z>s;lqLUtQo_TqFYhE3OjZKssgV^l!WFQkWEEg6tQnB5)2Z64=Xw~pVsD9M;>BB;84 zm?#xET;m;^9W#AbS1pWgGdX&^wD`eV=(PV557j3kO^S!rP3gD?|LX1(&F&)Cv^!sg z$@hx4o_{nvR=O%^y{faM>(sCKF3mh;vvD}3c!{7z8b}B}4X(P~yXdQDkGRr%K$iDad@|Kbg}k=_dc`TM4PtNB02XriaV;1H%X8%HH*>B5+Q& zsq9XIqFmFfU`9AOMv?Hy)HZswX7rFrI8wSuO~0T?|7QEHMEydyxAgHXx_bJK6;GJma9Mo-bnx+b8k|{Y4}hW*+t{n74CoH z2C`2)v8>IoXS9s(hAZA+3c1E&)*d4=IXd1Rr)QheD3IacaI@+5|9q(d!-_&6*)0K-g+~$ zOz}N_-CjYve1zvV8n0U|W}Cb>hpqeegYssMcJRw^g-bs@UDh?tc`j{D&TTi;S%Kq2DkP!*hQE7`9xq9pM4I9sVkjF9mG?>N1fDT z-p0BEVK?X4;L6KZjIp|10c+FR*;idxyxUlJB0fvysdV#)M%E%NGwg5cn zUQHWw3p4|T#gHlz+g)_-yO>)TtTh==5h@MOy3E>9`Bf)F?@4=<+;~Yu@!Dh^() z=qraErLh-%4dPsEQBB4C34C5GE%}9x``QfRiJE!3XMXuH*rcOD0fDy__&oll1fME{ z@J&#a*KxA69nU5UkVRW}?Zd&@`3;~kcZqrL2BskdZeqzY`!e;g*E1%bm+Fzq6BZPj z;>K4g&;*RBjcy(V>O*V4)F=2Z1N(|DJbjMXf&I7WPioArD z-C|or7ft)~d{vAD6LU(|7({*;Gh@cD+;E6Q<%Z@s)*e=UD9E^8^?34x#8&1Qak6a~aOyi&ivzREA~Xkuiku3XUG;O{s((ox0o&lH1nOqr+O*w^dDQqu zfApvkMi{#pq?VXt9h5jtjqR;!nqkc$nDr~U{#PF-$WZ*kHT#52%C32T*gcnXr4+21 zh#IlVP+rXr9Ia$A$F3O$4|Nn>%0ZBN8}n;#Q^Z>!JtUqC-}1U(Z4N1NO*70yaco&T8Vb&mm$s*iu#Cif9qCjOM(7>4!e@I3&)*B7->TeQQ}7`0O@mOc8d#=0ktd97+p znqqtaDA@ke!@Hk`7+muuCh%FbI^9UpL|r5?J1ie2`|sF&bhSQkeFvH*k}D$2}xI1p2v&H*q;M2c(~5zFzZwL22X6 z?0_K(JuC7A7VARqH5*1fs1-4pN^exdY9y~TTSK#S7QFkZXr9Jcn%$uCu1zBI=|Hzm z8~pRIX2{gRY`eDCbe3F!N^V_jR!3lOvQtOGmbeV_O)g;fy_GU6$&()`?vhnXidGY*_psti5McQ(L$;ihElr zBHhrG4uPPw2neW<00~V(LJ38I&_eGB*r}mONPtiU5+I=oBy>fQ-b?6BDGCTE)$YF8 zUpwdAaldbjJMLY7vNFaRS#!<2bFQ~M@AF7IjqzOgxH9T3XLfF0ItPaKm(=r(ymsOD z|1fP`;UR2>zg0oS)D~w)tk`w)SNjbJ{BHKwm@_F^rv|KBX5QL(a_h1q`yq4Qm7Rf@ z@5TD5FwOVaC(;yoU2b!y`$naW3NS^WL5MpqZS| z<=%s*!Smr&sL0Z+%g_9;3uH3KgcBI*_{=J+kzsAsHoxy*PYE`xkKFdh1udsGRcXRf z_e?D2Q#>c2fP+idu68*WaosTSqT_GJ+)30orb}mzt4k_7&%XYHCw*=;>f$+omp5J6 zkL!;0cumOk;m0b&wSeVBbXTx-lZSP&1jcLHtr11*FPf;Z#8^CSfrKqc*}L1PA$G-A zH^iE1A`mgMhmA{c4SuJt2_4KV5Qzoc3~Pt z=!US?kmEfK^GMe`Hnvtt)@=U$h9snrCS~(WGCRe-^OqYj4s0%ONFO-2(^Kss=CGd{ zOlvi^59FB@9REfyNL-%f33&v5NEKgES@Uk>s-2k|JkRq%Pv?l?>CV33_tYt+2hdZt z5NM1qmbBbRL8d%eQ!{aY7z1Vn6P!}QS4smuaw#?z1 z6SSCWYHr>-A@izh$RyVaRHe;-lk>~}+yGAiRvWbEXT-c&W!cB7RW?EVROA*COlXu2 zJS(H%ZON0NL}os*r!C~yt*c4rUFddK$y{m``m4>Pqg_o_42HBB;@4%?IGN#DP?-!u6IckAPo=GRfJ$B<0Bv_+wj@J62zxj1S^apmSr`#J9NEdJJIzK>(wEvlBA@&!NP-=3+= zkFf9wdZm3cH4_87i*w`AY$s~Yr~TKv{{ISjVCP7+3+P->?DVKrJmbo|RMsxhSE-zr zVZ}VE9GsfNqqkCf4|O@|x}2_OJlwrij<%w&(`4*=$B@PfYOZ@cF6kR^yIn~QPxJxl zv*d<|#EE;2qU8x|P~>}XbalVlsLV{d+xu5K;I#(DjBd!#-W{AxtcsQJ+azI9+?1|( zFS&eLoG6-ejgx-OK}|#ReU=KTO>3yIkUh&s|`+_c}RUs>C zqa!k`w+ABzY_$2Is8v)j%Es=V|9`3w*_7mF{ogA@=l`t`z5KUAWZ$_zXW7C^TL?D( zxLqu=Z%D?-KHES>wtRU+b5Z-)#m%W3mjD=9uM~B%>fVFt)(zil*6K0s?FS z72h#FTn9|?*iHCwf}hU`$mNEiv{fkU*FJ_Z8A`n^AsSva0ao6k8SGom<|}#+#tDh6 zL@R(tP|>9s)h1t{r?}m9g9wEF2Z+Ms!{=f*jEmt|NH&c^Z|@`L)?uZ~5#!LPt)KK|zz`%M;VPK0&I$7r9k z<$oUC`S=xI_VpAsxZd0_mTd-jA*VRhD!>smpjjeX|0QZve)~YGxpXLm&f%`rx=rUND z_7(?9;);6d5X5L0Qe}AC+v=u+$Wm1$=yObacJC$qCu#5GPPq%;l~uOSZiRN@cHq8E zES3*Ji1p}vT!Zx#PL=gWHwXNA9Kpsv9oxvX4+vi&9uRQE*Rh*ri4Sx4^nzU7>_`Jg z9BL6}*Uwc07g|ge#!KMH@^bhBoD`4=Ely4{n79RSTyT{v!h@NjeDOjYv z(0yUf9xggBB?T)etA2O7KH`5HK97Hhe`>?6W`Ijwse1d+y8PhIQ*riHXzF~*MjwDT zfr*Lg{eJz)gSUD7#m>X0tOCTj5XcrsZQS|=N)z5>z}E|be8~}b$^9-D-h9!7?;ID`|P7$jXQuqri# zxZ9^hK6rFyWS8#;b(@u7xXAzcYm%$4j;a$m;$Z>*=Vy5BpZ-ty*sCgGlog1BMU!JX zyjsPoHk8|y5o%d~+7$s0vB$eND^gVUpZGt_W zDf3xL68h~|_q$fNg%E)#Zph618JP=LlL2J*A6q*zpS^sWCsTH9EWN37)e z!1?~I&1-$fNf@-!*n0KYkhzc|r{9m5oGAHn6+{wPmABFL)zlakQq@BfT;Xo2z33+Q z^v`&|#%JrcDji4_s=wYu+b0__W8iyf{`QK)w&_plZTX}o1{<)9)7opuucLo4-*Ova zhTQD}S6gk{2r3i?2h)^4IgTxSv0kfNE`Kl9lk~08@0yBc?1>*@veGf1p)%;AuL;eF zGHbMqeKLND(u|SjD0y;*>vP2x<)KqkPbdm2K+7Nk6KU6BR$GBX3^I`(CDS%;-^x3o zzxe!>x;6+nUvjIl{I>8@vr;2JY2zWm1h>p_m_D!LNeG1P`}@x8Haz=cInz?(m?~No z31z0A2J%vcd;>D)Ee|Q(1W!95tty?j53*&F!n(EJJ2vB{k~(Sk-<;#sthW>{dHK=N zIr0IYAMdmK%!IUS_XOUWE*U>Z`Qhku0xnOoE<*J=VD@RCVT zbB5;_PDS$pLdYiXt6l-Q3(aa6iMWiNDtE}8B^CAuH}3jcW4)N$ck3}Qbi?g&#lYmL z`{%ZQN*yzVpEp%kx(?~dzUYh{teM8vfcSrSE6%zAua%FEBcr&$gmhB*U?UH!kV;M7 z-RRYGt-^5-KiBr(M=62*UhUs%V8 z^VpF?4Hz>8;$m$*9;>EQ4uPUW>)b5)N=8*c z(=r&Pxbo9GR;B8kWOLyBRHqDb7k#9i!F4?qvT5vGcg(DpyvVpA_QkM5>A6+lgMkbg z_>6&>jSJO8FDCs;-EZL(nC7v%yZ?o-vs=^md#-tT`VeP~yd8h=kic&t?sq0(Scea4 zxrI^yJfl3-rmzlOuHM1bLn<@J8f?IVepkuS>vyF*JI!|o4J+2)0U4TsKOXiK{wCTU zN2)qa(dW-!&p7?9JbTX#gwo9FqBeAb&PynRixEhOm*u|Gs$k%I7PEUQNGTF5JmQ)SixBbp64pML1NetcLd=jvwJMPWhE>T5(^LxUE6*3 zGS-_sSW$A*rDEPfDF1%pAg6njYng4snh2Pg?JcuXLX^6jxWcf;r6M8dF$E|7VyZ`5 z{nh0LF2K&7s_(TI6?+4<8z0%C6UKkS5||78b$Z>ME>w%u5yzVdD!@>MFv|C#n_o znTK#BOKgtioh+t=uk!)F9mUyx^!YZ&B^?Ipota499ZP)Kkd721a~mB%%Gu+lY@c3!QP$^1MY>IZ@liZl#4|5e=+ zoR;tHjzv%;Aawb0ase3=3Ang;+96Owv-Qbw*Vd=CwU6%!*wo+aDus+TcZYd6tTL$x z1FCGj6C%Fh=Lavg(Yp2!tZiQiV)x^8OX$%;GkoRgw1DbJ#rl8%LE=NI}){5xS)$L zz$F#ZdDCwzqV^Bjl!2!t-^DGW$-mH1d%5`3a)a|qg!jT{j}r(_W_6cB?~m!LPCq8- z1U7FW=X3sHi^WKwAcx1R9r1`>rTL{)L=l)!?R4Yg zi}@_sH9PCrbm7HDr{fNnF&@Scq~|0GU6IbiS2@?s=Q3230*Mr4EY z3=$^1Wb;iMvG%=#ttDzaV7q|QjHeOUu+Z}dDIxm%a7aVhix_fpR{ab>#v6q2TEaYh z9v2?GJO#qXDDSszAf;Q-4WYm!6_&i6T(YJ2kCSa@)4qVBBPA!qfjJ+T#H>a67fNkq zZ~GmRg%qrwP$B4F7d}67J?_3dV^gcNJwNx|?8PS=Ua0Hfeqee#Q30n8y8C?mh_EA1HzswNsDFu*6JJNE}k$w8%R+b zDCanBCK)gGR=sG8@S#M)(18pOgc$0%H4v-MysQ#)dbTg55G*5DF&OXn_jDQaGHP>= zMsH3wjnyC+r%1$~^KBa+|F?T!flLAb0Dh_}+uX*CE_NVpviPIQciY&zZ}8g+(_QW2 z1{~l;x4B$-dvB@HadHkrN@`t7)T^^n3!SLYzEiKZ06|%6-1u!^K}tIO)9F8)Q#kAZ zc!RN5`xKM7?L1)tUd61y1QM>pVVPu%;a+fXP_i3nHvZ*c>={Lw2w=NG#C4ZeakMpO z=kL;IZHksw3c_>maOK1xYodX#id$E{81H)+*5JHgc_;-xTD&O*>ehrdLI)vTBXUCT?U*%I|(TZ6HGSUNqDQ(V9mnQhX4a3)tqVqrC?)@BW^suvXx|cpjz5unpNT z&8&uH{;=~pwTMnmXosc2%d@G_s$iCG7>&Bi_I9JGCnj9xmU>bYe-$uqy~2&B*t$e# zEA9C+BB^jgBAYuU%87_&QpM5g^>wbb=vPeSjy7QWD9JQ)o1M6MD^VfVr)A(s);{pe zC^;vg_jm0Qc-0@Ag+{X$lmZa81(#Y=)e9019_pdN|6%)F*9J*xv2%Lrsk@!D596s5 z#0?uJp0hWNEz?t6aG$RGXwVuKO*oLOm~w80;|P6w=r->=isifU@2Anulvh6!eH+HT z(NmE!3ZM_}tdh#{4Q^FtoQ>G=_^KaylrGO;{YM5;$|)y{p2#RBjzKyVPe*4K-FgN3 z&U?uYJOM(^q8IE6VhCg+l~gEkq4#u#Kf9U9slZZ-X)HZ4EVP4SfJi>Fy!vNCak_<2 zbcgJRkF|{r7*jaF`>78wE zN#|98#WVz=xQzWHw7Pz(Vo8wLE_3zMG*mKI!cz{x?5k2`TpsKtzz_+83@K1loG#7G zFU1KLT#kxBU0kd8SxS`PzDz)~U~#f;)%%N!!q0|dkB+0V2kNY@1g6V>E=?49Kqhp9 z;?;LW7Fh5Lx)S$kuHUt?dmZ1e_yuy=IF~rFD85Dx(|i7`3XX#4DO!x609r99qO~vA z##Lyx(F-)fPWzRqHP|QZ|Bw-bazOXV5(2RG@4eJFgP!iN+Rt_N`4d* z)w%g%i#O-#U&qg3_#b^-szWehRo5j68R;t1pD|KOADjxE0Re69>bspeWaQWkC`3b) z$Y&yXoMRRLr?>dmCBZARVa^g$g&uyOioc+i?rt-=rH}#W&d&7)rw_`qRdP3T|6zElI)&S*d&@O7YuAd!ZkmB&r8MQY^i@Gd-Rp}e(wY7(KSwoBEunI8F;UoZSOQpzQ z>40PSvl4ODTX7%16ayzn=R|;miJrr^vXDscr71;fu0n6#WRHsaxW9;DyZrmx)ihm0 z{o8&bDst-kebN}iDnj{)nQ7x!fz5HjV-0|?z!O8CVu ziMaNepWi5+jaKt3KT=Rloqv{Ee71dee8bh5w}<&kq`IPRvsgwNO&*?H&=g%(&iJ?uu20x3-YmgimQliV- z!E+95G`P~k!CLh@-dvf?!_SYriI&(zJRyjo;}+ym)HG){>0^`WJG!gw2*sdtTugme zB{#=pUFpJ3ojRwwx4E~_i#r}n`4o}P?4yhOBKOFx{dl=-bRvf?XK!-vh{43OuJK;5 zM|HGOSQwv+jOAQDBA>6qD^zZhey}%ArgZ09enwBwiM(kjfG|UCR$|M1?#me7Nk_vq z>)L*TWm^A9Tp@{f`e@v23^uR|s|hDXQ)L4~N6jW6pOWfwaW zhrlZulB$zHO90l;Xa~UU=N1-k!Ls8byQjk0W|Y;O!x)KFr2&M^MUVPAj9byC=ehm< zfQnxyk!36=^G)D~Ro!=DsOHeQugRxSzKW?N`j<*fCPK64A7*sRGk%}QWcr~3bfxLx&@X4Z23Crvt%F~kNSnX- z)Y+-CzTO|p6`lmLM$RRvnw|Px>}yoE31Is%ErqW_7f8NIJh9bVWlakJT1LL+*8ewv z4PhZ81>CuS9CyZHxiXyUX};d7l7IH5&@(CV(;bW0fmc;O_GdtjP*w6E5scoXB{Bf% zQLZa5%eRRR7G{~r-@E~pC>IYjh%Z0*QfV#wn+J%?wbAywu)q#4&f?O(DigI)JWPfq z=Fn>HXn0e-ChkwUiT%Ma_sFwrD%MR^KhE%n&aLa^_v{2Z+9+`D<#<&OkNx)`N?&!5 zo_W6}I5z`NANCMhT5s6w&U*S6u*nFNX z1Wy{8*hs_gfxu68sfLhSNQwpNe1C7L7PTYlri!+#va{L1Z8O=$&Upk?13P2tiOL>n zp@i^Ws8_o&s%n?vJW<@NT?ERmC4tJs?{(;3aFi`sfaq!L1fePrWIN9<;>ox!Y|UTA z#YcJXG*AV(!^pf&RDMHsqvNB1%Xop)fz+CBMZZiIZe=B#_eUR{|%Q(h;| ztjzu~ARvL}4P(hhD@~!XWU4F?WZ#>d=5H|S0dViAxN@cG_K~vKk$ELUGUy|+;^cvE z_cbN~Yz*50$S6oZPe?(NNCi7p=gv!iSPZ^namrNf;kFsfadG=iic*c;dZ;KNRIM6N z=LJV3q3)!}z7T_gKhrBzaa@ucWOID62A@hPeQPj@aEvHXnNL(lhRHi~$9*%wA0qGg zvE8!64RxhtVN<|QZA+Nm8n_HG+w64y7(UD6l#?q{C5v=&WkN!lBhkBb$1S|(lc3tL zQF`QUbSOnwFqTPTp@(%jlUdW0e!#w*C;Gx(b2ebCdd=m&@Kj~bliSEenCA+m+aw9i zU(lOOAZot+!3qe1yk7M`kamBKA{Mv4KT?gN%D6)r&UH&h;J=DL7`f2j_Q5A|XJ4CV zUr7d+YR%Nv3_!>XK#vXJ+c*LiskSlU#j5feHNz|c0G3a=*lRZ$AG8`5=EUaAE`R%q zr+_Ujdu=P$HPyFi7#8eYKdV~;H%{bwp1^c>ca(cRof>#WIO%(O+hXG`RoRMpXfAX6 zqx`Ju7Fc6eUC|@U$^1T)K1BY0&KjCL*X)T)cr}uE&1Io4YSwSCcQ)Nx^t78+bubE> zum*vnD((n3twOqhzOPUE$JneF>;h&j#oIOp@>&Em7<5{a;bu$Vyc8_G!m+AqE@(?N z%l{`=mf*lN<3yu~CB+j3!>f~m(le)4c};QC8e$F~xK%s`ZzgYOu{Q&$qzFOc^)KSQ zir6u8OUej?p1hn1!gp3gXMIetj$;#DCs9yIC2o&?S4FPpXyI z3Kp6*TDriJ)~b?411<&hc2{Uh-${>~&FnEf89K@9EHk+W*R}A=)eSWL-F}FlKIE$8 zLb)a5mft__!ujaW`jxEhH9>o$U1p!v{B|qAwtSe(i=9y;Kg|_~i?lzheUMbUd;{Rn?+Xa6VL|@|WHJf%vJ^6Me{@a=O!|iKtBULXWp39-+53f;lF!ugB?Qa7O?MLQV zkgA0G=l`{sd*WShTJ}A*lV`Zk`qMxUP3^k8MnXaIbN1tl1zwgCo$?LF8TDlf zpUQvPW|Fs!uvMxh?37!U*I=5Tl>t3gRxTxV>fpN3aiZS_-|l#%G5DE7oIb%A#uM$+ zZTM+O^;k=kIZ!OAb+uTkE{k)(#-;63>{ZjC!55q0P@uFRg9HQ?27GvhUqv?NT@%wa z$?!7jb$o+QJBf*Y+I!p$770QV^iS(VWT*%#p(=az&d> zUD)~{p(iiA8+~UJnZ^;19b3Jxpz`x=a2d#un&G-xAxQ*UXoIh7I(lBvA*8nno6@s- zqr`tiv)xOkt(&v0@BJJSlTj|8lvg7D6peI2xH+ze&tnbFYRr%9 z>;#$u_De9&@_%*Smjw~Q*Y_0miu>_FPHqJ7YCnjpi-$)zxwWj#@D#~ZM{TtKx#Lo) zO7@&)x~J2J(hUJ*%RYyfsZz>$o{;VAzvXzkRh$7XpJ#q6Q&6&=gxPg)5%=A}4|o&E z^TD)0f17V1=LEdryxy$b1dR2TFXG`eEJ+dsX&f1a)n!>{jD8~s_omHXK7Hq*3N{Ba3MSPkZ4| zsicE7qv{;MFNkiTa_*T~p)iv&Y@Q8(fS=DWPbkWMGOC*2lM?v}}uTuUxIQf-FiLacKl_lx?M~+(FrLN=~ zMJ{4h8dT)mn(R8gVUo&I-YR|R7KOEmf0mMbOhj=-mUzK@? zkvLzTa*{y-9&}#5hD}qRt9*I75N^eUOm2m;ztlLJF0rYacg;RUC#Ln-K}ZlmSa`JbIGm>ta$ZP2Xoiw89D9mTLiF)1X?X{%Sv1MERo`wEseD`?y}%mNQ_8yj@*_8mwMom(7w-f7pc zYw|=UsL1=Kd-0k{OT+lwwZ4~#1L>t%`LE(}k)(4T=eK$5SJi+JqLx;_DSHS(FwagD zQ<*R^(w=-{m*+weBxJy$01m9F*U*!RZPZZe6Y2;m?hKNYyi3ES_4mYHy_4eUewR&_ z^WRz40pN|99z}u&;zm_gv1>$a)OA{6%A_f%K3a{{?ebK zQ0c8*EU<~U^;n5H!u&TciF#;98>Hz5dY1qBRGje%0g1GEcm*)9za<)upJIz&v|eG*08p*{W|=Kmm?w z=Nqa@(!9&avmd*a@aW7V1qIa)s=gQ(@#0a3s?y(YLhL5J-Eae!Fkxi&Cn5JpD!nPj zxA4iWpf&WR_u*_pFVYaq`1D724KK^_U9F1`ZmHAi?m;m5ig=2x9G~W-2Vvdl(BCL& zk9TZrrT6|}8wuqAA%%UbT&7(H+dYs{+G~al;#->Xfx!C~JiD+eXqkLpGkUtFHM%T z+jfO4#WT#D=Fn2sW4Ys^Fo60vVxG2y!LssH%&CIhH zi6G%D9$v{@Uv-M~T4QG~B6CiVNFD-8TJN)khgJJtk(Dz`37ZSS5%`VF^f{YUf2EN0 z05b4<8_Z4Y>XNUtJc7_i-;PilRF63w)K46-M)Zp* zE8>@C%*RvC!bs^tTC8$LNL=uOrXTJwu9HR^<4Zvv(?YS1ETK_>9D=RNZ45lpq|fW5 zM<0!VfoNhD)GyvQnhY&h`hGbQvMMd1dpoMVg!xksu{KSJQ3!H6bU;+uh0G!qRB#%D zdi)Sw-Qtm`g8DQ%)GP$1P#bBWf~HL#L1ZJJ|>Po16%|oM^P`8p~@Ovuu6mMg@^QO zV)-TM-C8vTQWN^S3DrQ;9YJ0KiywG9w=*H3{Xl>_JvZZg>6_LD2;ku)19%mGWy(V9>2 zb((4Mm?8*HIyqS2t6=F>v=IqN=S@fpBmq8O;C!N5{#FXay*@io440bx$=m>CGP=j-{bDK*;d_;`NH;> z!}Rn2KB;H;NlFH(+szgdrMCKcfn5bG5HUlI%lbms2H z_y@UFjHpSM;;Kf#eCzowvg0$+S^>dyiGXy-69)J_XoZzEqGyg}wq}}46C6cbmYK`(VA`j=-H22f$69x{Z@CIUGl?%}i9;73*>2O0Ac725Rdpdl zYQm!9^2z$YFIE1CMf5#d@S?i>PHx=H;3w=!uW+DL>!%S;tum#*wqj@)4 zzARB;LBz24FJTqkk45w4_!rzNyr!Pj9dtrUbQfso$WsGv=U>TuceC8xh~x!Y-rmRG zdGt~p{ZK!l_eA>t!JOGAI}EmDx|F|mem@MXP9{XczSzuV!bbA-oMw!P>W`h~t9 z54!H7gO^XmWS2yc$gQ2w>k3jc-iQV4BwS5(u`Mav5^1BW@EQ{w=5swOtW3IewRX>4 zpuE#%`lQ3BuGsq|d{h3?-M#!@7aKN@G}S|pZcIvE*yw6pjD!M=r7*)1lwOlm{v6=e zT}YvvQRsGIH^A zo~A;BRHn1t8v}uY+O>C!=e*65$sS$7s7VK_?haBrgMEV4a7kh{T<+18t9l?wLGwq7 zkzOPftAuSM&SseM<#Bh%aV8=+>p|e$#KS*_ulI6<^3Zl0skXhZ>hO(!q>2pcR#ZHx zCvbV2`tf61nz@;Z!y%zoysJT0U7F?+M&kBfy0=&O9oDHo?F|hLG94;mgI#+U@*a25 z$M|3?^Qvj*GIWvkYU^qf^JwY6{X_c=t0Uh|8ptC}11 za;C7lu(YIVnng0xnz25k50m+GQg`&kGX@V8q7l5bk!tf2TY-DTcFE%Me=@-T-RX(P zu3b;n_^(E8zt|kmcb}vVK+NjL%mMHH4H|@k{ z$E%&`cTMjbPC|ZCuZX|4evqYubYDw?$us)-s=Knx%j?ML6qvf^MB+m6LR4~E;6~K< z&u))9tYSjf7{b3BE5L0;9=HE4gpZ-_eOI9d_Oy;MMoR-^l5xtBV zgAM8JLKR=>-oXKskbKwszB$1lU|+NN;NhS9{7VyAxQ%7aWVf{^-@OPB(@IMoA^sjw z6`EUJLQ+9?))_9Kt(_COzxiq@^ed}(Pc2b_D6W@F) zGS8R}k_z~dQwVW{Dop>u8+`ILd|0dsxkOa5Y6@r+0a4XS)y1dWON5Ci99&Ns5}qdw zpz3Vhs_F@(Otj1M$CeSfNKO^sP1 zsBiLWoEOD5mq`(UlHUwFT_;oT0Lv$#(Bono{^2~k#N4N0ou#+GIJteXd={#hS-Nli zCqLcr?l=0inp;;=Y<@bv@r9bcTYBY19OTb(lhtRZ_#&6CU7W7l3Li=K9n$8C#y*_c zO>&p>sZ618OKE6-iyX3g*wFpm13VN|H$^;p*!VsJE}S!x5^q`?%!AU_!5F0kbh?C1=RSV?!^@8}HM>rGK0}RP8Sc@CU;IYC^wrScL>S#S-~9aR3eVWt zFHS$My}CXrv`&!SeOHMZeNlRnl_%S4OD`Z4tY&_{Uj_XUI|^o@c#)VObn3g+oZP33#jF<|HT~=q4j}Ri)V(;ZUzF~q>kqf}AJhVdY`bZQ?8Gg825yCA08dyH-r*A?Y(t*=Ad<`#a z&35$&r^I~_hF6TE{8~-+MOOr5o=D=%_ApOW&B+)%h_f+0{gQmgRU016dp+R1?W2yY zT3A7BA6&~Jx$5>0juu}9jN^pF-|Rsr+nDk%fC&i+TU#h_=g|VADvAAp{-kyD=iovP z8I;%WK|gsyF-428Sbkm}Ut*3MobfN(*cUOW(tJ9p$jYLYr{yIlCkG|}T62R!i*q_g zN;y}MqV}yScvNGp$H;?1ab2Sdse<`fuFT}7;yc2Qx!xeNllF`?Rh5(P!_KXQsly97 zxIl%n;*^GXx3UV?o|W->RoK=+f)sN+I%l6NDF7FlhY8=}?yf!i#W7N$y>t;+rV69<8|oMSK5v!sfv@3zG%8xfEB`^?hfCJl(ocqP%*KRsrqCB|81BN?8nF zGFA0i5Xr$Uf{*g8iRZHfdS>yNnR`^s_gpxd-&h}6rJ*`EE49u)-t#ukWstF{v!U`~ zAv87%g~ak2q8&b@qTYA@X7Or--MRa6tG1jjgLm>})x2Q$Z_$BF<%xOYi!A*+$V#B*P!Ko`>QXrd6~EGig(C8ZM&sxF-+f2xrK zR90JzRkuG!X1L5wl}^UdDueDT9x`p0V5N7iC$&yIo10BAhd?2e=n!nNbp_POns^7m z?q$)G<>?abU0Y7Yx5(TR;D15!$tWFs67Y;gBUL?|vpFn-e?2(u9#ncPZ^pn1@K2_t zU_%W}ECJxONZt-Hv;|I5ujg)xcJM*M)EK#=X4O$J)(Vp!k7#j)Je%DIHOf$cQpdf6 zetiw|Zgl#Dm;MDj9DyIjLwg|(ol4tDTA-7Wkf0LFU^#Y@a<5Il%vf~&+Pm7Zr7qL5 z;%XB_wxne7N>jB0NzsYZmAza*(S~FDr&HayeYxcZ|2N*zSp0ZaPHK!&BDATOkg?=Y zqQC~I7f!}%Xr8~52AJS+1bknhRqUa2YzOa@9ZNk3Vg3nvH@kif+#K-OAP!L*lmK^E z^Fs5opokc}6o^1(sk7gl5_kE1qgLdgwCo`3mciV|kTLYjhPPv{Z$|RUt%|82NIJdqRW$2e<5otUWC2dr+ zP0>ylUuo667EzIF$z937}hOgyQi$n6Yne6hJLLNx9q6b}N)>?7-5{)Fwk81Avmc=cJ?UF<3YvMve$= zCG_~Fez)uVRbQGB!mPOZ;TBPvlww8EUJ($t0kqa6gg!F0hCqd-}omiC?F^D;&wFO2%#6{p;<{teb_ zO@&dItEE&(&Zja6;q4YwHlM#aPl`Dg>|s`-ssVBe_e@A2n{&5`cP7hb+@GwKohA7j zWr?%fZVeGKT2KGTvtHHhlQJ)TICWuI`{T60m5Kt?nX_1n`G);VnqQ`Q@LC67w=-~d z=~NM;m-2>j=yU)nv1u>+leVyhV)_Cs-?L5QI6UW7e|_omWrgWQcl{7)$lhWSAd`7V z!2T`Kfsb91;hErUyPm;m^*YH;h=DCJtQhA#@G*|e`4m@PVYiWicF>RFvrd!|Y#n4s zGGmM0?epQLuYSog^VecK z#=K%BxGARs`Hg4_TxiZWj%<43?OTQ_JB&-wuqYTc(E*6(Uz7WnDkE75BPlHcO*}!H zYSq5d+3)W_Hi3UMeZ@>VuVZUs{Yv;Q4!5A`Nk>74kjU5ZYJpq$_Mh7|JW{SR&1sx& zt0#TEm!Ng)p{l8;u9isJ8L8*2R1Li>(PvEHCd_VdWCcv0_tQn#{KIyixFYSd`{_-^ z710-&v-B&aAF2xh*{GBJFj4q7rjfcJZSN@JA%{-AM%px5OpF$WE5>Ye!BxAog{ zXkVL~hTk@bv{Hx;6X`@(ypfY(vIA0lzxEH?WlWP-hoX6>&&|ed@RtJ9Q#Lft*9!1T zN(W}aQqJ2W5mcqI#*DN25W3zol_k_s_Dia{J_z*aSY{g5R<7brEgz#af)!dVHJ<0) z+R7(ye+HR6b25naDVYH6Py&Z?EmO@#wtY4u5nYW{`24_ zyyWKXu5Ttv4`sjLlA>M=PaF>+b3@VhX2z{vluR$)e)AyRr1F(kA9USaDM z6XYK0PtCn__cp{=BBQ0WRKS3Q{H#4=#?)(dh_h1!x-y)3At$!r3QY z@h!R&ydMAQMKF(5g}$$8XA1z^EM~k|LYw6FztXX4kN$^kUc$XDm6#E$Q(%pHAS#yc z4xfJ@&>Hb^w~eI`bIutfCy0<-pO`nA*ykHL_j|MRwo;p}^WuR_yDk2Sp1zl;75XdZ zV)&84tXe}gI1?Go#`FhPNE6Juf#)q0~uY`P57Yab>46GVtuSSpW#)1rQ z?`-*;O3#czoU2wfj~%sW*$VwAoxbm>ybGME`!eyg-z*TU(lQ&IokKJRCT-f zw};s8zm7S5>ve@gn@WV~laL?TFDdR-NuibV43(8vSVhZUAvWvRQMR!{?X}zj2L_2L zKufaQV z%ePm)!g2#2e)q_{tcbU5w-j1k3JDTABk5(7U4@liM#?@_f9LB}bBWGcpa**n{&L{9 zxs=BrwA3kg1to|cNn8H4w8zJh58-X|_S8r}Ynanl#H&@&d&{0* z;S0nUKHtNn*;NGO86}wnUjZKZf-c%e#VcoqF}v4anO0tG2aX^y1_{CP8JNncJ^EA0 zAXR46be$m_R*I?xklNxtumBg zes<*qLSB%O>w}~T?vTt9VM$_T_(8!uF9OB9s)vg!cVKj~h*XZp;1}Drkq#2C0&FBS zbFnL@v=UGX8ds!6yr}ZgO+*^+*&$;!P*|?h&b?Sde-&^3tpUbkK@xzo7|N?Y4U;LI zA1O--G#YoOWN#f5Zn?DUe&`t{@s3t@zsU@lSfCxb9JnkO)p*o&dc$WQJDc3ZXt|r# z5up7jAJZ*HSuz9F(e%+_9rSMc=qB?9*zH%F^OY}2t~-%a*xaY!6xfC`s`#D+`dP=2 zGEkL3z9ver?`8FW)tG&N;MXkEJ4OEkeyMGK0gt!Kq~-qD76YHCtvS-fPg@)}#Q12A zJaw(sAFnRT8M4D47xbNLK&Ki9SZ*ZcP@iC{tmfi4V(c8cO?}C*CZ(uQ3Sw1hcB>C> zCD!OD$sAppIwJWod+Aav*r01Rbv@sx0`hj-%5;im9UWY1CabI~eafS-#jn7>cT z;Y=RD^x~tmy%k7a5ju+!)Je(7Ft}_9vfYLOxyeda9M&^V2aMalKJ@lcf4m&0j_o3RzvEnl$F?lEz8lV{^IkLOP6O9Vr5EwEihP3W=7 z_I(I;CvD*ENcDynRMgZMxmL!mjw@kTdXQQiV&y2_lBDP4y~Q_oiCU9=R&**ORZ!ok zpD+&%MJ#^xx2w*nA9WqSe5PZQv!Tb1IGZF2k^m{?1&23nGa!HEP|Yo@YcQx)Ly$kG zgkJ925j&Dq=cRe#S(~PE{*t}kLw7x?Qic?A%`w<+=3 z?zz`Sc{iZ}Q&+X8bVQ0%k2Nl7YcOE#kp9ElS0Ma0M!u3c&icGH{0={r{xEeuX}Jy| za$ld{MrR)y?DPN`lCqrvs>bXLPo$0l3BC1iqmFxSZ;=e1Y$wD$&51_j7mwX|v z8mp)}TeH>kZ!;3vE6kx`Ik=hzImw=(h$UI{*au#^oUeAIUm=5V6|qc>{CA zZZ?rC-fEB3rGaYh#aUnB9Nw%PJ}kY*DGT#1E8teApEK65=bAU&_WQ%su|4k2n0>G2 zy-Cl(kT8*?0Wh!Dq>|JF!0#yDjq_dvkl#_A<-k(3bNE-9(#R*4 z-`-VnpPfMBFS*9qv95Q~MRHTYZdb%!U0+fUsOge|;H>zg-vFT|%{>V^;ribLO@D-^ z4j1r$-n)~{%Co0=Pu+Cnp;5{lXTPcV@edO9D~Bn8%htd6VyyyoJPKa}*wA1Pjkwrl zgzSFR`JgoEjP{NX<%RPWV|2h zQG$T(;Oq0Y{fV9nijj%&EYb*%AUVvgGNm+jqtGuSL1c|z;4^+oYI3JG+gLUQ(i_6r zy(-C@xtZdhTgl!V`)B@&J&`_~eoX>pI1_vVr2Ed-w>TG|1;g7oI2OG%>)Le|JQI*lbvL z7_*E#{IlcodP{8`Jhu+L^gat8S!Km0I5q0si$Lf?C%DWd*7~{{;0;;Ioi|ikDl67Q zhT0{5gC|z%W-IizBlBmpY1EWiS14p#f56_T$~P>dnR4#R(VL}3bJ#Y}-rrTz@SXnk z_i?GvX;f$R`~X^ij`!-tyA=DHh0U9{k}C(QzUO&=Itt|JO?r*1{y1O)ezAs>SHf(N zl=zE9kIeYJH1b}++!-cUL#Pz1f) zYH#i@G0}UX>{J5^A_D8aF!)>0Is}7(pLh!Ub9m~ry0QMO zEONImU-!9_^rwRHCcf&KS^m9IKn!P4zfV_wSGO{!x+9Q#Ezx50ewGOh>65N8R>GebcM5+;W5m%^#BW zqZHQd_u~%}n3~!*<0>n&*40pK??&Oco7L_3BOuZJz9*YGFMAF%cH*9lk?uwTvt#iU zkri9@2NGNg71sJ<0v46EUr;_-nzHjXzh^e%L#%}~KEGk(`hia7{v@>c%`V^DdFe^A zMf`K_Z8mDMb!bnMZz`%!O1zW;IR?$MhfSV8_x|PP_r6bRf0(3!dtQGzGzr(V!k%$; zv@hG1H{eMy0J)Eq`DP-pql@?+@Q@gM`@+7hY<N zy*yei{N^Riq0bOY_lt9mCjHNQ&t@5#-%#9)VU}6q?ueRm={g>azry(%&t>i%sM-19 zrSfHqt{JhwkS|*9-xVt-Y%X+xO@Y0*QcD?$RU_|0$;_b#vf};sm_V^DF*iK4@gI_C z1H;&tQ1)#t|xCTq+uBg zM@c;5+n@63%0hja6LM2j-r7=T-#yHsK8h3w?)`Co?|su$!g!q%d(z`t>mldU9n<&G zWvAIU`^31vnLP$K>!$I?P!RM(dy*bff}p8e2yHHL&t}Lra@=9+fAik`qDS~#PpZn~ zSUwhG0?tib)Aen9CS?Q3WSX$G`or|)(`7*^gwQ9?{MAo0n+PDMBOB z)jT=U@a}YP`+y1K#$t13MR4VzU2l|(y_iSc_iF@VSS&NRU^i2uF#_H*?R-(to+tfA zon-}7x7um#?ekqeHXIc$(w63-ZZnx>h`qfhD)O{wgA9d_yCLJDux%%2awgU^Z*F(I z=A{J{XOWX_pX=7~G~F~%ORZLME&cjB0XN=IM+c18%JloVwUxe5LzzszUF6Y_9I7Q( zFo+F}@%LYI6;7CHF2_hfLY>f>F^4gDH#f;s72^)NmLj0yfV1xe0vt7SmS`3p|GImN zs(tB+Jx)VWe#-Xew`eS&(TpB5iw7MMOhAVg2?=Bhr#S5JUMzQJ{KFAp>ZV&B-X>ED(LTr6{P zXVbaqms6L#Xxdxcew3f#Zro{R_olZw6Sg|D@*|e` z&-1nNznYja#yoDgo%YIS9gO6%`my{Kx1INFzYK)YFxFQbH2TBz(C1gC?W6dB=d6CG z5&4bYi&yuyf?kK|RDU!wW>3^fF)S{b8h?A@5E^G7rT z=czZ=DD7LH%Pt%iTWho#da9DyV%QOyz)qU;!%!7l!i^$iSA5j1@?znv$Sef79lpu0 zRy^obbY4SSwH9Lt39i|KM#r`P(_Qze&!&ds0vD|5&X-L0xip}3>;1&Dx#E31zl0f) zlLq_Sck&|x-y+{v?hfp#Jsmjt!!%R%+ZX`XvYoI{amzFYR_6ej@%O33TS4Mdc^}Ng z5_*>Y`)HU4BvncN^|jG!x2Ew+z! zj0;dAsBbFwTDqA^n`Xcoir~p*PV)v2jvPA|H_&>nN&rAuUdC?LVVrqFl{n0v^FA+V zd)v0}atu1PM-*r<6*$AjTWmv5zPOwMsTd>azg5%E1=X$fTXNQD^&@SdO4e2w$P|Lt zQ&We_&_VMV**MKTxy>^^SU;ie*^F|m*$6Q!7W;gb_;;fo@^VOWl!HRnh)Y_=lbVJX7iNhR4hR>9uY$I~>AHPd&_qj^V$4 z31R0B$;%%Lb(l)ieJglN`4C7`j<9@~g=hS}9NmyvtIgAmYA7rW%HK^l_}tzXW>6Jg zpo&+w&O^dxl=g+`TyA{3uQF4!1{}NaeeKLFo12=xnjgPza;kdN>b~@XtpFG;?ZD*^ zu4i=(JeWlY^-nJ9Zp3`G%0E6}*kSx(n!CiYU#($$x-qxh{^Nz1&-k4ATcJXZ_&2FX zQPqqZ<$lHo_c>kFRqEKbyAb)3@bYA$YK@GCN}S)L?x*g6-~IWAFI1g9Rc9%R0$Src z1FvUpf4=d|)FLb;`TP2v%jS*S_}sGC2!T!jA~sF}BJZ4F+do`zNr^t*(1l~wgN!#S z%8$g^Da3)>JjEPfZxD7JCE~r}Hfew>v2bAt9Q_7*;!AC*OFWponR~hMKF2Dmu!ifs zRlgZocp?36a@_Y!jiwy2btNQwf3W{em5U~WZzZ>=B~y|a%O<6N#Y8TiD0i|mtUuVK zt)2&)?Z=+}Bk+Hq!!-E1xYHhy42mos%gb<5-PJqDqC&3wNu@K`q0s37zrM=Pztj0V zpy%TUvqc8zjxzkl)(=iejkCzm8@KHD`uY{&+5 zPq!T)al3i1COh0p*4I?AMw?V!a}$Mf&gs^6PnZV=1LR~0eNZR_wNjdK(JNx?>61E4J5A_Dv7;QHcO8%52HZlobKIZAJRe$k* zB6f1ws6Qrix@XyR54@oi7{BmHS=YLy^<8H2HZt*YbWa|1yqx>&L(b3fL{orl`@!~1 zb#X;JAOD2ao@W57bkeN;FLUXW#W{fHCQh`0WZ$wK{D7YN=uKp+?BDy~zwJ!3!d2Ix zg$=94Xwg(ry{H=#h8D{pez`9LQPnXHrixxWO}+Z>FZ`@p<1KyS($Fuerv4pq_h&|F z+NA{%-aEjbhycT!J~TUzBK201lP*e1qhDTr4&0~4vKHX~>Q(s6twyndQw`;1t_0MTH|W3#jk*zUIfF2!VgFX^Q+q z^m4Oa#AEp=ePxd~s_b>=85kua8uG3(mf&BS56UU$DmxB5wsnzya*q(j z*$Fd3QUSWU#7rKQP1KLqHP^Uq3d%ej<5+Dm7@M8~bGS~!cp%K(iRLjVl0N0VElmz3 z&PFRZx_$=tRN_QJ4mdW3u{VP{z8B>)8Y`63SZk@jK7RY+@CCJpK}IVkm)alX8dDIN zuPq4!IP#UQu#*(`78+I_o^2MH3KVm4RTSy3nsW9QLtB%SMP*HMM&%VV4*5kz(NXX; z?lDui#?_p-=$`NQxzt3gF4X$A>b&-|!1!%H8K`6Sn*+KT8T7*y%#5ii(-uP{6a(ff zy9w&~i5m&UG0giMj+w6DwyMk3=)&Hf{m)p5gAR%aYcN08p`c@6qj(De^IY#chMvL4-@@^ zp;56Z0iIDjA=JulcHQI4q^(wg%VbH>f__UqFZ0M}ICd>ADBL_|m1rpW>J3|Qj0$>+ z(`1L#<`wKqd zbSTg+{t+kqXjP62=521V1}O9yYd9)g2dk=SU+&Gw%Nzx$9|zT^uC?vBC<;*-?+YpE z_hHN1GtKFW5U^=>e*jVqakbc!ErG#{EV&no$@#!c#`VL2%FLiqVkUgd-yXptQQTOM z>w|vm@fbAGj{=}f_>KAuFfrM%-TL>Rc!58$x|fg}d{I#_h7RK{9??m{V8gF+nsU^f zv8r=XE?}9F<|qWbJy%RT0K+Tz;~R_!amTQNvyT?gO!U5+jttGNdF|9hbsUVhcy{x~ zYnu#HtkDB`t;nDg<2=`@BNaDA9TCD98eAYTG3|#bzNu+fb%*f^f<%i_ z;{qCbQt}8H!@`dV{yJ~b$FIduW^%Y0AKMV1ly5-kYTg>^cx!-wx^4MY;tmn63L@W> zqQ@MIy*o8>`iIFLv6_5czzQ{gBYlfP4zZoJbG2*@u3BEO<7WmD9zCB+$8Tu8XUt-g zJjBc+CyP*X2};5b)Oo$S7%2^ZfJrZ}FV5mA>G7rAGgsbAtt$!J&9m>>q21DXMZO0t zM5RoZi_eR)#rOu zXXz3vNwJYD=0+N(P6x~I{7#VtCl%ECrf{w7NFwErQVdGIl-f;5%(~g zVzLza=ilSM(zNo%%#(6XZ>FrS{+79Y^A-5Dr8kqb7=>~*$VfjT6HidLW?1M=5)8^G z(J5>W#pY6}1H(ztjRV6OT4`}(y(^QkT-A&0`h}LFG`i+~_zr;5_sMz_mc5><|&-&KN#GMibaUm6m~D;McAvy7@>4XNpc%7ruD1@fZ3?wyWTTZ^x6 zKhUk+>LnlMZ(R$RkDqAdU>L6P0lGZ&!dqPF$m0#rlWSd9Io?k2X3kYVVFYnE@Bck0 znQdyFHo)LP33d9o`O{74$-m4r6y@f7Q~+IA=G5*eUJ{>at_Oh~Zad1o^iaLVYq;C$ z(Olx2P;Xh^pRcw+<|#Ow#%3@z^X6AJCh4BbrdK}I-HwH?@{4pf^oba_B*GVa3ik!0 zesxIy*985aAAcbm48n99hzqCrnhA=dxI~EVSGr~A%c^JWH(@fsCvD=xMe+XVqA{4p zm$ZXh4)ys1&*j35Q>q>nr_VRn5>A=U*a`^#x99%#*T(NwRhtnjBHKhMX7 z6ZR#q(6yi6?@vE*U_A2I74I1{TJQcWeYx)iQ%}76e|se3=B#<u4nzmuQpwiZ(>l({n=Avt7zj zXJa?OF$&@h(T!2b`$(JWfnG4BLXyBZ(qV6fB%d&~>YGjoeO2i9p-WlG1dg`RNQK+J0Gs~SZyQ#F0Z2W4LojV2eU8Q9t zY}`FxE1qJlR9+SMUe$5R6TWk2__^7*P`yz5gca{)ll=5K7Ggi}^PP}ECvEe(C>HFE zL6$9(lHKQeuWoyo|_ORkJN89Hn$ zb@(h-=1~a-FPVVNU?7j5(OpUWhSGKTSuJ*WI6fRFqYaSJzF7rwwi?-8mqON>50Tp1 zEQdT#2bH9qiiE05x&f1ELzD({iXQWa`MXLv$8e*78z-@ng=NbVL zZHdtf#}oHrecU&6^W4`hnBTo^X-#hPCd!nBHdUtCu(_on3hgHbO@9XUeR|Ik^M~p2 zsbDjlGeL$J+$$LZ5ig7dC|F1Ec=u!7eB~&!Jq3){kdal(21JL^NW{_Og`raB% zGl+DevA>MTON)^jyCJR59=ezfaDk)0SK0h56&EO<%av-;NLlORQItYZBmwJRuG#8V z-RtLJA|t-xl>2f?HH8pi%^tdY^h&8o0?@#OAp;!|Ent3SqINI5B^xtK9D`~R79{S> zKF(3tsj005S7ha79cS!;=csqzy_)tyQbZ*kCg*lri(I;M((J1eeH-~ok(OL0n902N zRsUNcInS3WokHt7L%ENN-jO>M*$4k?nuJOTgA55F<7%2?bvFWLF_Q$i#W!{v#C6ae zPc~oncxDizMg@*Tsf>zetE9aW^9Wmqi9WT1+C2OkU?$iw5XW*Uf45#%PA)F-U21W4Dberv;Yv1EHWfOCKQxT-PJ~-YB$Fi)pS3i_rJ7go zZdmR4s!UMEtJ*EQxjte47LhQ#`mWZDtK7}ho{5Q_bk@~D_F4XP{5P7}xBk~Y2d=5# z=idzSm9sTIaHg5@8U9-?v$mQl(Zm1{9VbGac$d2LP_*sq&pmomgt}CZhT#uU40xGL_asR@eBiYzobZ!0?8?Lc{is&~^^#5VXJ9_-+($1A5Xs_YB@XARW zE-B>Hc_KVI_0U+}!IR&bH=sLaau?|9P>}{|O1lzb3a(Ho^;9F@+Y#wU4}agZygwZv zNz@toV!rxL<+4ucPy&p|^YLA2r7IGFM)lT8;41u<=V<9LRg2wb)%CDOOVMIIO3RpX z-CXWp#pB^S6}^08P7Hm+O%<2G)*5%E9gB%(BSOKEsA`5R|Kl9FRYmrTwKTIePNH-~ zS_~W8MCA9=_CfGTdo_p0jEa5}OP#L`lFY>=K}ju#=SW6VKR&W2mt3;g>NYC0FZv{w zMFIN)xVX{0yICuP2^fZ$o!kn)JOn7E-5A}|{Rlfl?=EwU`uoNEB9u=CZXAaR_fq;9 za}mbxAJzi7dV1s)6k*|Wsf6L27<7o>@`4Vz z_H!unZe~mgUD?fjOib7&^TC!o^}wx9cS+m@+gMU-$nL}^eH}(-4Z4^A8GWfb0;|G? zkNcI8m~9UeKoHHMIL%ERVSPC)kCnwQ@ZUCS=^?J5%DNh*5Z_?JKqDVGDq%$|GF=4g zqejk(i;nK83&gs)V76bvTYUd8Vee3C`NQgDE

rWzK<%6!@dZsdB=?a&j7QLcVtN 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

>tUM=&Y-yd*&?Yvl#|p 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ⅈ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 z zlmMVZ?<4#rTWbsQ1RVShM6|%28uq#D=1nPt3hGbl350}Zc^QUJs9H{S_$|w+q{n0R z9mPm?W^A7|*vw1|_5z8;lB{Z~EN9xcEeyKDZ*Vr4l=@U&0qqJF%$PV$ML0u3eIy5U z6#)2XNm6KWfv)&2+p?{3so9K#@zQ{(emu9&kxinYvP&V+{i>Vsp7upV5eDb13 z+?;6rH^J${Y6W!Yy&r(XmBq`a`kMdxE_=D9Mmd!UD7awYwgP2|-}(#pW+9~kdW-iCY+OBmK2&Lfhakyz4~DfcXEQu7$ACpK zwQs|`b0;&Slo6DlL?%tIl^%8^k&OCE?>=a6U}xUqCH;?8RjeE>li?#n6ZfAtDhVsC zE+ob$%}-m;G*e4)oql4B30STz9!OzNYgtik!%cRYUaP-x!QOxhK;=G@7lC<^d=!*U z94vCL>J^#iiKHrIq_~ln>tZE418S8V#5E&Oi)q&%l~TKe1?G0P#!#&o-;P43 zlwm&uVZzMjje+=0t90X1KQLSZh;X6ekMBwrzixP;qn}1!Vm_AYGFcf_JH3(G;{OA(Gs-!=jPsP$rmpCj{-BlX3&kUp+Pk# ze7+kzA0k^QS}1jxwMa-W=ioD_QEm2?9)&lg3OkeB*#$zCYl25$X^>-?rG-p$=%9Aw zLrC@m`bu+8N-A;r+X}6%@Eg#!y{u%dq0DcQ$7F~4qc4nquy-8I*jFQt9O@qV7 z)M8lh1a*9lY7baaEDxTil6+39x?gi=xZhC>qX$iY2~w2v`ypWXT0ygb;6YTpaR4i8 zK+39^Pu7RM)B~8`F3mpLqPAC;;PUNKH^qm&*J#(ezFH5lI!M<^38nS~bq zj9?OpF1$F_SP{T_3reiN<;EoRGrPw>wh##-Xvr*Fw0VrHN;SaRA9Hc`xIl&6tK4tZD%u!X-BydI&&oBMp_YTQxzOOE z{q`n8xndPNmO`~u+!U{EN@rMf%5y>NGG7Qk^(pB*&^7TfYRizwHTC4~rw!YSbRfz9 zpMEbirO-ro*lCQ7MS-O51n*0!Jx6Eo%9G|zzq4mKXajAu-iCWFk%PEnnjwpz<%~WM^U9H}iycq+KIyXk>57-P zp>5>q`iuDwPw>Sn)JX8s$rK~nS~ogUAY%Ax(yHs_PVs(C9D}yp+87SjkRuqO9qG=d zw11oEc;xE=Up}crDA8s&kZ>C0W7_PLD0CTM79?geI7xEEvrq9Mt!)}GsNi0f5gV6D ztm_O*J*(I!xXd-o;VeLvX6ME&;8xRt*aUB;4R9v1GIwVo*bL1baWwO}Gbrwf`~8?{ z{!#g<_5O5I_@3?ZQ4WOTAdtI46_aIhRd0wUVzw_>*Y^>QTr0`q&{5>){^nb4c47vN zEr^7nxIwf65lUT7DYz|AW+PSf+ey{dic*?)al%;ZkiJJlbg6+Pqtw9MF7psx`ALKv z%)jTu=_wnVyy7|ZTFfl90n=K>@4jH}4-1Fdt{1DBpb&a5o z|07hnA2ac91-hu2nn1oBB*>j#06;X|#AB$SS?{%7t|39bJQFK-F}0SH}wxe0?T><6i!piK*RYhgN6ldR27!m6lFk<{vLoRhRL zBfkvp2JX%F;?jvxRSw!C&UUh#Yj~dMfbmT=p3>uBj*-U`cLgW+z(Y+SFLI6N7AdQi z1=UC1mzTQKE?3NWt!&h6xZ)&{JxHSLqEgOOcMQ*)Yrl6NQGw(o-e%H7y>+xYo!)Ad zhH}YG#>}E#AnyX!cRbvh77WFLL8) zos^+szD;G|t8J%u7j(VQXZS4QJbPtf`0{%Art_GOp-R7A<1fBra}Cu*QD9Oa20 zMN=Nw9-`KTUvJ{l_)WBM>DVVnyYM;POa)aQ-Tb?PZR1XoH!*@amstFD?*JT8rS!k$9-U}cqCTy=t zPfhmTcE)m91FgmSlqbHq1x{xu$|SB9NOfRyu>q>z>6tx9*EtFb&%vUwpjWAU(rT8- z@tK+7j{)*mpV0luP+!4ES_>>69;Pcb`bKKS*N7-TBXM(JQRyA{WSm*J{(v(2{rmQ= zXR4x1-wXrO9p;7G2$yd=xV}G5O#8p=+xfLxD86K_LNSTIl^1nMVausVG7i&QS{6` zW~W-?d9k%Q8A7?7Zq^i2-Y_Lb1*WWObn{0!d0uxSvPbRTrO zHeEd5+@ufJc=^`&=D??sE!TpQ6K)ww>KiV!iSH+E1p3z6Vqo!E#;2oC1-Mhonev^M z-N5NR{(Zq`zWhKRL@FUCxkwrjY7wEC-zrWa!=B6-95@mtZtd%Bhr?Ayi`go%;Vq-w z9?#{`%rQ{HQ$x(H&k8p z!@p~mg@3*2{?B=0{xWOzYUfw8>j%amL4r9Iy#e4}S$z-jzSFC}j z1>h>0#=;w_{Da|w?i`B$usaZ-5ZV9J5u|@or}eI+J|TB;sUF?opLI?RDDNRs7L>eUIH6}_Xc)8#ppkAcuxT0?+x@?JK>ssG#k>vpJapwv^&6aEBh}={{kG?)H7c3| z{*3NeM0cWuJ|SIDIaXwyiSm$)=66E;V4*X*STP=ZHp{(QLB>*n0!lG77zjzI2xE(< zk%jJ!4)%_3KA5=^Jjl`6G(oO8uMHl`Lio|EpD=Nho|2grm8zZ~NfeA&%qdq4&iR03 zm{bUNV+VmWy={QLPbrz>O45x74kk@QEaprl&mXhDI?J-B*2jwAtjocMxv0|GQca@H z(ellhzgJLl{Z&kyRpEybaT7h0jr}o`0dopa!e+-$W~z;2FmY`nk2wQvK9dCXI4Zc3 zzDbs#@UAW4MamV8K z6Z3!jr9U1RM^u&aI*%s7!FRyd=O3q59BXNpHoiU5G%w3JZo;o|WHo0o@68A(@+w2x zwwbX(e5Ga}7S{`{>FX<4D>?Sw>qL#s#StyN-j-V#kbUM6DKo^4-c3>CLlo?GM7a)U{sBC{A#ACe7eMbN%z}bO zWu4J5KU8H_j=Ft!qjw{JLDVx>*RXKp#~cPA+>-o#4r@Ef@pt{DwFs)vjqq#5kGK0r z4R zPEh-u2OZGS6>d5Q6+&Op^N!`5q;kiFEOTse7n$nA3Ch6(LyWNKkG`}{F#_X0B^--3 zSb$G8V@XNVLYUcJ_88vdy!+kF9{5fc;|kGLJy?VU;zMa9#DCo8@0|d8ihkJyvA@66 zvlV{K{$!8?T6jg>YxO6qFzYX^F%)7sGl!N0?JEw{Skt`3c^yzAD)(mGj6H9LTVe@z z>={rZqoI7%?scKI#6?4d`)^8%{SO|a%iWxy%ezq zFvYlD_rH>d{qN_;|06ZOk&tiLp3oPG)85zbL8mc%zQqFsg8+%sH}T(TIOT7uSKpN7 z5CvcvYdgm2frzPN``BBDxM2;v{kITBz0Jz_L8CrFqd|aSY!sT}f^&Sb$$cC^d!D5z zrTBA#HqD(FE2H7}|rOY<3vqo6YSSZBx zmdxAj4vN3$F|%2l$+j9$mEa6sEIfU)T^Q9$)JJ>ol&$n7@wfZvOg(aWoI7d-?{~** zGRn=^NUOpi{@l-=YyP-8a(^$58WWi2A!@?ZyHCoe;@@=X(A!;mov3jZ?Dcr4Fr zk?^+D?zJ|G^l(Cpdkw24JZlc_V8lb4HRklJ+f778;>}b!+;RI%oGMFrh61fk0#gs# ziARs}84LiHzbkc?MVGJebSdG(`@R?2sto4`VR&iO6*C7)j2kXZckb%N~0(va8h*Z;r%@tgFT zw$5E{X^xmjFrEXTC0)HDE{+g6rEG=!3^WhET@o>M7gJirQbIT*xwx3g?2E^K=V(#> zc(Un3i>oaI$zp{?{EBBBSuhhkHjssag^z^~Js8XnN|a(o(S^K(mpsx;{tk%q#`69- zYhee}CAugrJugLXX`YUAhYt)lGxNvd5){xch-Ry23TdCW^u4vKl1-xfYgg(2ceA{C zvV58(MaIUYZSXy4?wvKoaSkq<{2jU6EqNcxwqxB%? zK4){{i(BH0QAW&ue;-Aqn^4*`t#vy7lev+OHaBh@`?Wp8QmUIhk40c7r|<4k4;Ht; zd_UBVsF9YbQ55ZNcn=S!@cHsAZc!v=s#~|1HlOc2h;xiADUZv2|6z{Ipbl&aTS6yJ zZUMep)JfBK-`&ksqT1&es~zDsdBM9Di)^vCr?H0qIke($WG)H}2pqW?8Ne(tk~pH% zNA9`rH#SRsr10Pk7WExMGzc4A5Sl~F zj<2>?`p46EQ-cpxmhPM8XCR+GWmytP1i{Vd>@+v}5&BwuQ6eQ+>QO93o~Gj`))36h zPwBwUcy(mx)@#{;9gj@VZPOGJjuf^F%)LG$m540EyKw?OpQ-lsXKqH+GRdk)?ioZa z^awVtc`T?Dg>6s%rPqH%C~%VXn(pF|rB*2gsQU3yCX_&2%dH*T$O^VSK3ZdgK65U=0#_r$lmGp47i59Lj#1_C^i2ij=4CmoaMElQp_ z%_DkzBQIl}k{+;b+WVwWQhvr>1x=@t2LX=$IHHz&Urm>A zM9Vejl0&dg{g{3{%^xLMOY50Q+eK%U*n?D9BAQ^EXWA0cfVLX2mEdlf^h~DtXKC4C zjd?KX)?PvPel!so4f{#z!}$(ihBV!bJ3`T&tzxg+G`xzWr962g3nsvhqzNT-;ufF5 zeoTM^T_fD4TO@S*Eg%IAsCdX0><~ARA1@Iqc_4#t!~MvL!U3H>-T2Ihx97 zr=PyarC{m+fu%ZPwjj9BBjGk&SBG+&w?v08kS_R(57{DS0yYGns^h`9V?kr>#A0h1 zg+L&`;rM5Ps__LRy(7;)T{ntuZyjdrD}}KeO8CYi?uyNUzAha&jIXVz^4mLb`K7)8 z))~-3+?w-daT@wmPLS0VF&foho>+P@n% zcoH>lEQLzmTjffJ-monfi6Bah_krl~bNfN8SK|bVGd?J#3k>B02v0-MvZxJHXUKzBVg zy3fHYOL5YeoIQFF*p$Bi*V`F3)nhI#+6t^na^hxJr=u(3+WFDUd|SR!5Iv5EQ$a)i z({>0N-rVP~_P~0F=jl9K#&x^pO>h09t_x*r!)#h&I=89%ToN7uS%*~uI0Z<{wl>-? z#m$3c(~UWQF1&v_U{NuPSdoR$yU5s*v(_Kjp@*?nC>xipRhBx8lWrMD8U1y?^`nuq`~bA`2_P-9Sv$Q%UkM18k6s zR)Y2P^`$4JVg|eVkqNeOoJ(6l@5D8Xtn6yc20ThEjhc1YL|4H>_1G?zy%()I*(6`q z#@qQGeFtP7^^TK9hK#Q{a2A!5&f5(68NwHv9t#Ij-`?HQ*2u*Pa(I8be`PG{Pu$!8 zV~pt+8ak8?t2FAjQJYCq#gf`0FlDQWx1O2b5Jsn&v5RG|VE+=yTW()iUY=Sm@ZgK3 z4TrJ}bqO{E??u`oW`zRNeX^U)rSKNw@ew^ri240VI2I(%1ia!KtXCQXVj!x@H<4h7 zx2Y;Z%aem`BJc*S2Yetkc)a;p2~uxT^TUhTK92H8C+tZ&uu&AS4L+VF&-QP}F)lc^ z8{ye;@xkb$ENVcLW#W-b2#k3aYMn#kn9o*3cs(h0PC(vF;RH%)c8dIgv?I!z1kU9*Zr8tVmV!!!f%SKuQ`WpF?!;?~TNdEbbRwG)ictvt<7e zdLxsM&cjB6QoQfLuV{jC6qRq*6qW3n_48-ZhyUq}+MF=~vne$s z#Z!EN9U0!02|QITp_HSiU4EF}z`HMn(`Ko*>4W7}Qc;Yv>o5lK(H$VhP}2DvM30B= zW55C$&{s}h!z+206HDxVSUooU-gR7>y;-VwPPce1HeT6hl09^{>R93ALLQAk%`d&@ zBXa?F%tj~LZXmjF-S0l|J5yY=X|~=al)Q)45jyLa02o!Y{ieiq%A&i;z6vqsC%k9} zY%SH4P1@-n^+I;=#eE`OwrlzxxCD(v6%4*{{vsOgxtOvt8z8w=#t+X93x>Pi;!X96 zM%7oJi`AZrG)zQClvj>DW9ND-d1AKiq~o!RTHi`s=IZ)15ZyN8GaQ7;CWI6eatdjJ ziz?OR5fg%>a_eHMQ#YW(tB7VBnS zykuAN46dk@ZBfsJ=Go!LX7KbpXERZohnS(ZddB^}lI3`Qylwvwbfz5Z-X0*HwXoDf z0hQVh{2Q*_pRN?q$(wZU4-`*5XZvxDMA`V_qWx99`|bP?JnI%4FRDf$gHiQie6#sy zd6M^Nc8PcV#Qf#@?s7>>~phha8MU-{Dz0Msuo+pQKZ!+Qu*K(8z(`Y?m;D!>#d%`7~usB>#3n zk~`4{d4Y#;(@}bx)`0~6QZ#ovk9}r7xsInuhtJkOBev4rP5=o6^3LqdKMliH`(uj# z6$r#+*j_^NNIPT@QQD8bJ)1TSc683)@;Y%mW0FH5lR#_ZE`Nw4;&2L$`BJ%u=Gd`1 z$n&av2-+{vk9jA`?t5#F4c~E_E*3SE5XN_wj0$KCp1suIW2aq9X>6yRvU8Qd!VO!Q zJrx}Y?{L`*D8A)B7tN|WcZTOD=y9F0I?EL1_CoBV=Y4@)p&nC)BsxXwWh;53?E<@n z%29RIgwZR3m;I&A&#!@|i?%u}&7CYeZFV^%z*f^OoG93GGYQ%6lLL=0L#Fn?mBBM5 zY^t#T$J~2HHPx@}zCkdckN~0xB%ufhQbk%wK!`|a0wP7K(tGb!N|4?WkPgy&4IKn2 zq4(Zc1IHO4u6>@`1xe8?D#%sKxv_y4}H>vv@rO}jPPJ_|-L z;jex4#=Fm2;w7?sj9m`=PUk@w+1)haCyb061I_0auc?&wh#*mnVu7YpY-a%w6fwd$ zh0~^#xADRy`zK=;R*g(d^$b8^9l$TX@wy(6p-@}Ok0RKN2=l^t&c{fdA8D6GLIDpyz*+~7jBK9ACYKaY# zU2gK5aia*(`9De=%o7Kh#h+VaOYBLxxg&C(xVyM-A^7T5G@|w#Sac+F03$nzWZ(D= z$nC^rKG!cduAOg?RI!4G>%Tp;L4?28Yu|J?JI!1tZL%5EpK7<>reaa4lD2I&bu#Q5 zla42L=;Cs$QJ|`fXrqoPsKNW>pJ3*|>it(A4|5B+c9z2h>;>Q3Se5TANk><_=>wHV z%YQr+d&jwHLNz6`3r3vZKO=*}NK-`xAhQ2;yAi#)I_DU{8)By3$d5RfJoDs}{916u zj#T*j-T1e^|4b~zcM`ZP(US#E)0H)&wIdML45lEXboJH;T83|2Tr4*&6>W&tNKg&< zJ65{9PBWwA`ES7B6aOcFo+gactD%;HlQJ#!)CZpXw7q6~`tgu~w?WgC#~G@{Dru!& zYo;F&I=RCX9z}bE-5Ge@SDv}u!#Y|e6`DF|jLhImr|ovrqT9;vwKhW7nP%vR&ykxF$F=DF_7v|xbpF!WhS!K%AM28g5c1k+6Fz*lsZdKOnN2XEM zDP^grz(cynYnuo2&bgGo;`qNBQ==<b1#eOCA+@OX^Pv*350B$Xw*^jZmOL&)W+0 zx7kQDM})yPLr~+I98TS-bz($XePuVtJ|Z{YaP+;-crY6pLGA-0K(jnn)_j-X?pUZ# zHq6aK`0@!M8ZvjyKu%M7Vt(aqpkuDC#GlWFZwE4xX$&RD@xzH@1^5v$*WhA$AzSF< zraOh(X3Nzo(`WA@y!CKfDGvfwWcBf0w75u zI!E4%CHZ@4i5C7|b48iDBIfObdBkK(=bnC0yl@3o=%5)_X!}+64w$P-uWdqR5o{0N z!ZCQQI2{tS2RlcE>1(=rFi9FcucEULdeR=q=Dn>Q<+b+Q=(%Ncw$X>))|l3!1Yo-j zK%UKP@K*Yi9m2}l)RInEfMT>WEV(<)&GEPna4;t`pN~sw-r<%!#`x-DHqK8grqxf<_U2`0Q#mJbxElIcc* ze%`>@6Z3AKHzto@wYgbUjDwXw5oX(YqK4na1jB89u?x98DfPTRTm`wKkWHpS7t)`6 zQ5lKwWuc19ghyM;$*T|5+%Uf5OEw9V z@%1LdNw}nv#ivW%F>EY)n`#NL_FQXj`qQC?;taRE1C7Z61GD7uGB=KahlHo3*rlhA z&bZ1NUPOa1B%Qx+`_^x?*cpf-AiigetB!vK65c>#JU5UPPOW#8wtFxJj9ZE_o-HV2 zb(HyNC(ZZzjYrrQ9w0E z8(Pb^8)_ILCb9bYIQ2nhX0~=PJmVgjH|VNSJucvOVr1GlOktJ=x_*XYX1I6b79P6j zs6o24PX}a41AxDiEL=)9)Ev$jH=oV3d8os|x4;BIo7l0;@+h0pDI7OXhIhn)mmA2N z0v{u13tO!;xyN1_3}X%kSsI5YQe^@NYf_&ZI{br8=MebceushK8_^X)ZV$?dp6z__ z%zAgYCH?_#!P}~TGe>61Y~7N?sJMo7AG`to5l>1|j{5{t}D{>C|g@)6SN^+n&Z;ux=XF z+_XT1RE`13>210r9(-k4pYXH-{gZ=ZVFnUpaTuy}-%w|HjzpmL2^YDGwTZgSfceh4 zQ@*FsE)BWQT~l%bVvWr1xx~P(inF=e8i8-&63J+^%y%Xn)dni+!^b+gQM*7qGOtmk zu!)l}3czCEFI6sgjm+}sP6#buuayM-4I)}YBFYlVv@GIz#ic4Plbb1u>84`jm3(rRTVPD}Wh ztS6*M^kj8OpmmsBE1c&ZK?c`@(L9xN zE@hZv`AT8XK-=gk*`9MYgvrA15lmir3i5{Jf4v66Mbmfif2R8Wgd|7sp^+kAL?-Yn{RDo9XJ(Z>*_%5mo%6-?;xc>1h)GwK5aT z2@U^TnP#w4CnCxQhcHJbm(Cxf3TaF6kx;!AjEL!+6LqmrSDG&L`20kyN;o1X;O5@W2^I~gHtBm6L5{K)6 z3`j5D?~u*DlnsBP<4dqyKg5Ak)6$eKB6yPe`_*!?H9pi{?8t{20`EV*CY%bAd<;Gr zWFxxE&Q;23Fi<&Pt>qH_9PNxSt$OIjAKV4Jm%=WLD4i#}oBY1Fo~*=%ttEu!l0s%~ ztC_fR24DEopQ2#0xll;sUAgAH- zNQT%h#*dNy+-{u&f%lrIf2%9;aM)fKdnj<~8t!J|L)PtY{aPE8ayx#Kn1m_f$bHF> zPR9rD)CLOHefoIi(pcOo;Wd1A?T`^uSF%UKRc~0mn-W0L-Tly3&{yCM0dY}dpB9iV zv@bylFm(x?dT19bLBhPwKF!snznPbU^f5NEUBU^UpqO%t^UCxJatf@Xk-1(r$YrY|iuYlx&Y&7^Jz7(ke3KA}q_Sj;-TTfL z?;@YdKFd51pvESoWtgsTT4o0~K^J2VOkC)WhnVrHDxvZQ1LZCq(q>)w@0S7%_pDfM zgStK;{cjtEe@e&##?^3ZTrc>Jku|jnuUW-dJiebIr?ZU~>UdoaeVq=b(OY!OTI>o=p&f(8^B5h&xWu@K#+1~<9u{;>Yn#!YUC{~oLVzwj<@T{=tiJxhZDeP zX}56doN?8o{sqmYvMGVVG=}w1;p}wRCXKyhx=>l6oXQ^;lD;r%7rW~X^_HEK-GzL7 z@Dvx9GcDzJd9JNm+UV7Ov$6dIsZ4u5AvS&)7t2>SIw%`5?MsWf>|1zQ->!AHM6@Jc z8isrh7&W&4W5h{^pA;pA=Jf}X-W#f!mp6NZcb#g|JoC6 z&NZ)9Ho}`(NS;|@!ZeCA)EM1# zY&*?t_=nmV1?-E`mRiRZZ3Zs$x2*i(Z8`bgp~n5Pj6%U`DeJDUw<+fr_}7#8uUAYC z!8)^7YU#JIPlid9ONJ6Ljkj$|;?kA>q*g-IZt=(VG)< zQVN_)q;*Z`zS3{wH|Tn{Nl2KcT0YH}J55~?h< zZ=hlu6Q2w=R3ZZVc;=4W-O0J(qP}1v0a$zH`cgaa{#naVhCmuYL`Q$ zsuXe^ROr|Sz+ki_d+eDOse^+T#>ykn7DsBWrXA0LmZ{pot0O+ip>?}=Ghp`cjBg_C#qnVV5uov1M%ehbWUFiiUl{nrO*r6PR)xaUxmebSoV zB|d}J3-v=d2GFU^j7@md`K_?7-peYUCS3lXYZd=;@A65bb!&oGE!@r;)qY&RXEiXk z;E9u$U9v~v?x-f@&J{dV?fz~lM~n$fkcD(dYe`4@3+^xhrEWt6^; zgqKV^&~d(8t`skH_W)`8=koIsh#y3;;r&bwM$kPq+>x(Gw*b5;voGeF|R;?;dvF zj*`DxyC&UEgLnd0)WhvG01Nw!=QdxA7gT4P+#Yi21+L8G?D*2(==xZN$*WTE5V5FQ zEl7A~$pj($Ztvoy2iSI97?`$;^|!&(+pDJSdBxUiQgjVxj*?R>Iv36pR#Rb2%<_TX z)={|!P1h!xVh$(GfihEhpW|Qc4F!d6kvvpq!2t{n!G3U}bJQ zaI;5up!wyiqP)EMszoB+e2`_jwx69F+>FhRG2B>%%lUkuyt3>-GqhKq+o>{kmV;!( zgDHpKj-Xs~+Lr`|iTgb3L0fATc1za0ca&)`QW&-{Eu#faTk~Ik%VM!X zTSoJW_-#6J_UY3PzX56o7af%^&ntbH0{g@kI0X0HP*64iKW=K%2l3Hy_ zR7T5*x)Vql1nTf}{Ak8l)laQv)X1pe!Wq#$3GZp1whx2)YlSyoypn`_o}6f%(V&?XTGuLlUk z1rmA_Ww0b^lr?>k)}I%=FHiSUFv2P5?u+_V0&{Ufs@$gJZaNe=t)w`=dfnSt2QN0S z9F94}hvyo|t`HD28#8bSrCr94NHKq?r1Rbb*bNh^Y`2{$E_SH63d@p`-es+uS`JsA zJtrL$p}~$J;Dp_)COuoR;BO=TaVcJpY&kR2ZyhoBay_v^%jS&=^e@I%Y~#WUH2s$ztJ z;8sJ|X;aU_`UdJz|KgH$gn4MsJy2fJ+ejKSGf!am3NfLEk2&yj54P2Mo}PgDB>jD3 z+E2>Wq@Tk4A;8>?(7M_KFLf@Pi$)p8RPnJvOr&_l%<#c&szd3fkB8Q&KVG5r1&wkr zO%+{Xc-tv`k5|@pu1aGQ?K1DS3M2ZxhWbGsJc?BS7@@fVb=spb8_yK^K3K`i`ZZ}N zD<`?CB-JQ-?MujF%psi2bMyKYHcorfbzLwa9>e^=w`MmG;?*bsZ&0>jo0d*sPSPbWbp9UX}%5_N10B_PdCBPmbB=S z@0&_nB0z)TCfDf)ym-ncG+xuMPTqa(>lI1qix=}6;A8bp z1)Q~x?keoY0LS}ue*^qi#;o%yWX<~I&DRiN6=N5NTXksUhF>;R!ok_t`lGi+t?V+l zZvKNEEdtNF*$8@Jh_h~XC=cuo+%7k2` za#@FY`O)v!6QSR91I^8FG7Qp=AR2R2{pG*jJ>OKfQMIu78a`ibXwY}b@_z7fX{ay^ z6t+uwTn8*gNN5@jvkAKX$_$dNoV80x4nfn-IX~i?hl0~2%$JA|TYXgpRx+9r?M<@Ik9r2ozyOM+JkGxU**75ABtoB)zzN>`|)8G>p7+Z{(PqaRCLX+EyQ8^F=d!;s+vv6br%@MkEjqZTyO#kX>JCto=G3tun7}xe~qpUh#TV$16PY zPB)NQc{CW+S_OGm)bEKn&~^sO^~}6h;veFTLEl)g_;d=pr<^ zQ>4D3C`O1Cn1=!@Q#R6I2c{D2BDcOR|7iCy>8>?+5LOYGz~yq^e01ceus3tQCqdTr zE+LttO%>Fc;irNw~OeKvfH@hYqp|zqRv*f85Bye$Zomgd~ zi9P1^p26jNW~^SDDthVNdZgug=X$aoLYRcvEqd!`k#4%0wd~Hi$?Ynn3C7bfj>`8~ zH+6XyHQvD2oLV#M^!4bCAh$`E(64@mC$h)B;Hw-O(S{{{eNClYe7NQm{K?`~xWAFO zlgY0cRNg7M+f^4h0#z-IYc0Hz zDDuSJ^P@TOQ=O*jzFbU=k;xsU++@v@PFMn($`9vUI%v9|xtWqbdsDnj<&7aekyzoG zfR(e_Grwr`G(KuRxjS4|6mSa@_b4VlO#QO&JA!r*6bOB4FHGI22M$sSjEihultfqO zUQbJmng42u=E$n(zh~Fg#J~Qw(~Z~8?$j3pGSj#yW}767ExKX$Iq<#+ogH|2!1IP> z;5VYriQOExMNp^NB8%&YL-j`U-KqS$w3ghmvLk#k8DDdpvh6diVQGG~MFcY^V@y%X(GgGAvod!&Z`pTTgLZ+$TD*$n4UDh} zsqf>)Wln*gLnerQBvAK?dk%hmFqus_tgisw6Yk)|?elHodMKp1Ej?ziw0ANrCK}e_ zjI0cvxyW=>+v~F_0;-iKa~PKwBGQc2zUKYXelB}j{;uE<3i6N0Iv-e_Csj=79K|Om zu8shAiQO`{EVLk#?+}TPNj6Jsz=`##k|+d!>Ka{pWL7YwhhIGsUA~z@Ta?hj8&0Of zxJyJ9Gq}ET#;}dpz2cXo3wgS~)JwIBfczw6fiiWq4?G?cV^X#86Yx5=^9!z8WA1|2 zX`ES>#%!$NGa^5cD`t*lV ztJmuf7T8kIseL7U#I1b3Z`x5uF|A7!s;V=OK4v8g_0ud2hi}z`FD=`z0w9Du7rq)( z@;mEq?%av8Qdpk(?oJN0bW{q&yfC_U^rP6qGnd&uFp^EYrRpQ~Lk1?#dMygu{mkW1 zWRQ$CZ8C%EmlP#1+$z@fA{iL#mE42sO;}uSPCU&-G`e$eyxPC#|8Q+8?pQyfu^B_v z6iQau3%art>TPncax?UGlKKsB6c_LHC(~VQc^SB-O3QV*Og8X_N~H%%b2{#4b&`r) zw~P2h;XH_I6?nN8k$`0!^pO{My0()`V=&$-;1AwmR?nehj+LxZ{S8ny-5S8?XzElf z0Uv$Vd=_iw^0M#2`h*cvDQ}#kgdv(|L}0Hob3n_T^`nsFbp^F$_wa-$4=2ZZs!W5K zg>6g3aXaw1;xMh;$!5_RR{d(1&HK7x087xLcUI&(G|p*X@`YjAIe~$_Q6}tfunf7x zx=WseCW{=VbLTm;75ox}@;UA}!Ei{<>bF}?*D%qg{T9o8-kB_Xw45e(#-e0+b!9Bw z;tC&2{Y5Wof>x;a0QM=5Qjftxj-c?p7n@5sPR7zz`0L@yHH&{t;{UtYaB?!C#gj8b zvhPQl?T;)P|8h(8&f8?2fqm1#PJQYga4PEYQwVL(mjhi0m#PM9$B5UQB^B&6I2!hQFqd3CSVN5uy?IulcR@hWFKYiX^jLCzIR zQvG}rGMa0URPoqhWA#209Wp+bFjB;W5L{$o8b13&Of&V5!84J=NQ01Ixd!+A3s@zy z-b@l%d#p-j!)(`}zU?!`T|~4YTdy;QnZsP)D|%e5pQs7S8g48)s*l+XG?h@KbH1cd z&>}@&pGg%=n}Dt+hRIzfK8hX7wi2T54OjH*M)xujgW@}{L4hp706{#56CbC`Tf-aS z&0^bf+scw3*!933PZ~o5&A!;tM7<4W!!Dvk|7y+rU+v6Th7IcXTTk`ulAhD7fubIo zI_XEo5jKmb__ymqIMPGcy~D6`@AIie(tdjItx1|z?`l5lt2GTx?0r9l6bLejjOZYU zBkooC?|+@P_K+v1#VHVr-Esq z&QLq`yoh~k`N_l5G<89h@1{`49b6Q>$rh>gIuUj4TPJlZo5GY_o0TcWm74~9LrkF9 zA1`B{**)a9qgeCGr>K^C#|&8%J1?;eoXOXZauud->J51yqg)l5~Drz|Q>jS!-2 zQ`^tJeNP$x_$hqIJ%a`_rWvgX>N$t=!S##pcs+mQA`gL9qPthx&H+GQ0JDVhOQ4hy z5iJCn9^mk5R(7ho=Fd@oKbM02 zpG!dc0nGTk@;5-R&S4!1^%CxF(raOYc>;74>5B ztS^xuj=E=<;Gn%D0+`m^97!3ydx&SuV}m)+j28hBbjX>Si=jObgXPRr>2!erbv!b& zJQx#*K<_w0!9+Ge*D!@uf^Rr@({5O2-*?WHJMPtOVkYT|a`o~>EpxLu6N_*9hnxvb z8qzP4$HWS|1yz=fZnC)F1Y>&MX5DcPQS4a^TXh<3wuQ@V1AGk73>=_f{1u27VJxNj zZ0%hRshm5j=sqGz?B7W$5He-XCEeZcwMZ9GQ2)8~L#t;#YH5i6Da4yt?DW~$xew;$ zuHW_HzseNb8tSiKYtF4+yi}7-QKF*1s6XV(jCJz|Gu9p;=9hI@Gq>)lO%9LnYwko^ z(ku}qkG4^`IFDB_HJ&}!7+DSA6M#q1j=^|c<2P}A7M!>W5`C{S#h*$1JY7`YZ9W~jEl8}Ci4W0<{3`))?vh}i|8b>w z4&%_K!BlK_>(534GEWh%F^3wUlTB`eg!yDYWLPrS7y4CFo`YytzubJ(YZv zZBB?~kO9{)!PZ(Lhnq$<_I*-Ptk1$NyD@20Jz#qEHB2e5+6QaT7O^}u(37h-@Vu7- zAKS_TZHeoDL~1gTomr&kjNGG5A1@rVeTq7QE{GN&^#B3{S;Y960yHF%en`ztU^#}4 zRA{4bte|q%+bn<9;J!ivV2t6mmFN3cU^Y0}fTEu-{O~p<=cAj5q-_6@l7}xuR%9QV z^ck9e(|jMkFKmaX`^vb-nG(a9uTQNN^=?`Lqf50hI3uuY`M!Gx0cMO{EeL?f;#C*( zue3cH^CpsJ8Z|eVVQ~IiG)sVy(G66i&5}-;_e^eRPgX?{gzOn0n(6vc#|O_wF;?6y zx97(08#c4khWO%w6pxz)dpBcUjJEinr zHF>99HqWbHl{L{l2h%3lA^WRrrTkB*(o;^C2fShhGoM#)y-?}AOvRa7_RDLHtTyuJ z5QC(J3A2cG_eT6h-W(2c?`NhD6a>?f**Nhq8bSw&DhH9tPOxK{MHKGhF9hD_pPA?P zKkf;>PS}8E#nBHuzp@aQ1J}2lZzS3TewYoUxMU1(=IacsUlX_m$A{ay%NuU}6o2y+ z{P#DsKjISQ6~Df0H&8N^f;u*|wUoI#+8L&0!Wgatk5S$3>J=h7*dlX{0mhgg0@|~& zXIjYiuEDE!5XAT8_SepZ^2Xg&)rS7MVt1RJU(!=idBqaZ*hnJ@ZPW*CWQINS!uW!& zxElGOWR|#t0IDS!PU1x*Uu_C)_=KZi=i&YfJsf#UWyUP(`i5(Bcy$p+Nob*@`DT!b zIU;$-!wRmkrWA%M;con1yl&R73As^FQiz1OdI+0zFEb9>#t@s&Oc_ywxP**g=yd9b zu;XsOkkt+b4lq-#HAf15hLJ6l92%1T6M5v~3+E&3!PFea&t^3W#Ax2;^PLi~ilp02 zBuKr0jmeyn0W&E~{tJsouX%`Noy5ZK5b3{*r>u2(=v2bPW0^c!&;Nc6xWdS|L<_`j zV!2$T_(PlpPCwl^kg$rNN3qjCh4y`okswn-m3HW%a1+<8ib*Hfeq%CDJUNnn`tHX7=>+pQ~&`AHpU zEr2#zDm3sd*fvNc|D)dmBhy`>i@C&`B`?P?nOmw7`&z6f2(6QP)P%D?90l%Z5>6Ow zsTsiT7giT8XJWEWnk|hmcn&EAn$aJ_<3BX@*}eH$YayzGT7lVek15-|y=su^#uC`h z_6usUrlvxfucl@ch=OQM@ih`^jrtP$5>f1zX{t_@MyYc!Umxi-j=*U%Fi3^DRstf4 zZ$sx}X&LU+J>5cHGj@a9(kzjzu>yNG-eL2Ak==$B0U;)C2Wi2E@W#gi)C-YyT9Mc< zONe^OO}~N245W-<507V%&$SWo7Avt7}FMIvJ2lgDdh|@Os!?IywAEJc?@u7Be3z;&Zx8-qv^*N;z9~vhyuvJ zy-v9ZC8=YM-g-( z+IFpX&q|l{e9(;V2EjTn}M+0ID zAcA~MM;~0!<$rs9H6nG2Nr04l?-*4nL?JEgEi+?2aWTi|qef2cn5omhgp%Yv<)$^` zE^?dMXHKh@W;{qXbPN=)?asJ&GszLlT4SA*GmK!W2=l^ZH1MN9SU>N09 z2d6sI!Z7#XQMdGW*giPXZF8N6Cq!V?()Ts-nJwVS5<`2!^Dz84=CbGQLT;s1L9r&y ze7svf=kXYtg|Gf|n*a!MXupW6qHui8qBfsXU1WaHsDXK{KCAuEtPLUa<)QmgLzSKv z(XDj!P*u_QaUbpiLGVj%b8AnN(o=Xs7tg0QXTeG*IQT9x!yG%?OG2)@iW|z(#?l5u z79JKMTK6tyBdbH+z(F9`3WeD?PS9$z>XLNb{n$^qlFX-ejX{>7MPB^EU zIF|gwpm(qsTaA(VcX1l0fP2YzaZ5JbjFfG5kDo#v9ZNNBoQtm;Kj#woDiRklRS^fx zJQf^ULy2qz*5Rc_hE>dQH0DgS9UkU(jvrSj->?g*OWoY_;u{`~6E#kX%??jN)=#4>>7A>9 z&;>|K*xbJBvCS!H0E5qATC~tKUuz!ehG;?~LSG12#lWFs2dC4*1WWcyB=;5~U7Bkq zzi4VMuWsk_bT}GCbP!(PBS+eahh)LMDpyh&DUWU>GQUk!x&x9bq7j;iM?X3k#aoqq z8L*sD)Tf{^fAKdz?wI*he->xm5dp8h9M@D&W6jcLH|A2op2B}p3*S=mQO}{(`LI0q zGix#;SfthwmLjjzl#h1wow}gsCTx}T-iW(TsZ-!RQu6^^murjm&?h9n6oP4-ciQYs z2m}uNwYB@>&EG&_*n+vbvA#aSxBB_fPtpFYTq2FG2rdc>1f18oluNxo={0cDJ5h<~ z(RRf9ZDH$v)kTrPL7ttV7AT3Z!L|PwR0Wyy$Eo-~>;@bhgWO^H4AQ4jDJX8@(WjF7 zM%DQR$A)JORvM1Y%Kru3yFb7rF3<7$Yq@Gn*-`A!v&4 z-mz>eo@PtaQC;6FX%akuwZZ;m>|Nr721c~`@+|MufTz!X~hXA?ZP*OWly=bhpi9zXhRol7KOQ+x> zzN6tmnmf)QF1xfD+{9TrMo|oW9v9jbscRJhISG60vs~&dYo;zXI*!Qg2F2&|Q>8?@ zz*j%Xo))X{Ck0L&nckk%QTZ6^`U7&tJw5#TxioL3r@5@5y_=^UT_qd|sM6eiiY0yP zMk7|0%8al^mb!i8 zfn)(?iIEO+!%ub#vh*R|l|x2^d6e*Ke&l(|eR|cq(C`!a2(Nn2soR*hOFy0L$YoIO zz2uI@#YQiSBJXWKSq#kq!KFgDd;pHAH@;ghp#VLuufkGtkqZ!V$;^C#R5bk@$NztN|Q+L*Xtkm*V^_%8!*KS;}t5H z(VnYM=-L{X&Esc^`0S<(XI^K0Zu8`clGGgryn5Tw4kQWytzc@((v}1FMo|WBD2i^v zQ~}eTzo(V@D8gtYL@-MhUbiy;|I%B4OcKXouJtYD&aTaSI3p(w;fRh(B}g4 zP_w;7+NtRq3*&f-tusu`I1I_L+KuR8u}leOtVL6xnFB0P3&L+^^P5#G=5M#4-QFMb z;RIrG^INA4#+R=HWeJG693)+IKdJ;x#Q>*!$n}%HhFmcL9XW+mYX>7V@fQ3V9=rkm z0Nx|jR-eeO)Q=sjxlcLP`DwfUkf5biF;iHU1nx0antVK^pwknQT$Cq`#mHCZx-(^i zJaqlR*cKuCDxUpweS}`sIj=BYnFgE!S&5NeA2z_jDux zsU}1#*FZ0$f8bZ~lZI~2HMv~(WinOJ@yv8?#cf(gu_TTa{B z^s-h;HLaG|c5iqMQQ4;5A2H@Rmny@~7Dmoy#yYCqU`;x*ObRi#iKY>Tx<0phO2-h3 zKH85Nf5o0Oirz*&p!=d5wvKCt(!*a-IR5y|!4gqp`!3mS;esznMd!F}4elaz!uXjh zSmW`UCZ*iDzL-iHvhItnuj%m&J`I9sX}&6}?m=Ev5#rs6EG^=A$ebqf8;-IK6GU2E zB9imAoNrRP`sq)>@Rgk$+xRlO)65Jbo;K>qvc3>V`0a2f9878*SYfv$B@E+z>p}LtK~HhE z)J>FlR5|KBs;9xSd7>_1(z76kJU1_*LU29TGg)Ulrj>ug4(BR-8&b_!`)tZMa@NEg znCEU{hWJ8arFZ!hj?dl_Nj`UZQvEqz{QU;3T;I7=d~)?G9ft8=Z+Q6<=O1Zt>VLJd zZJFpGopS|SK7Hyg#MDh;{Yv@=PQh%pb{tcG%H#26T+EW*pIp zxd=_#&u_AqO?%EVyGghLnCA0a{F449@EEds9sED+cI`KaD+a!I{))yBB$UK^7BF~N zv5R+Ob0(jRP9_WXv^}Pw!+j$3azYh2IKSig@m9IID{Oz4*>Kg%FEXC$*c>vqS6k#9;3{m%oTteRtG?QGB~e>L+^8MnYBwfq$HMZ| z)8dBC@`;lmi&&#}m0%9G4Ypko6Zr)no%#8GK09@%z1o?!nB-O=GKJ>8EZ-6?7yq9i zma#(n`y=B^aORyXmkuGqpDC4GY|T(8+=xUVS8cznxH_}($bK$bRe^oHTvS*<|4heN z+lm3X~wm%zh>x8t*@;L ztvR8?RQ3Xslydt(HhyBXTdkGSHLyqTW`y1=x-CuNz#bhoae{NMN|mMZaRVfcHJUHpU^{@DSaUVzoPUlA15R2g`>+ zIMgwmPi(%E z4*m5T>1W)BOX#mPNgBLJPLSd<#gj#mzu=4b)i4n7T8>lwZK8*Hhc!ZHW$G4TZb0l- z1IAVO)_7nE!6K@*mrxc#;jr||HvZ0Lap+*R>8%%|wGz9uxn;>u7FNa!e3{K~J|aY) zW2#GvT5Fd3EkNS&DID7OXGwj3^wR#p1&G=QQf<+VG9<_-7Q z#t$+xH^y|fDZ1Cck>3NaRawn&x!tRN5Y*g$+rG{*%TN*}DkG8-OWl_cIbrQ?+dMJe zjrwlCh9hSr6R)AL;*o9|l-HK{+PO7+G=pJz z5Z6nK2ZY6}F;zXIIXEhU4y0Q{r~Z;YifY5L%DAxRK9=8mE&Y8>##wB=NmL^)(?P~X zE@O}D=WMjnCCrnGZ94B~{d4P1${!aq#gbPUnwPxzC-dX%yc%mcIgXyE@5U6nTO!A% z1!w%pqci(9k!od0Vs{vzxzYFrRDD@#904g95IL ziOH7sXqB$q(;mq*-x0g^6339X9dlGM*M76wkOgCn6{u8&kid}s6`c|B2csi92&LV~ zg0w4BA*(GyW2LBqX0xcpqYJW<;_+xxmv)#^R1D=S7+;(u*mU!=dOQdNs? ztVYzl;cJa9ehqw40e=|!X^lh5{ybm*E*FySCD$x_d2_jNY=?bp4PR+OYOMU+ zBA`8_jY6ii`j4083_FX zeC7^KD_f^r6U8&3y;E$LElML39&nUC&<$IVwI4suaUc03H4A1RmlA)cE6X}1l|lVW zhBD*Ct_&dXaH|GD3`Fpu9u(vUmPX=3VjthF!S36EEtAniaG0JyCfOk(dLE7QJV2J; zNFQX0Q(Vv$g>6MR2z!EqZPrVH*vfLX^~TPsKCE#*{7ktfdf+IhVNFFtGY-;PPisS)K+db%S#2S)U5 z?ri2wB%5VKun_A7xz#?hw>wVPv0q{WUiC!GZhG`Zeo)tcso~;!v<26v`QeLvy9w-( zK=r3azcgJ)LDdd$eM-l^Zvs7WJGT7BXCAS5s^f3kv+E%=DLQT9`eOq;t51aYVz}>g zpuZT6pBD)GUju7w1nxJz$tN$phlgbR&BO^xmq3t=VDJ*Z!aB{zV$vPmW_!cGd5uSULu*fMLfl=!Q^3d21!q+Hq4a@~pk=pakn-eI4wh;Y*5 z16BWvk{8L1|DE~QzGr`Xi&-c2g-4pT;n(D3wY1J*PhIPR87W*zt`kEyf$QoE9MzJ1 zyxDJf@>9r&VlIBg*|$aUe4wx;3p18XVDDO}&HoLU{9xPMv!3^XuikZOwA_o#^(MK9 zNw1pJ&ZlFIhe2B5W>iv{bb-RkHVFPX0sY2@bZtZef_SEbSa;0HEVfET4i-?+?8Mq3 z-HJX&Bj3jaqcUn9P5W>I=^U;0t(mXs8`(D>!-YkfxtV~rOcROST1QS?1FJ?Y%j-rd z@D5JK+UW&23 z_7};~U~^&>qmEp=Nx$h5#zMnQ{~t3kp$Jc-R$w^YearjYkoF)d%f691Rq-9nx)2Bh3LS|*DFs+|h3esxcyioLTYz4Y)tciRCRRDJpt9s$p&S z=-G;vF`+i&GMl zR=zTAEtOAZNmd6&KLOb9slQFJmZxY|f9F8M!Rn|jnv9#(OHHO#ilTxbqo z6w6u}^Dsq?1e1vH^NJ-Wc8-7d1Frwoxf($YC0X>!X7RSDUl;!-IjZ)gePXeRli1=7 z#lmzk^cq8KNuH8D+uWp4YiCl2v0mrBe*?1S5Ct5skjm?i9$>Df)J3EjTyN6=KB+}I1tlly@l9;KJZlv_ucRD zDY^+Kc7%X!99a}Y&DJe9MRw-*OUSQkevhW+9Q1@=?Te9_odPh04r+0EIvl3fiNtf0 zw0W*|@*2eQ5bzF7tl}=RjWHv0D_th6yoO_~S@mQUq{7e2XhziyB>JYjLLBCBgPb{j zmZD2+D_z_xex8gaLb;AuH1e)VR5iQM)pkW@^MvCJmc~ShD*49v`ztHg{bs?Xr_h%s zi{C$^D-*qAsHY#>-yl{r{8Fu>8@O)Aa^Z(spf=7>GU~XKCNnVSW@tzwoIC1MfTp^z0c^r_uV_ryQj~4dojjZHHtOhU;p~`oAaAJpL#( zB98x5zmI;cj!OazFii}N9eEf4AcImUD(mgeW!I7N=3tvR_Vr|VfT7W1Dm(y2^>TTr(3eNjlJM@(D}O>K9s3q;+Pj|BY#h66>c@^;-%fO4PNNELBl zM{8uyfPf#jIC3Y#>xrr(mrbL3)2@5}!ZXYFgOU!T)@JVhd|HAh4x0I-8@?`Q91d{- zpCtEE5&H&JGQ`h7g(bXT*LFC{q5iRr_AcIbt?k)Va)G*n`Qo9`>nYnX(0)% zsqsO$x#iVx(0Do};U_l2%;uGc_)`wMGbwy6xn<1NcCjOC@=XA8csVwV!%pFguG8Gl?4FJa6BlqYKq z>p9?w0?UO;BI!`@mIY6?%p&G?C7GF)BJM@{8E67mGA+TEL$T*Frx2Y{Q+xqtqB9NP1%hmZ-OY1aPuBj3H%+)P!GpL4_}vAg`>=|DD|5WxpEMQ-Ej%@T0nJw(8TI3da-59s@Mn9#Hv$sKp_#t zP7y#L1;Zc={t(FzW~c~dC?%osN+bTfQF?`ojb+o~a7)O%T;tU*Njr;BQmfvA*`h1^ zyEIccO+-&KN2uM!Qqazxzzsc&-dv6K&a9)D!sG#HCYz-QUXY4o)Vm_Kw_-O*`=p^& z{aS0QxxfI0iRKQrY2Pwy+~J&tY0Ikpeuv$xG%Z&4L9yr#)(+J5F5fq2|LeW&K;j=+ z>R*P2oSDAN%hE=g^`^>GjEl=a!ebBl{m5_4X^}{E+#4;Ho=%DI4>3~NdSG+l?N*a$ z(3se(efHtTaDYlxML)jDpEFN^F51>uPJ|J|#<@7Q7Q0ErY}hia>j9Jc6gv+!V%^b+y?0g z9IC~-aBl4hzp7FUtYb;>Bp(#ALaurD2J^{|>^mlRvlH=D`mj$WGihR8@T9|Xp{l(B z{3GArbXI>!&+3B~)g3)aeVj3CQacjG6QY@l!czRPgJh%=$I1?Z#mD5nh|V&oWqvC; zRprAlkZC}o(=iU7Z39<_j1qR-4!f9j!_rHhJNx&g%>|X_6=AA&zo5%s{=zRh2hlx`xemerBSM z&*acL-(n~|NvliF)m@k{UwGWMM_-u8Kd$f1x0gT)d;U2q{`@C`-q?w%&OL z1al4_Z;4rL_lX-(&L!;I|@Y>8pCa6pvr1-x&x}f>REi?zFX)&KxLybqLk-ogLT* zykjEd`%|=qO?=aNSltO;L=E``39EJ?5V|(0wimec0wrHaAQ~;KgGNsOs-s(fi@L{98%?eEHQ+fAvcb zTQ8(6*i(sir>_2{l=fhz^}nl>rW%6&hjFO%gK?;;p8&(_o4) z^QWG#f}tr4VEl*i5x#f13-M4-46$fO1YEe z3->ChD92}3wAe6r?;93s%q@CW%K?>My)k@q68H5{645muS(AAu{c69ou@U=EX| z)dtMVW&!pG1j`*+E?OtRbxa(Rivz^ccj4VUb{XB>QA1&H^TbYibo-A^dp2G_c|(dy zo7O4+Myfx9D3KW4hXynJ2HC;@{Jx1wrsQ|P1t&wLID^D$u>pI#2bEY`2wbrX#jS}t zR68<-X^4v+Ch$LJV5l)?;O}HgupTtqrl9j>+!tw3d*-@U^0wvGJ%({YpBRE&4I%&l zFAOu#hWTgvOpaHTe9_2yA?3rnY1Uf`TcTwyRKj(v+47BRjxP_P4SUU1ADU98KjPR3 z=2`s+WwQW*qJ=?W<;``p1xJ;pb0@M~0Fx1%_xwNu@7@tQR623b$0MleZ;UxC6cnon z*yz}w*Kz4PrVs%@@4bCEOt;Q<7e+Z$wfjjKpgR>}yD)2GUHK;3+15;ER5n90G|#e zg`mi|%TG`?gqgDU!-z6$j`7BwH9F?SB~q^Dr2Y<3-8d0ED2Ocfk~{#U$j95T%1-H& z7Ln>NTnx9gcnR>ZGU-C zoGUBoc#7Q^L;X?&8|K>lCWjlteUAf~jjMOI!Ui!}2NLWI=j{+`k%oBUMA)sf4hqU0 z2pW2JyipX&EkYoF!S$NxP zaR>4}MJ}0t*>Hh2pYEhP1|j>@7GIv5U3*4jZ z*rN#R@+FjaWZ5QORFu_K66Z%_cBc#~H(NDB?ZF{*cnHQJ5EQvr>%NtE!BE2Ia`2Hx zqF>+AJH?K&vtcvmG-EJ>qN0Y0|M@H>G&UaPt2J+)={y4rO11yktpKu@mXv+nDJv;A zw`ST|5|$ZidP9MNScCf+L$5R>-?X)sM%-KwL=c?V|`yzk!a0$6Mp0T zW(7-MiI>JhACFBoeV#~z2{^#4rqhVtNUliJ1ZLJ@^QnDiWk)Y^?Py0GFyeV3k}R|h zQE~?;%>?}&;auJw0GQ<~sxSiCXN?6s*<=ei(p-~Xej+>ARwM>^T*E_x;Zl9b|Gb|T zY88)y=0w;>Fpp_VR9!6$(Exe*l3L8+y=hzahgMUku@y-5wbXag=r3yqvc@E`Hv;A6 zGn*Am!ZeQN0fK91uTkzDBa9ZpEV(`<<4kbmSXNev!2lRE1ei^MkOq`C+R%iUeK`2h z9mgfXJf0IQ6rP00qwjbegWnHptM%4hHEHG+hw9H$yq|3c^O6bA1ldc1oJC9~G(s1q zaV-5&B!`1a!xB9s zpbWFeO9a6PxDk7~iZut!6;N1i)2!R4jl8JN{BWlf@b~qg2(ud+Zp>0djmJbqQs&Sk zwm!$93AO2Ar6^N zp9IQ#ARfj8p8W-ZqUVhR%P zS=reDW!x?)>j+eZ9mHe}2|C`<^`$gee%7o@a{Ij3pU~Zjt@$|_8R2R$%H$&y z?Mi1R=?-{icSeQ|0^nd@>&S_g81oqpK@wBaQnTPjR?<-iEfR*-ax{D!{3#sb`|(Q4 zVs*~C*hHKoV_qNAd3s|WgJf9B*czt|0=eZEqM<5|Pl?98058H@6w45P6qxuoJP8}$ z)gZF5;#t1&62^j4A6=~uSQN0cBOV?v;jpv82Cz;$q34KKy<5KBCrBDBHH!%kM_QZT zTtzp271-lEZk(UyGoV*c5XZyN7cbabSz#OP4H_ggYWo&H+L$-&<(Mi<1|hz9LMHjr z-EK(`nGB7;fSX0wQu46)h1E0%=Zt-uDtx&$-TnDSPuyANKRcf~C z;BP%dAdr=Ht8P>HC^p_0>p17x;+2-f9fnk4q1oO7!@jPiZGEl*z(BJD(c7o!VTQGG zT&`pmKMHx>=VNvPFQlFwd7Z8BYbtq-;%8TdK^63|vIYRH0T98G*ND841t~H$#HCGH zC!tg9U}wElk}!+8Lu6{+G6Yfza;m3zj$uaggk8hc`o6DF?)|ymr26xi7=H{WNQH?; zoIQFbEjCnr9<_tG-+pzwfB6@s&T2=Eg zXS!IwE>R-3CNWZE222``4f^S$0eBP}tSZVehzK|JhNKL&Ck4)45+2LJd0Z`{+LS5p z$ek~khmt0$Uwv*t%NAj69FYeuzr|<&F>hvYZkdmZ@4CCR4g|2%n4>H5aU6t%@)X5L zaJo6h#_A2{M_H0Xp4vnI=dJmku&G7CXk2{(k2%(2=Qh{)jgT>KGte@gn}ls-6eb2w zB0)c)U5q&5QkYT6uwFp0_B|_@+uMPJ1nOIe_$S}&KVzZ)PARqj8t&BiGu)YOGr?8M zvK()LFR3W=SR|=0QDc0N2vR5J{q=lqYD*KHr9CevKif1sjRNXDk*zTTK#}|EjhMQH z7nfTvZn$0@WyqJX%+md#rBs$fRWERRBYLa+Cm`U*!8HS0dEwCkKbV*B36&!8fI$rj zE1%f9O49Sh*En@E7=~s|L8sX>%4t}7+v+$^mdTY&yjO+6(Wyp809Y*K;rKrU6kAXK z0u+C9k^IjwhWbwh6n`yz{_mcM+8KGN^4fj^ZgP{U_39P?EKXFs3y1(|?;gwyB(sT( z&1}G~#XGv$rVcM7D__D<_O;iLXhhYv;o}oNN&9|+3 zWVp_J|&Bkk)>al+}sFn)H4vuPL^!NnM4QL=k(qYn5-`Nlm4D(zwr-nuJJw zDnE+=E@&p`m?bRbN2m?AHa}5b;*Ne+lEI5j`_50^W*t5xM-&F(o3?aa&uHXsii_*b z__9jJf!=+68_TXec>PnUS7IcLwQ_7x8Y=%^8HsRVs;{kIJ2UhprvaK{$(1qqQT;V5 z3vPTZ`#KQ8(h+w#Q~M;l9uzbtCtn!+sOvL&?O4CSJ+3Ovf<;>n{m1yomtO%Aj$-jm z;{B(!Cq|3~$n0Fs60nrvS0Xcvqp@9n$TUDt**0W#I=;b!U)&%hi1}-hy?YbCGLN=c zaYQmjW9!2UNdd)+xl(z4vHPU7_M9AZun&4%T7ie+-2lGWQHQGsh#6HTt{vh7x`Qr4$5_m_RM{n`v^&uVgcqyF;5Xj`wmdngraI;j*K-gWB;He z{R@k5Oxv5Oqjz`lELVL_f^~ogS;A5i(}&P5C{8_T;~@@#9R8MX=*ZxQh4pry6VG>> zXs2!m!w(fPIxO-dk&Y{ZJbwg&|IM#|j#I-PkgRRl*sD?mma_l!$NtDzzMi#-E7 z`5K9rW}R-%yzjP2Tsip=QT+X0uPCl~7WsLdjY3F=P<8Beire?Wn;)MWMOiF=6pcLr zhvRUTd%V$>Vf@mP;L9K~O>gXYjD~F9Tr5zWx!DmDdsP#hnbj{x<<9D z2>UV{nmZ$_mvZD0(uxNDnlyk=g{JEt2wnx?xFx#U4v#3lZ9Mfn!!@=%kU7fPw& zt0BcjUb5MqG_`Z@k$3vFY5uCY7s)qFeEH^l%_sX0=|xJ*K>#GG0I8<*c#Lc z*C?9pm5 zWtAjNd^w?rZE`LjFnU&?U-03#s_%+!G^+W)#;X-lL8WgdIJJHvy`?ikqvS*S2kIx3 zTjc^)+VKT3Ps9 zA5={IO2uKgNE-|n!Tp7cWd0*A^1tO*OJqT>?eP=v=#?_Fv^Dm^nvf*v`w%9%rdD*7 zh(3-<+`>_aP4CRiD>{(8{9`2`Bxf@jQ!dgso+-x zs}d^Zz6>hDpvdYb@$ZT^e*htr*#}l9uRD36YX6?5d;hu6#IMQn-}m$sH&u2CnGrG> z4)A?&fd6;#q5qf3y|3RmxoLRNw_?3eO47PLMoTNa;RptIQEosF(oN8}D;=3!FwWG{ zxXK!@OH@@KUqsF%uMIr7Ty-z>e?$~ZE=VU5RFPbNPNwovICbKJ%LVn@<3e7j>Thik zHXZ&~X6)^ zYu`|%u>&!lv6KD6h&yFB%AKP%ZRxC@%uK|R7VD3N`aB3-2$|r0oWKM}7Lbcf<`!4} z@+TmbYO&c)aW!?<_V{HDEZrB1)tlP?3ncy*Cxwy&>ot(7P>d}MSAyO1Z{wzCQ2L{$ z;HG6IQ&H8-Qk&=IRgA~N6H}pJDy$dPlSAuo_9DS4le?=f4j1whDQ}^e=y>(Yw>%%1 zLkbN#0-{<@YaPc%^s6dfmV}qABFZZc)*6#RMUmNo%c9>W4vOTTu%4AXMfe&~SlwZh z!7MpiRptHrX7j&P+|R5e)A9Tc`Cg%xxk28-`ESIp_t zG$$}7(@ym=&)cc4>%t@bxLd!!mLvayDS@1(#E+|ZNuEm7>AI2M&R_1`=$UTwqiQzZ zgi)Yu5@O^YQFW+sPd1d8nsW6@wa<3{WVWG}sa*4^eu~ajR{I)it&K{Z5}ttOB8h7j zwT!7g%?F^gg6YDd0o>_)GReE@aDH-kFa!eL>stz3YL3XSJcNY1JLIN0iM_OfS(?xyn& z{UC4hqIK22FWuCTk z%U}3@C!dkc{o>xX8zwZgaVMw$p@h8SN0HS_&E+crv&(n*8}``ux<8OElpc!J_*bd^ zs;j@+)n9$^uaV)eQS`4F=daoM-(<}YO#2gXc`$P=2fZb^k0M{_MvZR|C%y0us+|hC zar_C`W+C$*s>fq&2C`ozGYV9`9e}OlDlHpR$%fLClP=p;``GqbXPc_PD|HSdJgrCk z{(yzLZ;M838<5t@-n8ey(YWq`bp65~tLHhI&en|l=Ph4Jw=YxEZ@Sd08~?qJ2wFilw9FOg-MjlK;MYsw+!jusdlpcURACTSsO;cc^s9 zyOB~FgGvQg;j5acM)Yw{;##Z?swiL>T7HfEf#VfWq>}pL6EcQ`=}oc+MunVX?DVgO zf*wBY$2)N|_C)L2lQA4cAZvJe{vgUF`%5J_%_;1bQ}BbN#B?nJIkTX`_w4H^mas+d zv+p7?D)VOs&~8ab;|rG-ORoyp)>!JmI^iT;s>N8Oi}HiWq~tAGRI#cH_%S`slhm+j z!VXVb3mEcf`i}9B_W8`5Eatm4jJ^z``Jl(4b*X-D_zBc&RevuGcO9=cn? z4Y(W8fk($5q9?vgm7F~f5CJAhj$`}+1K##sfD?OR&ON$$f z;Qj0HRWd5S8@u2A5j+lKe?(Z!x-kC)*xdXCd|Q0sxh4AWxBlx6qGYNMyXjw7FXBE8 z-8GXncctI7Fa89?&1)W0{Q)xm(`%;2^m2z!i6{LUjI{?lBHme7zf#Zq2}t^c@aY#Q zju_mU>aMt{RRaUcujV+SCZ@X?xf7qEmr<_^6{_a*gRgtWyc`;((1I{`f!iubSO(O&+rFyHTrCx5n+UmL9X6~p%>w={mRO-`=B zy79WmrD_{Wt_s|LJ6}i^aQ_X6kCy!xT|k9ZbU$2w z9Q_1nm}<2TReot<_z5V!Z@;#RZ9Aj-W32mU*FVq2D1w`XX~EcPxLFhia6Ji^CV~(V z@I(q5Z4U}!bRawe!INXsy5aq811jLeI8h*n{m@p*?I!rp!PwW=7J`MwlhNAl7#f_Q z)I=9Aloc4j8E%h=%ld5;9`Ra1@)FeIXoG|L*@lejV-?!;DGodZIl?LI3#a_R4E(Uo z9#|&f{J+X1{1epcCq1L4KnDCLoA181(v~Je5QyCkD=t%&e(~t0|6t_A z@WBIE@5oTtms)ljMrD%z3Fv+6A{h{?t3Zbn@rcV*klGpT1RQIkp=k)emPVDq1zJdh zRTwI)ez-s_F2RWX+~KVozt!Mxb@zMeABs$IE$`$A1f#1iPn>8~EbchBkZ&IRc%^RM zTXfO&v(@tgk5FS_c`FZ~<*f7HJr)EMP$V|=O1_%;@o>R|MsbX~}& zSjVt@MuB2bgJlG5q6zPX%)Y<4A?*J%2K`=IxD$CyVfJ6=c_j0PLh~yNImy@Gm}Jra2TU?KY|#&skS~vMjbqAk_amNsD}GIe zlldPOC7;4iD2#mzzLK8(#wM$KsG$O4iWR=AzrE<+-aPDkZyF?6gZRfc{|${#L38|D ze)s!lF|Z{Bh6ju|Q{M}@uFUvPCCd^eGk@6maQy0<>Zhwacb(==o7)ewFk9o_*9f^v zta%3cwmocRcJLYigS!}+jZh0kxzU1rnTA>DXzNN*oLZO> z{MWR0v527cq?5YPk6t#-!v_eY6~7M78Ri)a?U z919#=GIk?8cWjMqwPlwaCK1Gq{X3i*!~&4Zdn~KOuw*rC#AVZro<-qb096lP{~4?@u+l^eo@Z++dBum1FJ=%W z5uyVk*nptOAw)2Mq%>=daX--Ikf7oQWja>%u_t&4+_x_DYHonO}4p^zW}WN zRjMzlY+U>KtE=>z7q4%B2$pNF)!(;&`3ab^EWL`vz;H=2-Cx+xb;7L8X-EaZh|6b> zv3csDd5J~(31iHZZtt_rz}B9eF-QL18{@W!8pH+h*pr(rz^hp<=!J^r^s&g2?j4+g z0j75SLGd#AsyVqa*eTWLz7fQc8B>q4#%CObMkI!y4|m4wW?mEoAs!uR{S>y7`YFdU zTl{!#@mi?8b~bEhw1$Br&qBJ+r4OasO|dZND`vT?1ItxVnG*+TIjO&`U@A1FG*)wp zr;a1NTHf+@7BKJ;Qi?j>GHLZ!5O)xJtx|@aI|X z;GG8Z2g}{97x~!XY~5XIq7%wfs^9v)6)~tp)E&85AUk9bgcj%j4m)b2H^*IlAU% zUFqCUuZmJ*&PU!sjb6N>$It5bdRgw^?JzN|f8t`d7x39H@6=3Y7us5f*r++xDkFX@ zcQxO_GP0i6GJRBIwwNTz?2Z;SfLOT-Mdt5eiH*$xD^&%2YF{dD(v*$Wj?N?e6qz^E zs5H1~E%GVSXo|~1?+A_ed%~L8zPptHhZf27_;rCv%MHL}aeBbO*SQH9cAKw+%eIkwhs^fzTPk)F z8I~=mmfbPN{S`e%lZ5_UDRQ-w%M&CNM?Q{|Bng-_5NVf)&Nkzv=B3J7PSqZ>8Fgl~ znMu#AF2rPbrx|k<9)WoI5Ar`8V)2m5bv=uWLzQ}TZFu7O6OOrgHdGX96i0_%1iq+m z+kH)3DpMMX(`Rhu27U^<4_fR~O~@cIk76UCJ;AD#5ELSAe)x2}-qbeg$N(T2i3^4e zak1;+?Yh*E{)pY&yFhzwe-9f{4s!pjR~VpWP!eqOTEJc$>u$ZNDq=hjwY2$o$|c^U zdtRl#3#?XEv1x-Vp zxf-iQhT6JZgS-UNa((kQQm+ zaTqdp6O7RB8Am*8p>===Ruw3)jRy)C<4MT*uIhr~2U;7oT`ij|bS7OK4YS^! zjT50+mYa5RdR&0}xBJYUn;H869s^kD-| z!mQsZJF2)&!m%5C`trVS=k3FpY68bRF(&Wh2;O0k%Z_{RxJrN;*zID7JTj)5N@J}3 z^Y9UeyF;=^DLxx`0r)Z4y49DVPf(qWbcVbv7AzT<%}`^jr^CFPS5B-1!r{@*y6ny{*PQumo0VoxPqmET)j*u zeOKZO@hA=kjZOS8S$o(JG0p4V!Zos-pPL4$NtfFlq1G|r&FuzaMj)mEiJ$H$Ps2=Z zR8;Korw7Dku38tgcKNs(qE~q`Mk%HRBDwOWtEn?An)rg_^tm^~%ItmuLf8z;Q2vPBY~d zS+$HZyYPnzshy!tLPRcuvcTzJ+LS?qSP(&(~bo?H@rd3+iH z*kmMQy-1lZ?7O-bGjR)bs7w7uJ+{I-%@I=QRzpW?Rc^%H?^C*)Kym5l^O51-GDbD- zBDtmVGfT1yJ}v;`s>IM=yi5H~5i@_S*$@}^=mmtB#Q8b8FB&$r@STE`sg(uS6yc?3 zCeFxe(BWsjTbhiPe;TFv@0h((HYWY@6Mohkb9?D@&-JO9S7LV3>Y@VRWu?RQyE>NK zh(}~n=Y2D?uQ-DTJKC!Fsyzq=#TjQ3+%+&L`llSRH^qwz1!%elE9kMCg!0DV+PrIF znN)*+jA{tPjDlA95oE=2#2%wpzIUGQAYV|GYjqkxoYiiAI&c>$<-?n^eQLAcJ`GaZ zn_Q^6NKM;jmbz~+9_XoQZ}kvLk!8|_h0H{X+6H`bK#o(*UUMmERVb9m%F)(XR%wCh zLs<;R-V`AaT2L!ByzVtiWgTFo`So@{c~zfg$4>r9kl>f*j*N+Z{l>vTdX%kW8m`%U zvk~;bs_YxTG?I6B(3z=EEJFAr@)kJtOO$^RQ@55YmI=uZY4Jm%&keQzPE5rxx2*jM zNc;(C`3aZ+C&FI&4cv#>t#p*y(Os&`E3 z!{gyMXJbbD;)^TeYT$|NDhtZ8@&KVY?x=Nt9(`oAZfD@N=vX@*<~JsTDg*nGiM|+ck>n<`Iy-8#hRJ~ z9g)Ws|83uM`oMUfF0Pmbol?epu0f61$Q-|I)i}3g-cdg+>NxHev~hiOOk1HhrS3t= zg1h#eU!+XiL{-MHF=tmWNTO%v9b#=9GpRLrBalg-1CR_R5Ti4&UL`Dp>m~DEth%8i zmmC#0_h^U?8{8|BeeM0B{-UI)kiv*5%+mt?Qk*rwxLV9YN9-#v1a*wD_N>U*0MDu2 zS5qoj#T7v0#n^EnufdeHVhHf|TYGSU9-d0AqMl8gz= zzR9Q(x;fJ5b~7WQyE5ib8iy^q5G=_?&4XOW(_u^8SZ2n@t*+oOg6l||OKiSpuF1^N z#~j6tRN37k=7%HLTm4wPnd(#8zNI_aSQ3|PgbqPQz)EO^c5?RHTcXKud*zOQM?4MI2&BA@Hg6uES`9`9fy}fG7q2fOpA>OrVA8|N@=j#Oyz$nbTvFLIP6f% z0ss=)Mse4+qB#*QaVt1D4!73{-WpcVqkd+`=PaKG6WH0(==p}9J$tqs8&YhqWBR2i zmDzSbBma?jeK(DN%~cSj#pjjLd(hIC*C2T%oNpZ|2vL<~p*ALXf<1P6tDu_w1Il6p zbB0{>k4I<*3nEE7sWAB%T|W#HBcJ_VHO>$vV5F>!G1dUJa3Db?+6ZM&ibMy1jm8i2 zhf8s0l_me%d|e1?JiEXO$#OJ5`g6f8VyU5;z0w2?GmV)CnPd`3y&{=5#b$B@7>@B1SNur_>+)$zmoTTY+e5;ayU9ndxe0FMRpk$_i$$n4l-s2tjYqM4s7 zuy$KuHpN6kLF0f`P_dPl1aYcyzuA5wrz+9br&3JPyU8nH8V2ieMpdhZOf&aFUzz_< zF>?vVBeNZ1lE%!#Nnl*_rDP{kn>bmo{EzYYGqZ8260J<_V(?_r*NL~Sbv4$BQ7wIa zwSD+_&Dm%@p<Q$dK!Rv%8?kQ_a&K(dGU`A`>JhΝ#MqNty>4g?4N+7fW*qWCPF}3KFC5x!0 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 diff --git a/assets/partners/banners/minimax-zh.jpeg b/assets/partners/banners/minimax-zh.jpeg index 0bee98460df05f62fe6429eb555326849c65fa62..6b286c425eb8bdf237bec66a7a02ab349cefe8df 100644 GIT binary patch literal 197994 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%0Ao|?-zDwg9O>7G%HZ3}fD8`3PJ>RRB42(%7 zPz@l*q<1d;>NdYDbWd$F<=DNaDN~)j)Tj8Wdkn8Q1!UErAJxo@SH>8JM~F@SU3%|C z-wHTy{rH}{A2~kTQ~3H-C-suH1MzE#i}0^^^M zQ$N7KeZas60?{6B2gcs!9Mb=|0FHiI#~Ut_0B$K9PDpHKhS9^C4#=cW5U1utVbT|9+iY?2vx&MP6UBj2|44>$(ljl0VDUws<*=(}6 z#akgCzmvL6nzlx-&M-dj*fMe4uf8FQbm7{JwRa%>vOu{Mj^SpPu$YEh(0{9WFY{$k z0t9%?TqR9IeXH4(Jxy$VSxhgFvXHqNEAOBfes76)ncGOU+#~~EOT=aDC;EoNfUuW< z*W&Z3!ylmLr1z~Gk#_;`X48DjrnI-SFD^;C2bMs7bQdab1A*Xg4%3o88G7Z2R){BZ zsDC?J(Up~Q_W+I!>Z7mis-z51n4Y>|d(eKHMqtQP2ckecqI3JpdseQ=*$l`+ZsHwm z9qS1Hi&N~RM#dH`k#WF#X8WX!~=+UJFmhx zH~9oUpj=U));YHuz<&k*iT_qpXN~|h9S;Lm;A%~6F)x4(VHQ%Ud1KSa2f6%R1+-+EQ_+&PCp^lb14r&^OjX;H zGjmQ_zbkYX(4yzDe^%&!$DLA~SXDCtEjt>(za4z!Nw-UD!UqmkWC|8P0ESBcs3x zA;?dK2E0Vb>_ zFBY|B>Ju$Z)c&kCb&iT%l$uTWj@B9(fwCKwFU(UE;7hg(}B_#)h_hQ4CKd z+n5|=D=Z8oKf_C>a~b~1@K@%w6S+^-dxLSxh|1fGl|O0j@KT|?vd&=x_VFr85%bRA zYO)DlK0#-Q5XT!}-nbJJ*A!#C%+f4_3+(Qh>n_=goWF8Pee)i(cOm!D1q*elvH#yx z**Afa{${@|p8gUE*)(q5L?qDL+uSZydppQ|l*8=fb$u`T#5{asVj5~LZl@fl&xM}%G8 zmIV`5lWlwfI9hG5#J2@LnrOCmczd(SEjnY>==|gm<&%v z&#jYNdj0wX^f7U*GEt`k`qpV+Ra7iVI}AVWtf!mj6G*0P-gc(5_M+!m zN)PcLAc*zrd*(NrBI4TgNt_Uut)3*WBJta{mbrQX_zGS-@X2nn8xfipp=~XCFQ^k6 zJ#0u)3(c&n)XP1WYyNzw?b{%ho>U^4BXnsp_W8-r)6y%i|Nc)VWa027>%6gN{$r!N z*Fiq&!+^JF5`|e7Mt@6eBlwXY3P!hn)=m{pyXo)YTRzjcYqTA#kejRh$v<^EI3min z?ZKeiPDu@BTEDk5sJHCw&aC9iQo5gnI%*WXh049D*Yne@ht33#%cz$TVUv_cU3LlN z@Ss!|Bt=YCt*Sk%R8<&3ZFyj`TZl){!zwkV1H;G{QWZmskfRvlB=59!^?PS{I_I*9 ztsf9X@=P`#v#x(trPu-~KP*u@hB^QWK=zM76KOYTh`=b~fu z!*=cUdkQr_OO>e8o8lP4Bb^Mwa4DNCU0%^MJI_9197VQz{5$tYHIdmXMo;$uGxS7l z!y%!)cE}VvO{p=-(YZsZ*hXbn%OukJFqmwp{IXP}BI@!3i@Is(>b@Vm<*PoVG~ku8 zOKZ`Bf@UBq8hhL|n#Ze0ue)E&(Z|WkS==J;iK#$z1_j0CdF4V90vo^AD%bD!Ulcxd zt#Ib(3DFO`KVmmM@fgqUbb}X&TK|W;3@<>dk8#EYlnHqWjk^W*wrg!`?-1t4V`KLM zx=+^mE=a28FW(;Ih}wLah~@o2UtsxeJC^|#--FfH6U_8Xn*0L})yZQ-`ElPx)$NsLVHZP(}V2yD5m zrSKW~Cv1iMaE`fU#ahqnyzAWW>5AL8p0+abHYfIWd(qrN+YSp6k$#2d1O?>8bI_cn z;WR8kvq1UbSoM1)+z;=w-1n_e0fEZ1M)8r7zxwzlcz_H@NA3U0;jxeu68# zr=ctlfQSg1y`|&U1jf0 z)WG0U9Q`y{BLhZ9x1lh>L`}WASMcPk=f!q75vo=z*ND?*NI$n(RZM%iu+rYytgewQ z)z8uksJ07;2`#_8Q|cLKKMXsqN-Tm;3eww!z6=dm)Iv<83%cQk5$wyuhHbq>2w?M3GdH?dIY5=CzXKGZ$Y+6QwbOthKZ;$h$+f6b@MnnSfv zXW6QpA=A$-8{*k?yMmBCowmBk3h5<_hA^)(fTUN$>N!6PJ`}Rf;#7=h9N`o3?BRt;fHrH zb}eb8P~Xhhtu8fkBYR^`edog5ABkJaQ4?U;!b~pSj1y+D*3C9TAYVyih0u&+eG?&6 z4>?)3HW=qY7t;6D91gmu=ZtK-Ci7zW?5b((Gvqq*HK66}lkg7~WuB?;&^<>|S}x=- z@H>g36q$}{StBb`BecR@#O{nIXB5NvDnn)93OYKMHOW_}$d8?ArlSt^XIr%!RX0pk zEmI^5gajKFON86#86hz8kI|h9Ir~Df-)CV0Fua{Wjniq@-7)mnFa^ujtgQKuiB4%A{3uv?r}dgoR${GKg-|l$ zMBeVY>9yytE(nS=f~mi&rg44e>`-LTf(K{UXu=FkAPx>wYy)A()Jd&rh;C}i5n+Z~ z!ia>#myhm}vptkFB+P}yh{R!M0rQr;18EOeA-r6d7=z|6f&IS2v^Xi3G`xJ_yj#R- zYE3*RvCG!j;5`xs2di>q+@l50r@)un%ocjnDkhIQt1eQCU&F1Uc9F>b{^C#w=d-@1DYGbMfDm%Tx;Wb&z zgHh?x0DibV)rJXxIq^@+WT+#I1gxKqP8a6twzEwF#JPM56 z46m(vFWX~dm4n0AUn?&%PrJicR)lPos@lqW2KICfc^qzuufrxn*kULDliZqL#emB0VC%*3OZyH@|v?w zvuM{wv7x!5MN$KhaW7i0q0A?TKQ7#;4HDggeirPa_MCBkOHr%67GSf9Ch+H#w&7JQ zvou7<6Mt#<+WiBx#nu}Z@5C`vwf)T~IM~u@N?GMgF5H5cBdaNTgwiZ_!e&gYv@0ZG zvP0szy3;~Yq|4V(z)HV&V)P)5oB^w##Iv(oz;$Zm5^i@ehuc~+$mo^rJR*^;N6YTW zxp6)YCAW|-dVpiGv4>E{v1suQ7a~Ue^1k`*v976hI#9sAB)Nc0M+Ip`^i3O*Fn2rm zx`c^QuV$!iCsy4QySnACgf)zcTXyfpp(QQ;snnxx3ooQz)S9TTeSL-Uf*mt-tJU{6ViJ z@vc{5jEOHngNqeU8}^N2SQ*A`nDSoJrer>kfIpb=-cd2FwyA!3;kr(DmXrf&hoO#) zo%@6djudlbhAv?LP{Lu3-HW2${V|ttSiS98806khV=$)8GpZ_c>1@t!k(Ck0b2h>& zh~t9?OEUK-Zg`S}>~(R`PRHHl&v>g}f-GM|uxq3m@*dc5F}iM*`A6e)4|35w71HOK zRVi3I@iei@hZBe4b<0r{&A?ViFPz2QM2B1=1WWT+pk5-e6>{~4oY8=*=eQ8Z)YlP$Sf zVt#Y;VSN3|@3Z<2D(Wq#=!H?6?FUQG%oUY8VPG5f5#njDuxE^H-#a z3HR_g=2giCFC?+s6_8Gv`4Oqm37rjW8HtZtm43Z^rz2FVl3kLZAAs=eFlup$T5> zqlPU$LB4l|PL9`l2sO|ppOsa~E@-&;;Hv>``2+fv@W3PCJ2{lB> zQo#Z(F6LcoiT%oas}u0!4KV?)W5S4U-8*-YZgm$vlYF)mo7E79AsrZz*T;NbS&cZ_ znNd0lrJlO0pO+maU(TTVD(B5?Nk@xqODIrc!jN@4app(9ff0d3D79@Z+)?P;vn1Bq@;48ItH$|F(8y-3Tc&i=dlBK)Yg#6`v)Nysxt zGE<534E+f04^VkEp5d{3hr*EMoj30?t+W_7Yi@>BS9aKbtBA1}v1kc&LS!~$Y948i zCaSnZ{0fmPq0qKLgjL<~h`qRxUblNW^^@8+wHseS-B2Ia+W5TX z7PK55cSE~9Lp9;n$7i}hlgLyo+I)iE7-o+N=8#t`VYTVJnr6`>R5KjhQ(J^;7g2B_ z8?n~5l&i>(RSdp8jKXn5-xk@;>((OFO8_iTrIik+%^1sTp6p(oPw}HV9k%%de#Wyq z-Kuw|4Q2=TS+ZIKj>gsD=T11MS(6+U5n#brK7-eDUI~UEp8IKoMI_V z&@5$@QuWF(XExx*DmF>fcHjKacoO3oo7CGHW%6R1pugHgli!VQEIHBJzcW(1R-!gq zUzFcESddpT3u95Q_T(;oNosPlW~uIvfYA2jWx=<>{YVvr1u{`~S1OaVs2T-B!m24E zWHnKi@;*_I7ib9E8J|CYYg9ozoIk>QlmeCJt3^bz`XwQlG&#D0HuNOUG*IAjm;-n@ zZ#&N7Z zf9834qs6{>(G5oxuiY`F;!*suVoJJ#S4lH=QG^b4jjomC572-B-|{})G;bZB_9H2r ztiNGVtJY1XfUIm{xvUrgY{~MiBDS$#1dfy<5r(uKM$KX3EyN=OeW~XXW1JSj0>aGK z%E+s*+5DQ|VYu5QN9-aj_TpwzJ-38~ho+F^TSUq;2Qhx-U`Tn}BaGM>;|C=!&)JF! z``@#%m&3B|NN^}+sl#ByPJxjR8qHMlBwLMZeq@Y^BLM%)QnAUrLOL4kChehe{mQ$E zl}4NL>?dtK?)64FIzljafm{9oY(n(cBJ-lD{8<@pIfW|F$CX(sObAckw=P-ha-b1+ zY^$c!C_E5Rz6gUp-CdvDg=GrENseA)MilcsyfXgtWCj8e0^FNNO6e~uC!FRvM%fLu z6TPCpqjxdC92gX;D(I%7}pjL3WdLf^t>#%8$2E;Sloy5meLbdgb59y z>?VVv*)=zibR0eCgrl1wqaH#R9(t8%s=96ZIG4Vrxr){9aQU^E1AcQ)a3`N8uKEzH zS|ge8+U$~F{Fve!5rbgaeDd*iElE4#pPBxB|0!;qyVVV1;zr=KD$ znG{;q4mUH{;%k%ZXA8wOM$@_S%Ldhp+Q@&%7`Eykzd3r~C{!UyK=Y)w^q4N4Pt`J) z4J>1id{f||7H$4cECd4JZ()a%TbLl-22_8lG*Y58^sbjUMIMK)sv3!x=aA?4!${pJ z4^o^p_mn1>-ahZ}e{uKLL2XBCzb_SP6e#Yr1eeg_4#go5TuPAQ5(q9uYq%3USdjz* z1SfcDao6Bhv_OFZ#Y!o?dG~wIySwi>d*<9bb7t;2zdyptEa11+Gt1ZWe7@HcG=8Lq z3SkbW5pHIw%{f1dx`*t|aj35!Kcl(T^B?F1;7&sWXl#cPR!+J3 zyJ%*O-%ia>t(y~tJNu%6_gbE?0!E}$)`Iz>S!_tR{aj|zt($isIqpK^J=V~)tG1b= zzhv4rnYbA_S;%TA7%#xXhM0C@6$I!~H`CF+6G)KCh;Xz|y(GlD(yklYPB~(i7PYWp zAWxSc-qYVSWEByqMutBlU-(C*JQx%n=#JC#*v!JvPx2F$2upw7 zwzYL=QVQB&J;w`)?C3V|L(M~$11euM7Z0EZ83}N42C^GaN3?1`>xMq-!=^R zDn{u(Fx-=+(9S`uN)Bt@1DU6!!8v}55inFSpin0`eEKUHgA z$FZ!Qgv2X^%ZN|J{r$0f?&EAd11%Zl}s80udQIk!EchWM(chM>7Sx`OlRqknn z!E)!Cuq0k5e7rh!+81ME!PhFq`p;gI?w8K5&t_P?^xaCjV=;MLDg;j_5y(Q}%`|%#cMn>VJBRl39#oN2T5-N)1M10Y9#Raq z>pYLLo6jT&$#xmd@+L&He${-Peh$5W6~Fm5Pyzg9rFzk%Nb^^oa(iDms8m9vXqj># zoJFNMekY0e1cQJUc*{LX$1hu5pE_HJHKE=BpZ}NHNHaA5B26hei>2Re>-I4}9n7bD zU#tSQMsoNI`p)BE$rb28$rE{J)HGt-?0xZHFMY17`7bmoVjC;cTdA*!j;sf|8mbTS zbX_MKou;}w!L8US76DuA5mrOuZVhyH(wr3RGuAWm{O-T7>w|9)zp^iWSJ04g71-=i zz#$@^FXE9c1eO^f-{8&#FbIqq;}P=pqQ0$BPW8n$I`sB8>X`Gp`+u`KwYTsG+e${z zaGu1WuJ@Ww6B!gcUkYUz&#TEjdJ8A1nJp|`#-fD@KWdj|5=PqZaN4H9uqiO7cCh!1 z{M?(dgaZ)x2fPHp`3{6YUc*HXcDKyNN3lSH**(dfiSRtdsY7Tb< zvjrRl{3M?=zb%CmZ;~s5iJjYxmcN-gy!80=T|iUrY8n}olwW@RSK(kho!A-s*|Yr* z8#O7-R6Oh=+DL}mJ2eBQB5)rWk+~VLHu$;U2ZCD$SAXB3yN$~F)1e(i23fqXRO6t( zBdOHVwDqZ;76kC-*G;PG*4B=F9j%zL0iTMMczC{6n|C`H%rm)Bw)%GnWpw>trHp&C zN%3@|rt@uG?BgtN5%(6&WI_2+C`%9C?QPdPCPt)W5R8#d`%R$P`L9?k%3m^bMCh*^ z^^S(7QzZ!|+-{o@tmtf3CXL?oS zMkZAp@85gz+gIFm9M$yEpO5x{e}By08yQur7hAV3e5_i5hzcdVy^8u#y6`%$Dnwp~ zUhlDz|H?q?;<6P`=;0TOSW^=HgL|j787CBJsmUH2BHerBk?4W+*{QfRN z+3MfU0kkwaYa7=|bt_BhvNilh%AOuN{lmpx3x-6OG81Lt1w$LH!xFmp;fkgV94c#P z^_w$#Hyr-ihyLYmA0}DCt)o{qnB_}sWEkhXkJFDZVECvLtzW6DrL`6D*<3BrWeGR>2!wgTat zfBD|-lVn-xY`z6{*l4twdw8^PdR0a{k7^X~gXF_l-Q&_!I;d&y$bp+6CeTimio;~; zqO$J;*kg`*)5gJQ_ffyOD@r+E`kgSc%M)HFIK=}>*j}`nEejQQi<=HAqo9OwiChZf zon}LQ_9_(t9>qk;b*s<-;2k`PDJ0xd)|voDL*Gcr3j_Ni)Aw{E0Q5sRgy96pX&SGt zlF~t$E)khBAv_t7kYd!_OPKbGTN{XV4~tHAGU}HhSw$fXm)I~TmD+}EH;W0Hj9%Z) z^*V5ZRYU5yL<@Oz$%_oj#uq^vdG>%@$2?*z*Z(*|PTZHO&0ha|$yxKZk5?&=BJNa!QzObW@HX6o z-e1NC_fvup_*Z+zkHUZBuB_Sq%2S@}Th`~a{YC8Td;8Z?OukRR+BZV(jJuDr*j4Q+ zp4u*26rbhz;9Ao;zED#mb&JP$j!UOZ$`!-zr}ihaoZly|nIMPaIX#OhaJ8YKbFxQc zocBoFi?YO%ZZ+StT4OzfCbF`BG)_wXfM_AMs0q zdD6f7fz15V8++5vk1QI62Idd85sJPO2>_P*VZ*Uv99*D4^+*M{~)v@fd zk`(PEE`o=$HR1QpQcsx7CtY-&n!2JL&4sa0;Q*Yf%1d)S6xMV-GY{?)nszXN0N{gX3uR)7Uzl7yoK3J? z5-yt_8)gLAL8E|8Rd%&7aSgi-UW2?PNjlfDzVrW3Nl3~h&L1B)f+pWDJ&CO0QH!6q zm!U$64ko}gVBma}LJ$~|$%eC6vW4Vvk-Gn5;{Mn0)|(Aq)47Fe-$w}Zk`o?wS2WAO zM7-c9x6-tdo_#a3=ogP8-EaPeKK3N|`Sst=mE?JUJ2>#Vs(8l9aW)3Ntp2*fXHJAM z(kjj5`(&Wt*cd)PE1VTG^Aq1Gn*0gj|7|64_ zn}5~&N2cNLHsqB@OaTkyn%#tjQ0_aWie-eCa$p7Z*~>L_mI&=k?=>@-UqP$i4+zr`t#QSi%Pr3`JRrqzUtf= z6p(r=MDBE)lN3tZ@EoHcV-PuY#`KK6XO0IzbS=6mJ-*njAhm36#w+$!f7Ik`1M0ikv)It*g1>SS{WU zDW-SLWIh(8VZx40kWZ?C9=&5jas%&0HrD@iC8zNmI& zv(wYMcDk4o80v5-^+70+Oy=pcvtcc8v>IwaiT6NqhQtcPu|Q6-QP}j`(+Jt(ix&9z zoBRKF1;?1N$$FaUoqgmSER6HEEgaI7T>xD?5`EgC_?5l;sq@0@3+69reMAMe~U zrF>z8xR4#sdcE7Pe0dv?-DHmCV1=X)BfUo&ljt0@%4!W>MP6w^e8F5} z&HHd6LG?4er0g&J%uBT9XJE%sMNk8%WMhCL%Fjv7YItj|xVk2QW?N|QP1^H+*6+Vk zcf=x%vqTBe{9C@+C#Qng7xuh7AcG$x&1qR$6Af%}&RVFTF!XlN`_orTCkqxlv>|#7 zc@k(Qf(f`oPva!7_n;p~lzi?H#I&uFH@CAywYjWVPcv+RDjb^$A`Vk4X{ej`-c)y? zMtpy4pBe3^8KdARc@k!@(sU(W8kv#328}LV95D&7b`U>?X(ZU-LUkAr!!f}5W=`;X zQG`AO5L*&8AFOnAS_aSxhmbi}WQAtBP1~3mh*NDV)g719AdHsHw zdo3(fZgfdW5^nlTq7CfyOSifn5Q{E4=|t5b?7Xd0B8Z_foB3>YV=Bh6St6PVs+b)SvuI{G;N>YIxNM(8Zfmwf9X5los`WY7nk0l+yX9(jWT(c>}v*1;s*ESdjv(k_I=t97r?Xl^m3P&W;6 z0uq@#O$f+&k&h#aDZ;I)&#lyS_FLqAh`syqf2xw=OTppy;?5K2=(bgmWKhfWS^e+< z3;9MkZUO{UYm?Wm_3)aglsc1TP-`jFwO%;sEsd9M5Pj9(le}iKG$#l+6=8jxWkW{X zd`-9h%dMzz#^e`CGge!vK#x>OvI6GK9s<%Gxw-!4@IPQ^2LgC@;=h0Xr{-w}u9}V7 zP$xxg%>5%R^=;L~=e?(CInzW>e;2OJ8P4CWp1pX5uaZ)7UJ9&uGnHz? zN5c>V1VwfKDD-px|KTt!GX6gvwXJr0-LIW4Bsyz~7sfBVf9;y+5YSPW{#AeB#g7cq z0U#W_Bk-6J~Ii(-2@w0e>k2a?=;o}r1|so4Rtfo45;6dLFb^fbfE1(m1miUmW^EKH1`%ImWN30~y z5vq+P`;#M-6Y-Nh<1IR|1DCyeMa(50c5R%4Xt%LX+HZvOQY{|Eep%zUIo#@QJ&Kx- zBWZE(dao1d4RTicL1aRngkAK;NT%Cys4g-5V>Y9AeMy`v!wyxql6l`a)Wm@|PDsE% z>v+r4Qh4q!>+H_k%s`#*p5hHk-2Z0M;h)xd|4(y%663f9&N0;t*aur)quYBJk`%Cs zsgG?VKZU%3-q9w;0tMKoDc)hDlAZPLjkx4oGH0*Hl=;O37wta|>Hqo3^&z21Sgz@S z+ug;hwbW!4Wnb1sh!dk%@uR_RTLs)y#Y&=XS$cXS52?{txjc+LNj8K+_GgmxUFyh?&z1vv91m>$VuwC0{Q@nwK zLrUpAC#79&VNcLy5)mH34IW&r?JdHn^Kn-B(WSGktdoxZv!egcX2A5%ir#2?M(^^M ziG2Qjlk4>4GMXSS+)JV*9V7NVtahNT4NalvzrQN7E*a-#u<=}`Y>s&pReKXMeI1<``CNI z8=gXbPClxy>)hB7{_VL=MfsV59MutT+oNkN`%vTITgx;@*9ZIc5eCj5`C38}n{kDu zpW_E8JE%kHo-730J5Sj)_Qj;XA`-my@;9Uv6Y|8mIACYRf&^n>)c#07zs>u5CBZDg z1JlrK-k+~}TtE%(HM2EG)Ig&xL?jt=p7y)QZ4bz{D>wOcf!0BX=C5}<1O(k48Wb*j z3{||ZGDD^)ZbryQqxH;Xh(UYob)1LUkT@<-AdfDMt>fA2`&+I>Yn!eYwQfxU+@}Lk z6--rE_FomO;`;2RU>FW38Mrz(7(rRT&zq`pEBFlyqgXF}PHhDB%^z0oCTI36D@3r! zlNSf*xZ)+b`e)$g`pQ^0Po9$YtN*-yCL@` zUqWT|c~quL$7ly)DxCyZH1z~M_+o}#8x*1;rQy>s(?pNK7^NWDYhc_`8X{z=__f%7 zJvYp1+q3B{be>Jf^cXFcE_J^2nKcma85p3GO@YUGS^)b&YJL4RG7Z9lTA?5acev6k zeSRL~@ioI%{JPae1KVkfTH_+@As~lk3aa~(Es0%VXjJLxZ<7PvJ!VY!41xYNWsp@D zBjuniL*+x)xwKPz^YI6{`YZ_bPA5%u54CTRgNkc0rw=%j7+37msl4nWuvA~;Xb7#N z^wYVvJ}07v8y27p-yJdJswhm-^&5urf0X@(63>;OiCMuEY8-#hm{I&i%~J@q#B;xB zI*eYkZ^VijkuE^0U#5|&-8H21U6ZupP5X0^ACT4jLB%@%{jq&M5VWXq1jUe`XUX6g zG!|r1_WRl8gc0IEX6db(WO>ufRY+mTcg0Hrv4NX!g*w$!-#Ohr&5GJuWy21kp9u?1 zzW!zT`_#!3fB6y3wDb0KjAomV1a&xQ2QAZ$(M)i#hMH$w(z?um-l?+#mEbe#*@%pf zPY9mGB*;jp*SXRo(VjMOcL*UdB9}oI73{wok4;SQk+1jRj)mQt!@(w$qb zf$4t;!f$pFpf8GZV~dwEoV`travix&K%1Z>9ua;dSwBz;zoO9<+%*F4n)1%c>BKr2 zVGxW1GrrN)8GNc~sh;_RD5t^`&e*Yb9a7{6%0M1zR=S|9^cMDVn;v=?{c*9rF1SoJ zSDX7QW4!&uOTDN&ATga#afeEtAq^Rw@{HGN3|@=vSI?-Q+$NSEb=9qi-j|&JV3Ttb zriB4!JS!AUKg$cAf-}fNml0?X0O;R!OmLHOc*{09RhscNb*g>Nm?qBp(~)#36JaHz zy=jmeUVSM$OX_Ez(Zi<0`i6{$&h=IP;(^5Ww^D0+`oSprCUWE*Ay-gr`U0Ee4tqepY3vE5K35Ri&!Qf_ z{udbzfiopf$G)9X^&l1~eGwjN9A+;eHOo0WPZ<0O=+(QmDEGcVGV=W9Pn>H)7azz3 zQHV#frxgM3vFfIw0=a=-#9VFy2bTU2cv|OLJo`gH=63&KJEYOkXXhvuGKMF45?JXvlH?G_YRF-+dj{=~L zH40Sr7QZ6_0dnT$g9FoYMo80P?hll?&Jq@ypoiDM312L6m7KsbEk=k>%$aaYl2dJH z5{}=9va;ZP@oL6K|A>{CH^R69+Ulf~;bD=`!{EwYpVrw28nkoN)7INmk5gFBdqC~# zqUqAb09b0?hUsCxp)jC#3n}8?^jNEb(^+^cmI{K1^kuegd4J(uRycxR_6}7qyvU3E zVWUxLuz{mb+#$%&ptDh8tvtZVfj9sCZj9MvHovi~nse zV)${KVQ&^L{s?Aq!aZW4R>op^-NYG8zHa%prLM@4!>)Y3Hr;yQWJLn0@2tRu) z|B2h}F0=EFcv4zG;B0$z%9MGEWcj@9oX9{ecrY!l#=qFGhbyIkV@1QZLc2cGGbu)K zefa(F`tpP8?n_IdN3?zkqNI7(ssH%?pXvqlS*t=L{5I~!f=C-H6|Rwn5{&7XZ3Qct zjd*f!w@A@m@@QR zebo&AN2R*GH?y?=7!8RnaTjh}xZRXy|IH5P2&!|sgq!28%7W4H%&YTi>EZ4s-q5J{NVVATLcHEe*V`<5`qVrtsJSWzx=Aela_SC*1*aW-QvQdGIcXKWs6Is=BJ5;kyoOc$wI zn_@&&3V{Z;Db%RIp*uUDj9bIj*-sNPZLrmHWLrJy7U=#G5ti8_cn4*gUGs#nf*%be z^s=Di9|rsDgCy;HOUbk{@xr_2W1FoF*B~E z%FY3_{zKgMN_^hm`R{&J&4LG%i;;5#I`2NKFm8<@5zk2#k_yzm!~A_8Q&^!1v1W6U8&+2?1H}>Or5zk26xu$qE`!Y18s9s2> z$Q8hpq692PV;Ykck_E@{0>b%8duK|nPZNF}io(i-zZ{;d?rR=6MHh(f54agpOfqnG zwxrmG+eVlbM9FtY0+~=>tlA-?W-DEJoxx54X7}O;Rr~V zY#TBxBO+i)s7ZBsLhu!sVzJrgW^df(q6f%G3!@L)W6ibEFz3RB2q;p>N0)^knP1>!-RN)YB!8E>GU^T5r~i{JCI?@9tguhw%p zBr`Rj^u}Zd7VQ3a+J;<$3+oFOR^}B5=pLY zKn{i=5>+oC_Gg4nepCt1D`bF8Plr)G{^zEGg8Du4uSURKwwMCJJvusqR^iK7o^qA1 z`Wtuw@3az+yJ*@oC6n~8OXdwaR?b3-`abfHZ45YHz5!YO)zJPkd5fZpPq@K@hf>Yw zE0k>N#(Icw2Kfx#8O~9dE6Pd6*C33WL$whPJGpJG%%AGKBw{L}QH5Fh9R#>|IXvg&ko$%*;}I0s40B{$!axOG)2X z517sHW3K|cYZiZacxJ;oi=HX4*zAq7J}TwghUQRY>e9h|*{pEtIG%�EOsdD7nFU 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

eiGtcfn+&Cs zR?+4LfFnr1Tr;;*RgJ5hTH9$7-6;p*aBqn3vPME!U{JH@;?n4>BazpkP8?e}5>`YB zT52=i9)vKAxfl_BurIs{yaTu1xF=~#1_as%jDnswy502BLC>B6vMc-sOaY>&Mff)bCa;3VpBfqg z9o*+^vKpZT)hrmPsppOqkR~H+y{j)w=@e*#5kPec-)QY@(sbi`Lp>E!a&E6Lwa8Si z=$hzu4vLXsZm3bKR#9bTbU%Z}+tN0U)|?%EU&DRHm9)lpr2~5KljYXkh))*}T7~P~ z>g^>ECKQN}v=4pEl=)4ab>i=w&hjvEqUyGSXtqP6l^|!;EZz#CH%cx_<)%;_J7m*% z0>NV8&~{IA>NW1B<&b>?|7Po>jwmBCh;s5G zjk`K^|BS(hKHkw&(TwpkoRtn>}?@E1bnNtpJJxpy-oMC24~5@5bOH9}y}kes(1muyQ6I zG9%3ssOS}7)(sh);N)>k7V|W_wRUaaCQQMp09~if!9N7h0%AcFs0X52@8DXvesjaX zOCD5?6zjd>LM|TeE>K5`mK0fR!guE4gnKF>be3eYvPBv2EA2mWh575Q6!jF)0 z`^|A#5T-IHxKsAmo`9j9j62nJ=!~Oeyr{4s0M)v?igEvpfyLsZ$CJF{zDPMT+F{$Sg*J0Bye1uA(~g1Fz!I>$|QBT{%qza*mmcifomeN)>(-XEae4 zDv-)tHZ$Q8XQ&Qf)cTm#W?EOLs8+dkGmeF#%D{%Wa+kWyR|plEWfdaRm0>%@E4!y5 zC>nMQmo5)NnYD7^^s_L=KJ9G_;YnNin=XP5;vIQQ+gmhLKtz^ol>?7Tl!d%?pjIKL zNnucKq3sGA&fo7sy|0VtA$nE5$&$=p|6tp-v~Sefr~U)a`q{;!OCA0eD!ZjoYdtQP z-pQUpgpE+X=D2&{%W#aQ3qGqFb{Iw0JUKN5%nD^@ns}C$weKz!PPEASVCtynV9K$a zbE0lGjyoSqs)i(bp0y9s7KpS&k+;*_>6^_?F$_s~u_GNngiVX{zqen=3JO5V<_z(_ zAqB&q`*EB{rOMu>?OTK32bEuSExKWPCue#}yT%HgC?_v#kWb!R4ZJ<$-)OayLmMaZ zdy+o_#XZZPrYho$62B%JxuwJc$l(fpiK~vZ>x_ssr%`)kwypl)aZS>;7MfTJ&6;C1 z%ACma{&=#EBG?DZl1~?2;njC@G{T*+C`yV%TXFzn%ix6--RE=o|HyMk7Ovq+G z`RHK&nw^1Sl18z7{d;6IOLl$0C=(NvJ&So?&#M=Z;0RV0@iF2%+Pem7pOAu5e;oA>Oi77WinAYR$hShi(ag0T6n=P>(c<6RxZIJj!)cv-)4l>d* zTTN2>;;_>{R+~Dh~4tg`niy}H<(k zc+6+x0=P~Uxfd*CKWu3EH7?8IbKsg?qc_Y*?fLYzYK`9i%X4xn2rPbhR3jpXgl_>X75Gz&RN|QY#C$irn|pfx#Pr zfyavM6AV86giM{b+IFB;Uy1InR`vpoV^H8R^8p{s&*{Lxj=c}muA(L=wR$y&TNaXx zi}?lVPA?7YeG@L|L-sGT)Cg<}vg#zc2*KaM#7dKGCR8)BK+G)h`v~^H!Qn#dUJKd; zy3$-8W>Vg9p*qI|vX<;;315PW#Euejfw|?6#*s@nQC~yn86qI4GDM3>Eh+y<1bN9n z;O7qKF#IAs%awRMrsp{~paVZy{Kw4Yl3s-;r`6@RMG-bS*I2*maJQ#}AhkNSCF#n+ zA9!wix}^^EJk(=A-Ia+UGkpw!YyoG4dL(v(pOeroEH`jgz|<}}9#+1H{{@xrAUAfZ z-0G#AXN1sqA+O+sA19~nTC8Ifv%e_c`ThG70y!^7l|HCV)Q8wus@m+pn6g)31tGQC z_s!Zoj($6{LFE`sL4nmf+r;oOslR+9UYRf~ot4+ja`!Uh_TIbk20`;c?ZRs}zd3+8 zuqK(4JTtBe+#e8?@h)@Ewm^=`@`Kz9jn7#s4^Byw#PJQ(x*Kw^4t6@0M$!%X*1Odo zGpmRKSaNAWRe3wq)b|fnx2rJ>j0hg3ZcDB|+z%HwqlIRfomPT|{d_yOk}|19GDWmy z#&u&;)&ZMIT<%=DFjVx7%e$NbJv*1Bf1Jk3$8-0Q!~IX(5Fn z1Q7B_*!c*6ORn9^QDZa(gi+Eql;wd(Y{c9#))YWIean&maUVrK%9yo zF8--Gibg55iwD1$Wsm7ss+Xo8Xd1^k>l6rcD|{F&5LLk!)IH>15~!q#LB!GbI$CrP zOLyb5z{)0>HA#cC8IR(=Ez>=453L_zDhu4QWT4cTvM7s-6xT28wW!lI2 zRO_6OPf>!}`=f<5E-+^3ys@h&q4;d?;N4*v#{*Q5W{P_wpp7-_&Ph}SXvj;gTkB1= zb}a$ge%kGiSV!o-<6u*)?y=h2e*BljS)^+zBcwx!6R}nrxAsv8wqi4%LBG>iIVLz^ z`3Ih*HphB#A8Pb*A<55aJqs#4AA3Ja5M${cI$u%M=>Y&B}62sxzI9 zjf`DqprV62x!CdR8FVHs?^z5nEcEfxsm^h6DV;U%5)Z3xq=fOdOA|B2kUj&1$A^AS zU-RzRgLTXwX^PWD>SAZvJ>JuxcaZP6N1qv}i#x~;Trt49xdp$Rf{#E6#$f+^SfFK9 zJ04|TS09-zKyt*sj(QrLPgg4hJ7<`;xXshwEp5`8B~r^ayXa7iO^k5*C|dJde0wBl z!ww2qccTym>jg;5L-CN}Qc>7L)ZE<;T|I%WL_SaYwLbGPu?mU{Znh=cM(+{ZZ=8mS zpxlq<2f2juqJ(`xw76K-fLBF}B3u`tS7j40>}XlUTW*!!Q1)x^SN)c0#W~sVL(EV$ zg?-Bu0zzA`gP@k0rD!O*HLf5Z)BEs+?Qi);n^@BxWQR!&?}z&#t`&TzU&Z50=nsV; zv!g;nyv;*cUUYO`<%$ci2%s;oMKBO39IAlckk(v{k|WxaheJwDiolyiV0f3zhj0#Z zo?IQSo7Z~CrLlVHi2J+GN2_yCfUTOWgsl|SkF5OOTFy-zGSV{-Swl$m;0y}3q5Rm8 zUFC9b_5lT-neetx4=4=dQ=-ZTav7!~?H_Gdafo#Ck;tu#T2o3X< z_&GYb`BLhQCzOjOwzbWFPIMhQ2s_3QuR3se0K{Z+LB>79tF7kYbDSAus#f5yDXKJo zmHxi=UsXlo-w77UuKXQm2wEeZ!`FqD(@DHPx@KH=kiF13R9vzZhtj==OiI`9a3)&l zHx%w>LkFDv$qgDgzCJ~&*n0070KB{G(JW2z%S>OC-gUXEdGfM9R@7{*(;#lo<2B0; zLb0L0ywCU-Ua4qXvYQ?=nC%1M<;BLlaULO>4}X!wm37DjZhf0wIXpd><}>(y+^h$4 zSJ4P2C!><>RaQ?b4EcI9^ip9ovnNsn0oVOMvhJlWmNS!8dx_ zdz18|r-l7_t2j{|`al>Sz6Q^qUHF`(&5`}}W%jo()U`B)LJcB9N2hQu3Nl%n&dJ|v z@AX{WjpP<_=e7|x646tC>rYFOg~8jJ&dTJi_jEW#`Z<(qgJIj-lh@5%Q>)-~ex-dq zvRiH6v)u7XOU7Bo{Q&Nnva9=0k&q2f?(CWG3=-mD|B?E8p$z zj`i4}jCZ$jFd2B)IU@wwU*whw#nE zvLkBJfs^0$@$^NBr|-nA)YBl8^Q@L+0-hf|T#48vRjhKq==PA|u?@$q8B4zOmM@At zTK?pcQ@Qaxle|o8PTzt(;w#_XQ3OnLJg>pgRje|yrQtRTHuSQxcQd(<-g157-7~q& zrIO7%;k@g>8sCVWT^Upz!(VnUx?%R7ISu2|hm?t@W3{|l;x)8}HjPJgK10Cd zmpU4Jz02nI@~gywlIU9xzC;Bz*}rHqOSg^}DIlL2!4@g5(?lEGTvziN5-o8gonC_d zs7?E?r5`!ZUM1Em8B!os0QG)Fm@P~D4FW06D|A6(6x-aCvjiKEmpcXfRlj!YE50iO>;3P7S%`k;BX{I``r zjz~&IB)bSypKcfyf3Z?s&hF72!G}W{(a$ja;H7#E3y<+1=&Hi8A84+I$>@`+|FBPjKl-(=ds5&)=7dj(#*&PYcv~=BfyDEsN$^brBgZ!8l=w z0a@z1)#Z0Ar^ zGlu|9p{!uubHPMJXuP8mN!ql}xsNXq1VwE|NAwwSemD*ic{I5XlpY>Efv`beE!>^r9*8F*U?V2M zdi&{9#Nr}OU@5f0$Xi-Nz-3)i-#I}m0zRbA%TrBrb5_@J*NuJhH;%jf8$m+Xhi`OU zCf2eth3T~Abe3uw=^}Dh81bA!j@xi33_^S~6+dqPy|b5Z?aB%) z<@P9{wQibo^E0*5`c_*G*2G#IYL+&n>~8+X!E(QG`DxtgsN<_5?0oLa)S1zsMX zUI|(5Ki1v4Y90vQnN~+XHjoGm24T_xw18ct;GMcAMNg!FHX?9Ko9XWT`s0G9h5a-| zXOb5B-iopWTQWFJA|llgGsO12wquf^Mkehz6PK=sPk2LFxD4eR_PxX|i6}!Dco3^N znIc-3%o}evu56d9WIm?N@0M&AQQaT#2hm?f2Wh8XWz;}%^B~j$d2>j` zfXHKy!=_mAb%H7vv?L{41E5R`KJTZ9^#GniSD&|hVam~|g>&h#aNilqAy&Ih+4e|Y zywMQc-q`BPF)=7y+B}RI&M1(=3i;zOqIIq;zDR9dM=m8FE*;%sm4f3mqv{#59UpCh zaazhWiQ!sIDHm8X8K~&QSO5x+1nzN@j#LD6_i3=fMfbN=+WGy6x?s1?{DJft?0`jq zl#80iV~r>ARk^fwYNSe`%#7=xA6n*z#=CIN&a5L!h}MCdt zsgCJ7@+{A43l0G;Jk(&AE^g?acnXMA@ht$sTlsXs=aJ(-L4zP2@8Lx?>5_MrJG0Qc zt_(kz{CCwGkO$q8^$`nFN2|d+)YML+o`90&Qpu$`Gjm0KFX{kM{y%lUzx^*&&woqX z?>7`jDep(Luy&5+o1{d4;dslhq1rN(#^O7G)2u9^=mPa`Ns84GH8}3N3@&cTtPq3* zN463G=oUT3jzU_R>hfg)u7T<#0uJOYK~Qf0@&_c1cFx7}Df#k5sl?2Z(xN64=6pTU z+%p|bc8uAHMB0RH$QqZ}?P_OMuJ?KgW=WZRPg0-6KLhVY+{=_&u0)J+jRG+6GP^dn zKQJ4`G>lcVzH`gw-Sb*Y{$%bf?a%@3*(41eujnBs=MEiilZHhYWYHFTMK#Q2Sct*o zD{)e!EaR2;BK zs^r`*NL7m^t51`M4oaTQwa!9E6*?zX>iE;)0^r!E&;LPF@ZYyAsU~mru9vCBo~{bt z`8KxB5y$ytssM+^&91S=V&#)&JrJPeBW_8_VQ@@GRSZ4Rny^)Bhs~+UZ(C6F@T4Zt z5@=_<2h3mKOgtt*yBYNZ1Q@(eY{=J{yC@8t8Q%3R4N_fVGhIKogJa_ z8}K?Vg2_w4cI>-a>`J%G`1PEfSxzzDj`JD^9cqxcI=FV>&twXudv@=P9+w?*Y3RAH zG0np;;S>kwK36t-6>D0!b6wJyq%^CYj&@RSk>pUkk>tF%*BdlwDYhrxpr9>!TYDo> zyfvMtNVR2H2 z!zkuKJ8xc8fRt#!@z}*6`@rjfKgWqAFdO9|Hic*iRVIg)pjRE`iz; z;B%6q21X37$7R!J#92XT2A0F|3IRd@j8UBqd|;Umf;QmH z)#wO(QS@-YMrvx-(afiRdyMe6j1Yt#Y-SQ@{*7<-Ddbi{E24O%<&IZNPo-ne(Z|GZ zskHJX0jQy8KYX~~BNdXvv_NNq&>IoH;^eHK$zH>PP)j52P5jAWA03C~&*4@XzuvR# zsK93}b_MKeY-#lM`^Pv-=&H6cpfgYhRRGZe5qjR~4`2md&T5vTwY8Zz$+M5M8PmBu z^?Lh(&%aDL>8i>K_Q>KNRjuD`0%dn$ey{>M1VVzAw}eGQ+DpQ#0w) zx-||V?Qcd%40lJg(Tm8Hmw30TH#z{9FqEI|G-dny$7_}&cx0!U8PS+)k$j~_TqHqP zQatXpepPupc{6`fxDh+TdtTb3Z4y-!1>TA0kMR)FjhelvpoXCx(NXeS9nZ;7lM`s3M+3=9L_Aek|YH zYDq4hTAow_kaRlukXw?1DqTd;^Ha6$)K;tBXXrr!GRs&ENObs_%o-v*CY{n_Cp>9U z22_U#P40g63!MCD%ob*Hw>{p?_6p%{Z?od2{?oj?y;qll?s5H0b|J|j4`xLsSXop} z8?cUhhUb`d(ZRQ|>3G9{JKA&nPL=>%gZix6bJoqyPru@M`NFkdpZ^no&LA(9%aPHz zJ(d)9fw`T|f1j(WQpO5(eR11MAB(3A5vK1eCtgzdnG7kQToIeSI~E|>tBD#U_+wbu zR0iQ7dd-};xDDnj2YC|Q^PEK#YhNvGFq_CkMa`~x+GMeK&%)1OP9;NxU~5{Qq>uJ2 zMZ{ngahG{p$F%s2DXY#3lHe{kF7(_!H^-;>P0n&s!{%ccgeBcxP{m+us%(w=7Dh7D z%Fx?#&n9@i&b+`{UGuEOw%v3h{7G67aA_{>ArxX_-d~M%5=F0>JSmYDvmmqo^gjXI zfAAQm_96WQ`q?NR3=aa75J1CZ*L}W1)@5CeWRMl!q`0B73;Q0-rVSe7c`2Tc#bSNw z3$lksokp&7QI*VAiv%z{U1%F_uNQC>5?*prwA;S(l#tydkI}=+EY@Yh( z`Ih<0YAR}xoiXwIwf4gg3@^;+`SNM{isTzEU*`nW5e79#*;7^RWb^3g2d}A9p5|TD zbiUD&{_2|Cv&bOB@p$>3Vi(QKvl#=DSR#;xAgR={DKwyEDmL@6vYL7->*~jm7=dwI z#_bAWXX8TSx~0U+Q2u(jQmckJ7Nmz1(q4oEnVD(gklHin!qr-wV8&GzP8)8F6W4j7 zToo|0=67~CXq|H!q%d z;T1Byn^1!Kenj{2< zk%A$7MBAW9L*|8XPpSk8w^#!i+5j;7L`QUY*ZTnDHmJL9pGmhAU4BWxyI7_Mvhvt9 zQbc)H)mh|`3q*5|eoo!)o|RmFMs|)-?HDwStGnSly<7uKYW2@~=iH30^Ltf{*yj@% zMs7i$?NdaDwVXx>7xfmFM!vOH+4QYwHZ!)qpD&bK_}0euN{XiW&6eV}xyx$-P1eIY zWsds(LYb6Xg5LXR^&F3$IARwX8VM&ALFm#KKG z6{q9;8DI zuAC3y|17YBe4K|6iXG8*bNk-0Xf?p<@=ms}h%0JpZhuy@Ss{Z?UYfhMvgk#xN~VvZ zdGAV~wEkRMk$bjz?b|czzKjl4>xl`q21v_PA1J9nX-g5Ov7;Sw0@b9wV+es)>fCWt zn%Jf_rIK}7t%bUZiQdC#(6#qxCEm#hAI$qu8m*`$^<~tfA=qWsuCk`8w!p%H^kzK6 z_b~p8zs%60TapjZG_5dY)%n|5nde_|cAb?2a`J0sK(Ir8%K>XAFzCV+s~IuN)CG;e zfU2n@)33QjJ%VKF&42kJXGpQV&&?$8W6^i|v*=Td2M(#stHi#*ZL7E8dYjxzO=e#n zyx23OeRdYQlx62o`$FjpK7^cIvO9Wb+o9>8Z2Gp&Z5p?H|G2EF;@wxq-vqq-BqS&B zS-ZqnVW&=pIsx#6El%X>L|gpOzbESU#E6`j#uMM-#A7;fIZqtvlSt$wzWUFN8ZV29 zF*>IhP8dDAE;R2wsA2cihjJ^e$!4~LR62wvR9yJUNP?CxBgH3NJ%~{QxL^+IxX9~M zlczb1U!iL`pVa@sC!YoSrQykh3tx2!f=dUaPCD(qLH#GQ#%Z&6=3EGuxIa0C1zD=s zy#36n(O@jsBQ-o}fHKi60FAGB5}S1GK4o2-8S&c;n#uiWDn(!~;pswT46w^*S9Gd5 z^KZoNXiXA6?zf1ENOU3N!ik6i>E_@OVD3|J0^>W~72MQ@K_v{a-ELEs3S0Jy79TZU zh|NxYxv*1E?{FQ@5PWsOEWoFgHLGyQ)S;A-p&)Dw8GAEtuP+7BeUQF=(;rPptKy&E z9@Iefr`kn%LbVxZ`XdF0%(&iBytl3!NK@6rp;TRRcoK4WAT%_LGhAZjN1=mmqleI= z{N^Fev+k;c{#9wC6eA5smP=Rg3dDk7@WHnMHX7ErzUX$cbMbb^@856EkY>Xt&l{CL zpME|+wRlziMxL?>O!49O)uvmojrxS{sstr0kxpGGzQVz%nScq`z1P%BEF|8HrpcWj z^ue32RO|?@Sx<$DaBel^tmBVz`Z14{lflaJr1sZn#)Ut#n(Cnmu5KF&%4okEUsRH) z4=fD#h~N7cL2s|>7F}hxB1lr7@34|3G}xUqI|1Z`Lr!GjL^+&j%m4d!VPF+Kw|mLS zm`~6h?lrs>hB7K1l@ja7E>_28mrIoQq(B|KVlw+uSK;zD@+f7KG^IJqj*aX!hSJ@M z)uw=tm=~AX$89A_-C%3_jXmhemF=N=nqs693wKyV>Zh%)TxJyxj0u~EAQxzVVabIf zy-!8y-R|H{S2!9Uo5+5smUt@d0RO5$Ni4A3UnXQvfcRG0yQy+|N!oT^^FjYop#8f+ zL|bRfQLavCu-^$Du2IK{JlyC8Ur#3nQyXa!V&&{etKTR{thuV78UfmDs;GhI*0EsctMevI(3?BkE%n5GDm!<}XCr z3icq?mFok%LG~(+u$kH_mXEd6WgN3r7GkX(wA3NH8Q-T_pYZJ_lmVSe@&|3HpoI%H zc5}kcnSkn<&kkU`qynyG&2itRT=BPV?#x`Zeh?`r9LC+ap@jRxk(W4$~s4 zSt2JS)H!hz`63^X2OH(ABx-uTUZ*O5rIxs!cp)M`F zA#U<-P3MPieKMI}>HSxPIzi^Y@QkW}^_tPwhqJYGAnZ^P`TZ$~v>G0b6P21&$&f6i z+DB?Ouz!7q8WQ}-mDvSpu#f5VPfV{dVIhi{0A&woCd&gT%2fv&vO6zdxRdl#fT_{` zoi~|e(3O%y5*I_h4_7krR1w>}&TTQ@3&;4}N?^PENQ8Q;v|!`WBY86CUt0mNc4FEN z_?U%DixO|Bbci7Wvh3&YaxPgI%wIU~O*RhQB}J3zpmuXq-wXC`rhfZ}9Ero<4c+B` zj7+h3s6aTA>pFS>8Yz+?03WJ z$An-1KCa~R^PL~-ehEsGROXU`@2=fz{(ZXK(C!D6Ni<&%4+&$!^YR zp727LwTa&Jjkul$-P)(kqD&DA?18GqzV#QJHS3pa_oeMer!b9rMWynjnoSHtS)b=w zBtIN>Jv$1qd-A$Cy_9$MwoTbp`^(ut&fHrIAH+aA^7G5g#K0}W(Mhd-1QFDNoHRIh%o1X5_$j5Xmkl* ziV@YXb5F*FIULd|BvjPGWavgx2^v7c>Yhn%bLS&%EVVB`_@q{64oJdP%-%? z_=kv|`JGXELs^9%{9>69W|vTA+qU%&4}OR!)w^+tH!AipyhOh4xKj4cu3W$ekIdJa zzlo&5pGA`Us!9xX@?rBYDW$xa-?K}3()v!M3xgr9I(-JV#fmcv5{Vlj`-r6^eMyCz z3*u3k*&u}!zZ~74g2f7hd_o+(1+A$tc&eDPRpyAZzB>D@_#uJSn>Y8_|4zFzc4ZmA zD2J0~zX5jw$T3bj0ptXb{{=dv=CYYPf;_Tst^qBm6JB`E;rwBK2r4Cv4A>Qp~V$<@=B~wrN`vCMUTP(- z6Xea~Twq+%XSQYjF)^c~4{;fqPr^0AZ|95aK18zG%QJ6}ls3eUN^(qRHr{=Yyy9N} zz@$^!p0$M#qUWj>@M?#QthDu?T>t5-Fjz&OqTvJP>tN7~I6Y2@kjfgqmG)rz>AALu zZKD#a(Q^il$m?LMF^OBfM$jYH}TLQ|6B<6=GuMXlf8I66SR z^4~cxPg_9a^4P)`-RTJ>pa_(EO&R}53)C9w-?UnbLU5t>!E4Y}8tEAU>jH`dSdHa;Jxu}N^Wu<8EwZy~yTm7K- z4;I|8)@yIl^bF78GiG)9tz`$u$S&e<|GujK+h0y~Q1{AOJ8V8o?vv|*D|>2nCqM~q z3GvI!q&%otIazbZmb!6|(QABF{yN;jFSj>!vdAyEYmDr?MNa5H35{Z+{~~5Ee|T~T z#^ZO@r0Fpa{(ISAgUgYUcXP`^ImXO?$*x|#6NtzpQg(=% Date: Fri, 13 Feb 2026 23:13:33 +0800 Subject: [PATCH 06/51] docs: add Crazyrouter as sponsor across all READMEs --- README.md | 5 +++++ README_JA.md | 5 +++++ README_ZH.md | 5 +++++ assets/partners/logos/crazyrouter.jpg | Bin 0 -> 4427 bytes 4 files changed, 15 insertions(+) create mode 100644 assets/partners/logos/crazyrouter.jpg diff --git a/README.md b/README.md index 4553aa825..91c4a4524 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,11 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric Thanks to AICoding.sh for sponsoring this project! AICoding.sh — Global AI Model API Relay Service at Unbeatable Prices! Claude Code at 19% of original price, GPT at just 1%! Trusted by hundreds of enterprises for cost-effective AI services. Supports Claude Code, GPT, Gemini and major domestic models, with enterprise-grade high concurrency, fast invoicing, and 24/7 dedicated technical support. CC Switch users who register via this link get 10% off their first top-up! + +AICoding +Thanks to Crazyrouter for sponsoring this project! Crazyrouter is a high-performance AI API aggregation platform — one API key for 300+ models including Claude Code, Codex, Gemini CLI, GPT, and more. All through a single OpenAI-compatible endpoint with zero code changes. Features include auto-failover, smart routing, unlimited concurrency, and global low-latency access. Register here to get started. + + ## Screenshots diff --git a/README_JA.md b/README_JA.md index c3a2a80b2..74c77fc06 100644 --- a/README_JA.md +++ b/README_JA.md @@ -60,6 +60,11 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% / AICoding.sh のご支援に感謝します!AICoding.sh —— グローバル AI モデル API 超お得な中継サービス!Claude Code 81% オフ、GPT 99% オフ!数百社の企業に高コストパフォーマンスの AI サービスを提供。Claude Code、GPT、Gemini および国内主要モデルに対応、エンタープライズ級の高同時接続、迅速な請求書発行、24 時間年中無休の専属テクニカルサポート。こちらのリンクから登録した CC Switch ユーザーは、初回チャージ 10% オフ! + +AICoding +Crazyrouter のご支援に感謝します!Crazyrouter は高性能 AI API アグリゲーションプラットフォームです。1 つの API キーで Claude Code、Codex、Gemini CLI、GPT など 300 以上のモデルにアクセス可能。OpenAI 互換エンドポイントでコード変更不要。自動フェイルオーバー、スマートルーティング、無制限同時接続、グローバル低遅延アクセスに対応。こちらから登録してすぐにご利用いただけます。 + + ## スクリーンショット diff --git a/README_ZH.md b/README_ZH.md index e762dc6ec..446c567ab 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -61,6 +61,11 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更 感谢 AICoding.sh 赞助了本项目!AICoding.sh —— 全球大模型 API 超值中转服务!Claude Code 1.9 折,GPT 0.1 折,已为数百家企业提供高性价比 AI 服务。支持 Claude Code、GPT、Gemini 及国内主流模型,企业级高并发、极速开票、7×24 专属技术支持,通过此链接 注册的 CC Switch 用户,首充可享受九折优惠! + +AICoding +感谢 Crazyrouter 赞助了本项目!Crazyrouter 是一个高性能 AI API 聚合平台——一个 API Key 即可访问 300+ 模型,包括 Claude Code、Codex、Gemini CLI、GPT 等。通过单一 OpenAI 兼容端点实现零代码改动接入。支持自动故障转移、智能路由、无限并发和全球低延迟访问。点击这里注册即可开始使用! + + ## 界面预览 diff --git a/assets/partners/logos/crazyrouter.jpg b/assets/partners/logos/crazyrouter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c3f7b110a8de0bd77e1afd17879c0c53ece686fa GIT binary patch literal 4427 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 literal 0 HcmV?d00001 From 6098fa7536281b417e8e8fec85643996f1797507 Mon Sep 17 00:00:00 2001 From: clx <75774737+clx20000410@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:24:24 +0800 Subject: [PATCH 07/51] Webdav (#923) * feat: WebDAV backup/restore - Add WebDAV test/backup/restore commands and settings\n- Fix ja i18n missing keys; decode PROPFIND href as UTF-8\n- Stabilize Windows prompt auto-import tests via CC_SWITCH_TEST_HOME * chore: format and minor cleanups * fix: update build config * feat(webdav): unify sync UX and hardening fixes * fix(webdav): harden sync flow and stabilize sync UX/tests * fix(webdav): add resource limits to skills.zip extraction Prevent zip bomb / resource exhaustion by enforcing: - MAX_EXTRACT_ENTRIES (10,000 files) - MAX_EXTRACT_BYTES (512 MB cumulative) * refactor(webdav): drop deviceId and display deviceName only --------- Co-authored-by: small-lovely-cat <77799160+small-lovely-cat@users.noreply.github.com> Co-authored-by: saladday <1203511142@qq.com> --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/commands/import_export.rs | 29 +- src-tauri/src/commands/mod.rs | 3 + src-tauri/src/commands/settings.rs | 68 +- src-tauri/src/commands/sync_support.rs | 97 +++ src-tauri/src/commands/webdav_sync.rs | 357 ++++++++ src-tauri/src/database/backup.rs | 15 +- src-tauri/src/lib.rs | 5 + src-tauri/src/services/mod.rs | 2 + src-tauri/src/services/webdav.rs | 507 ++++++++++++ src-tauri/src/services/webdav_sync.rs | 649 +++++++++++++++ src-tauri/src/services/webdav_sync/archive.rs | 346 ++++++++ src-tauri/src/settings.rs | 183 ++++- src/components/settings/SettingsPage.tsx | 6 + src/components/settings/WebdavSyncSection.tsx | 775 ++++++++++++++++++ src/hooks/useSettings.ts | 6 +- src/i18n/locales/en.json | 71 ++ src/i18n/locales/ja.json | 71 ++ src/i18n/locales/zh.json | 71 ++ src/lib/api/settings.ts | 47 +- src/lib/schemas/settings.ts | 21 + src/types.ts | 33 + tests/components/SettingsDialog.test.tsx | 61 +- tests/components/WebdavSyncSection.test.tsx | 370 +++++++++ tests/integration/SettingsDialog.test.tsx | 1 + 26 files changed, 3755 insertions(+), 41 deletions(-) create mode 100644 src-tauri/src/commands/sync_support.rs create mode 100644 src-tauri/src/commands/webdav_sync.rs create mode 100644 src-tauri/src/services/webdav.rs create mode 100644 src-tauri/src/services/webdav_sync.rs create mode 100644 src-tauri/src/services/webdav_sync/archive.rs create mode 100644 src/components/settings/WebdavSyncSection.tsx create mode 100644 tests/components/WebdavSyncSection.test.tsx diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 49e1759f0..0950d9063 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -727,6 +727,7 @@ dependencies = [ "serde_json", "serde_yaml", "serial_test", + "sha2", "tauri", "tauri-build", "tauri-plugin-deep-link", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7346899a6..12e288837 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -61,6 +61,7 @@ rusqlite = { version = "0.31", features = ["bundled", "backup"] } indexmap = { version = "2", features = ["serde"] } rust_decimal = "1.33" uuid = { version = "1.11", features = ["v4"] } +sha2 = "0.10" [target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] tauri-plugin-single-instance = "2" diff --git a/src-tauri/src/commands/import_export.rs b/src-tauri/src/commands/import_export.rs index 5e944bdc9..c4482629d 100644 --- a/src-tauri/src/commands/import_export.rs +++ b/src-tauri/src/commands/import_export.rs @@ -5,10 +5,15 @@ use std::path::PathBuf; use tauri::State; use tauri_plugin_dialog::DialogExt; +use crate::commands::sync_support::{ + post_sync_warning_from_result, run_post_import_sync, success_payload_with_warning, +}; use crate::error::AppError; use crate::services::provider::ProviderService; use crate::store::AppState; +// ─── File import/export ────────────────────────────────────── + /// 导出数据库为 SQL 备份 #[tauri::command] pub async fn export_config_to_file( @@ -37,27 +42,15 @@ pub async fn import_config_from_file( state: State<'_, AppState>, ) -> Result { let db = state.db.clone(); - let db_for_state = db.clone(); + let db_for_sync = db.clone(); tauri::async_runtime::spawn_blocking(move || { let path_buf = PathBuf::from(&filePath); let backup_id = db.import_sql(&path_buf)?; - - // 导入后同步当前供应商到各自的 live 配置 - let app_state = AppState::new(db_for_state); - if let Err(err) = ProviderService::sync_current_to_live(&app_state) { - log::warn!("导入后同步 live 配置失败: {err}"); + let warning = post_sync_warning_from_result(Ok(run_post_import_sync(db_for_sync))); + if let Some(msg) = warning.as_ref() { + log::warn!("[Import] post-import sync warning: {msg}"); } - - // 重新加载设置到内存缓存,确保导入的设置生效 - if let Err(err) = crate::settings::reload_settings() { - log::warn!("导入后重载设置失败: {err}"); - } - - Ok::<_, AppError>(json!({ - "success": true, - "message": "SQL imported successfully", - "backupId": backup_id - })) + Ok::<_, AppError>(success_payload_with_warning(backup_id, warning)) }) .await .map_err(|e| format!("导入配置失败: {e}"))? @@ -80,6 +73,8 @@ pub async fn sync_current_providers_live(state: State<'_, AppState>) -> Result( diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 7976c4316..f7a1b81c0 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -17,7 +17,9 @@ mod session_manager; mod settings; pub mod skill; mod stream_check; +mod sync_support; mod usage; +mod webdav_sync; pub use config::*; pub use deeplink::*; @@ -37,3 +39,4 @@ pub use settings::*; pub use skill::*; pub use stream_check::*; pub use usage::*; +pub use webdav_sync::*; diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 2a6ca5039..8aac400f9 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -2,16 +2,28 @@ use tauri::AppHandle; +fn merge_settings_for_save( + mut incoming: crate::settings::AppSettings, + existing: &crate::settings::AppSettings, +) -> crate::settings::AppSettings { + if incoming.webdav_sync.is_none() { + incoming.webdav_sync = existing.webdav_sync.clone(); + } + incoming +} + /// 获取设置 #[tauri::command] pub async fn get_settings() -> Result { - Ok(crate::settings::get_settings()) + Ok(crate::settings::get_settings_for_frontend()) } /// 保存设置 #[tauri::command] pub async fn save_settings(settings: crate::settings::AppSettings) -> Result { - crate::settings::update_settings(settings).map_err(|e| e.to_string())?; + let existing = crate::settings::get_settings(); + let merged = merge_settings_for_save(settings, &existing); + crate::settings::update_settings(merged).map_err(|e| e.to_string())?; Ok(true) } @@ -54,6 +66,58 @@ pub async fn set_auto_launch(enabled: bool) -> Result { Ok(true) } +#[cfg(test)] +mod tests { + use super::merge_settings_for_save; + use crate::settings::{AppSettings, WebDavSyncSettings}; + + #[test] + fn save_settings_should_preserve_existing_webdav_when_payload_omits_it() { + let mut existing = AppSettings::default(); + existing.webdav_sync = Some(WebDavSyncSettings { + base_url: "https://dav.example.com".to_string(), + username: "alice".to_string(), + password: "secret".to_string(), + ..WebDavSyncSettings::default() + }); + + let incoming = AppSettings::default(); + let merged = merge_settings_for_save(incoming, &existing); + + assert!(merged.webdav_sync.is_some()); + assert_eq!( + merged.webdav_sync.as_ref().map(|v| v.base_url.as_str()), + Some("https://dav.example.com") + ); + } + + #[test] + fn save_settings_should_keep_incoming_webdav_when_present() { + let mut existing = AppSettings::default(); + existing.webdav_sync = Some(WebDavSyncSettings { + base_url: "https://dav.old.example.com".to_string(), + username: "old".to_string(), + password: "old-pass".to_string(), + ..WebDavSyncSettings::default() + }); + + let mut incoming = AppSettings::default(); + incoming.webdav_sync = Some(WebDavSyncSettings { + base_url: "https://dav.new.example.com".to_string(), + username: "new".to_string(), + password: "new-pass".to_string(), + ..WebDavSyncSettings::default() + }); + + let merged = merge_settings_for_save(incoming, &existing); + + assert_eq!( + merged.webdav_sync.as_ref().map(|v| v.base_url.as_str()), + Some("https://dav.new.example.com") + ); + } +} + /// 获取开机自启状态 #[tauri::command] pub async fn get_auto_launch_status() -> Result { diff --git a/src-tauri/src/commands/sync_support.rs b/src-tauri/src/commands/sync_support.rs new file mode 100644 index 000000000..00793b3b8 --- /dev/null +++ b/src-tauri/src/commands/sync_support.rs @@ -0,0 +1,97 @@ +use serde_json::{json, Value}; +use std::sync::Arc; + +use crate::database::Database; +use crate::error::AppError; +use crate::services::provider::ProviderService; +use crate::settings; +use crate::store::AppState; + +pub(crate) fn run_post_import_sync(db: Arc) -> Result<(), AppError> { + let app_state = AppState::new(db); + ProviderService::sync_current_to_live(&app_state)?; + settings::reload_settings()?; + Ok(()) +} + +fn post_sync_warning(err: E) -> String { + AppError::localized( + "sync.post_operation_sync_failed", + format!("后置同步状态失败: {err}"), + format!("Post-operation synchronization failed: {err}"), + ) + .to_string() +} + +pub(crate) fn post_sync_warning_from_result( + result: Result, String>, +) -> Option { + match result { + Ok(Ok(())) => None, + Ok(Err(err)) => Some(post_sync_warning(err)), + Err(err) => Some(post_sync_warning(err)), + } +} + +pub(crate) fn attach_warning(mut value: Value, warning: Option) -> Value { + if let Some(message) = warning { + if let Some(obj) = value.as_object_mut() { + obj.insert("warning".to_string(), Value::String(message)); + } + } + value +} + +pub(crate) fn success_payload_with_warning(backup_id: String, warning: Option) -> Value { + attach_warning( + json!({ + "success": true, + "message": "SQL imported successfully", + "backupId": backup_id + }), + warning, + ) +} + +#[cfg(test)] +mod tests { + use super::{attach_warning, post_sync_warning_from_result}; + use serde_json::json; + + #[test] + fn post_sync_warning_from_result_returns_none_on_success() { + let warning = post_sync_warning_from_result(Ok(Ok(()))); + assert!(warning.is_none()); + } + + #[test] + fn post_sync_warning_from_result_returns_some_on_sync_error() { + let warning = + post_sync_warning_from_result(Ok(Err(crate::error::AppError::Config("boom".into())))); + assert!(warning.is_some()); + } + + #[tokio::test] + async fn post_sync_warning_from_result_returns_some_on_join_error() { + let handle = tokio::spawn(async move { + panic!("forced join error"); + }); + let join_err = handle.await.expect_err("task should panic"); + let warning = post_sync_warning_from_result(Err(join_err.to_string())); + assert!(warning.is_some()); + } + + #[test] + fn attach_warning_adds_warning_without_dropping_existing_fields() { + let payload = json!({ "status": "downloaded" }); + let updated = attach_warning(payload, Some("post sync warning".to_string())); + assert_eq!( + updated.get("status").and_then(|v| v.as_str()), + Some("downloaded") + ); + assert_eq!( + updated.get("warning").and_then(|v| v.as_str()), + Some("post sync warning") + ); + } +} diff --git a/src-tauri/src/commands/webdav_sync.rs b/src-tauri/src/commands/webdav_sync.rs new file mode 100644 index 000000000..e1bbd70d7 --- /dev/null +++ b/src-tauri/src/commands/webdav_sync.rs @@ -0,0 +1,357 @@ +#![allow(non_snake_case)] + +use serde_json::{Value, json}; +use std::future::Future; +use std::sync::OnceLock; +use tauri::State; + +use crate::commands::sync_support::{ + attach_warning, post_sync_warning_from_result, run_post_import_sync, +}; +use crate::error::AppError; +use crate::services::webdav_sync as webdav_sync_service; +use crate::settings::{self, WebDavSyncSettings}; +use crate::store::AppState; + +fn persist_sync_error(settings: &mut WebDavSyncSettings, error: &AppError) { + settings.status.last_error = Some(error.to_string()); + let _ = settings::update_webdav_sync_status(settings.status.clone()); +} + +fn webdav_not_configured_error() -> String { + AppError::localized( + "webdav.sync.not_configured", + "未配置 WebDAV 同步", + "WebDAV sync is not configured.", + ) + .to_string() +} + +fn webdav_sync_disabled_error() -> String { + AppError::localized( + "webdav.sync.disabled", + "WebDAV 同步未启用", + "WebDAV sync is disabled.", + ) + .to_string() +} + +fn require_enabled_webdav_settings() -> Result { + let settings = settings::get_webdav_sync_settings().ok_or_else(webdav_not_configured_error)?; + if !settings.enabled { + return Err(webdav_sync_disabled_error()); + } + Ok(settings) +} + +fn resolve_password_for_request( + mut incoming: WebDavSyncSettings, + existing: Option, + preserve_empty_password: bool, +) -> WebDavSyncSettings { + if let Some(existing_settings) = existing { + if preserve_empty_password && incoming.password.is_empty() { + incoming.password = existing_settings.password; + } + } + incoming +} + +fn webdav_sync_mutex() -> &'static tokio::sync::Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| tokio::sync::Mutex::new(())) +} + +async fn run_with_webdav_lock(operation: Fut) -> Result +where + Fut: Future>, +{ + let result = { + let _guard = webdav_sync_mutex().lock().await; + operation.await + }; + result +} + +fn map_sync_result(result: Result, on_error: F) -> Result +where + F: FnOnce(&AppError), +{ + match result { + Ok(value) => Ok(value), + Err(err) => { + on_error(&err); + Err(err.to_string()) + } + } +} + +#[tauri::command] +pub async fn webdav_test_connection( + settings: WebDavSyncSettings, + #[allow(non_snake_case)] preserveEmptyPassword: Option, +) -> Result { + let preserve_empty = preserveEmptyPassword.unwrap_or(true); + let resolved = resolve_password_for_request( + settings, + settings::get_webdav_sync_settings(), + preserve_empty, + ); + webdav_sync_service::check_connection(&resolved) + .await + .map_err(|e| e.to_string())?; + Ok(json!({ + "success": true, + "message": "WebDAV connection ok" + })) +} + +#[tauri::command] +pub async fn webdav_sync_upload(state: State<'_, AppState>) -> Result { + let db = state.db.clone(); + let mut settings = require_enabled_webdav_settings()?; + + let result = run_with_webdav_lock(webdav_sync_service::upload(&db, &mut settings)).await; + map_sync_result(result, |error| persist_sync_error(&mut settings, error)) +} + +#[tauri::command] +pub async fn webdav_sync_download(state: State<'_, AppState>) -> Result { + let db = state.db.clone(); + let db_for_sync = db.clone(); + let mut settings = require_enabled_webdav_settings()?; + + let sync_result = run_with_webdav_lock(webdav_sync_service::download(&db, &mut settings)).await; + let mut result = map_sync_result(sync_result, |error| { + persist_sync_error(&mut settings, error) + })?; + + // Post-download sync is best-effort: snapshot restore has already succeeded. + let warning = post_sync_warning_from_result( + tauri::async_runtime::spawn_blocking(move || run_post_import_sync(db_for_sync)) + .await + .map_err(|e| e.to_string()), + ); + if let Some(msg) = warning.as_ref() { + log::warn!("[WebDAV] post-download sync warning: {msg}"); + } + result = attach_warning(result, warning); + + Ok(result) +} + +#[tauri::command] +pub async fn webdav_sync_save_settings( + settings: WebDavSyncSettings, + #[allow(non_snake_case)] passwordTouched: Option, +) -> Result { + let password_touched = passwordTouched.unwrap_or(false); + let existing = settings::get_webdav_sync_settings(); + let mut sync_settings = + resolve_password_for_request(settings, existing.clone(), !password_touched); + + // Preserve server-owned fields that the frontend does not manage + if let Some(existing_settings) = existing { + sync_settings.status = existing_settings.status; + } + + sync_settings.normalize(); + sync_settings.validate().map_err(|e| e.to_string())?; + settings::set_webdav_sync_settings(Some(sync_settings)).map_err(|e| e.to_string())?; + Ok(json!({ "success": true })) +} + +#[tauri::command] +pub async fn webdav_sync_fetch_remote_info() -> Result { + let settings = require_enabled_webdav_settings()?; + let info = webdav_sync_service::fetch_remote_info(&settings) + .await + .map_err(|e| e.to_string())?; + Ok(info.unwrap_or(json!({ "empty": true }))) +} + +#[cfg(test)] +mod tests { + use super::{ + map_sync_result, persist_sync_error, require_enabled_webdav_settings, + resolve_password_for_request, run_with_webdav_lock, webdav_sync_mutex, + }; + use crate::error::AppError; + use crate::settings::{AppSettings, WebDavSyncSettings}; + use serial_test::serial; + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::time::Duration; + + #[tokio::test] + async fn webdav_sync_mutex_is_singleton() { + let a = webdav_sync_mutex() as *const _; + let b = webdav_sync_mutex() as *const _; + assert_eq!(a, b); + } + + #[tokio::test] + #[serial] + async fn webdav_sync_mutex_serializes_concurrent_access() { + let guard = webdav_sync_mutex().lock().await; + let acquired = Arc::new(AtomicBool::new(false)); + let acquired_bg = Arc::clone(&acquired); + + let waiter = tokio::spawn(async move { + let _inner_guard = webdav_sync_mutex().lock().await; + acquired_bg.store(true, Ordering::SeqCst); + }); + + tokio::time::sleep(Duration::from_millis(40)).await; + assert!(!acquired.load(Ordering::SeqCst)); + + drop(guard); + tokio::time::timeout(Duration::from_secs(1), waiter) + .await + .expect("background task should complete after lock release") + .expect("background task should not panic"); + + assert!(acquired.load(Ordering::SeqCst)); + } + + #[tokio::test] + #[serial] + async fn map_sync_result_runs_error_handler_after_lock_release() { + let result = run_with_webdav_lock(async { + Err::<(), AppError>(AppError::Config("boom".to_string())) + }) + .await; + + let mut lock_released = false; + let mapped = map_sync_result(result, |_| { + lock_released = webdav_sync_mutex().try_lock().is_ok(); + }); + + assert!(mapped.is_err()); + assert!(lock_released); + } + + #[test] + fn resolve_password_for_request_preserves_existing_when_requested() { + let incoming = WebDavSyncSettings { + base_url: "https://dav.example.com".to_string(), + username: "alice".to_string(), + password: String::new(), + ..WebDavSyncSettings::default() + }; + let existing = Some(WebDavSyncSettings { + password: "secret".to_string(), + ..WebDavSyncSettings::default() + }); + let resolved = resolve_password_for_request(incoming, existing, true); + assert_eq!(resolved.password, "secret"); + } + + #[test] + fn resolve_password_for_request_allows_explicit_empty_password() { + let incoming = WebDavSyncSettings { + base_url: "https://dav.example.com".to_string(), + username: "alice".to_string(), + password: String::new(), + ..WebDavSyncSettings::default() + }; + let existing = Some(WebDavSyncSettings { + password: "secret".to_string(), + ..WebDavSyncSettings::default() + }); + let resolved = resolve_password_for_request(incoming, existing, false); + assert!(resolved.password.is_empty()); + } + + #[test] + #[serial] + fn persist_sync_error_updates_status_without_overwriting_credentials() { + let test_home = std::env::temp_dir().join("cc-switch-sync-error-status-test"); + let _ = std::fs::remove_dir_all(&test_home); + std::fs::create_dir_all(&test_home).expect("create test home"); + std::env::set_var("CC_SWITCH_TEST_HOME", &test_home); + + crate::settings::update_settings(AppSettings::default()).expect("reset settings"); + let mut current = WebDavSyncSettings { + enabled: true, + base_url: "https://dav.example.com/dav/".to_string(), + username: "alice".to_string(), + password: "secret".to_string(), + remote_root: "cc-switch-sync".to_string(), + profile: "default".to_string(), + ..WebDavSyncSettings::default() + }; + crate::settings::set_webdav_sync_settings(Some(current.clone())) + .expect("seed webdav settings"); + + persist_sync_error( + &mut current, + &crate::error::AppError::Config("boom".to_string()), + ); + + let after = crate::settings::get_webdav_sync_settings().expect("read webdav settings"); + assert_eq!(after.base_url, "https://dav.example.com/dav/"); + assert_eq!(after.username, "alice"); + assert_eq!(after.password, "secret"); + assert_eq!(after.remote_root, "cc-switch-sync"); + assert_eq!(after.profile, "default"); + assert!( + after + .status + .last_error + .as_deref() + .unwrap_or_default() + .contains("boom"), + "status error should be updated" + ); + } + + #[test] + #[serial] + fn require_enabled_webdav_settings_rejects_disabled_config() { + let test_home = std::env::temp_dir().join("cc-switch-sync-enabled-disabled-test"); + let _ = std::fs::remove_dir_all(&test_home); + std::fs::create_dir_all(&test_home).expect("create test home"); + std::env::set_var("CC_SWITCH_TEST_HOME", &test_home); + + crate::settings::update_settings(AppSettings::default()).expect("reset settings"); + crate::settings::set_webdav_sync_settings(Some(WebDavSyncSettings { + enabled: false, + base_url: "https://dav.example.com/dav/".to_string(), + username: "alice".to_string(), + password: "secret".to_string(), + ..WebDavSyncSettings::default() + })) + .expect("seed disabled webdav settings"); + + let err = require_enabled_webdav_settings().expect_err("disabled settings should fail"); + assert!( + err.contains("disabled") || err.contains("未启用"), + "unexpected error: {err}" + ); + } + + #[test] + #[serial] + fn require_enabled_webdav_settings_returns_settings_when_enabled() { + let test_home = std::env::temp_dir().join("cc-switch-sync-enabled-ok-test"); + let _ = std::fs::remove_dir_all(&test_home); + std::fs::create_dir_all(&test_home).expect("create test home"); + std::env::set_var("CC_SWITCH_TEST_HOME", &test_home); + + crate::settings::update_settings(AppSettings::default()).expect("reset settings"); + crate::settings::set_webdav_sync_settings(Some(WebDavSyncSettings { + enabled: true, + base_url: "https://dav.example.com/dav/".to_string(), + username: "alice".to_string(), + password: "secret".to_string(), + ..WebDavSyncSettings::default() + })) + .expect("seed enabled webdav settings"); + + let settings = + require_enabled_webdav_settings().expect("enabled settings should be accepted"); + assert!(settings.enabled); + assert_eq!(settings.base_url, "https://dav.example.com/dav/"); + } +} diff --git a/src-tauri/src/database/backup.rs b/src-tauri/src/database/backup.rs index bd3df4181..484b22569 100644 --- a/src-tauri/src/database/backup.rs +++ b/src-tauri/src/database/backup.rs @@ -16,10 +16,15 @@ use tempfile::NamedTempFile; const CC_SWITCH_SQL_EXPORT_HEADER: &str = "-- CC Switch SQLite 导出"; impl Database { + /// 导出为 SQLite 兼容的 SQL 文本(内存字符串) + pub fn export_sql_string(&self) -> Result { + let snapshot = self.snapshot_to_memory()?; + Self::dump_sql(&snapshot) + } + /// 导出为 SQLite 兼容的 SQL 文本 pub fn export_sql(&self, target_path: &Path) -> Result<(), AppError> { - let snapshot = self.snapshot_to_memory()?; - let dump = Self::dump_sql(&snapshot)?; + let dump = self.export_sql_string()?; if let Some(parent) = target_path.parent() { fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; @@ -38,6 +43,12 @@ impl Database { } let sql_raw = fs::read_to_string(source_path).map_err(|e| AppError::io(source_path, e))?; + let sql_content = sql_raw.trim_start_matches('\u{feff}'); + self.import_sql_string(sql_content) + } + + /// 从 SQL 字符串导入,返回生成的备份 ID(若无备份则为空字符串) + pub fn import_sql_string(&self, sql_raw: &str) -> Result { let sql_content = sql_raw.trim_start_matches('\u{feff}'); Self::validate_cc_switch_sql_export(sql_content)?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e2657745a..ef47fdaee 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -886,6 +886,11 @@ pub fn run() { // theirs: config import/export and dialogs commands::export_config_to_file, commands::import_config_from_file, + commands::webdav_test_connection, + commands::webdav_sync_upload, + commands::webdav_sync_download, + commands::webdav_sync_save_settings, + commands::webdav_sync_fetch_remote_info, commands::save_file_dialog, commands::open_file_dialog, commands::open_zip_file_dialog, diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 0875fdeb3..7a49e531c 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -10,6 +10,8 @@ pub mod skill; pub mod speedtest; pub mod stream_check; pub mod usage_stats; +pub mod webdav; +pub mod webdav_sync; pub use config::ConfigService; pub use mcp::McpService; diff --git a/src-tauri/src/services/webdav.rs b/src-tauri/src/services/webdav.rs new file mode 100644 index 000000000..c9d376e70 --- /dev/null +++ b/src-tauri/src/services/webdav.rs @@ -0,0 +1,507 @@ +//! WebDAV HTTP transport layer. +//! +//! Low-level HTTP primitives for WebDAV operations (PUT, GET, HEAD, MKCOL, PROPFIND). +//! The sync protocol logic lives in [`super::webdav_sync`]. + +use reqwest::{Method, RequestBuilder, StatusCode, Url}; +use std::time::Duration; + +use crate::error::AppError; +use crate::proxy::http_client; + +const DEFAULT_TIMEOUT_SECS: u64 = 30; +/// Timeout for large file transfers (PUT/GET of db.sql, skills.zip). +const TRANSFER_TIMEOUT_SECS: u64 = 300; + +/// Auth pair: `(username, Some(password))`. +pub type WebDavAuth = Option<(String, Option)>; + +// ─── WebDAV extension methods ──────────────────────────────── + +fn method_propfind() -> Method { + Method::from_bytes(b"PROPFIND").expect("PROPFIND is a valid HTTP method") +} + +fn method_mkcol() -> Method { + Method::from_bytes(b"MKCOL").expect("MKCOL is a valid HTTP method") +} + +// ─── URL utilities ─────────────────────────────────────────── + +/// Parse and validate a WebDAV base URL (must be http or https). +pub fn parse_base_url(raw: &str) -> Result { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err(AppError::localized( + "webdav.base_url.required", + "WebDAV 地址不能为空", + "WebDAV URL is required.", + )); + } + let url = Url::parse(trimmed).map_err(|e| { + AppError::localized( + "webdav.base_url.invalid", + format!("WebDAV 地址无效: {e}"), + format!("Invalid WebDAV URL: {e}"), + ) + })?; + match url.scheme() { + "http" | "https" => Ok(url), + _ => Err(AppError::localized( + "webdav.base_url.scheme_invalid", + "WebDAV 仅支持 http/https 地址", + "WebDAV URL must use http or https.", + )), + } +} + +/// Build a full URL from a base URL string and path segments. +/// +/// Each segment is individually percent-encoded by the `url` crate. +pub fn build_remote_url(base_url: &str, segments: &[String]) -> Result { + let mut url = parse_base_url(base_url)?; + { + let mut path = url.path_segments_mut().map_err(|_| { + AppError::localized( + "webdav.base_url.unusable", + "WebDAV 地址格式不支持追加路径", + "WebDAV URL format does not support appending path segments.", + ) + })?; + path.pop_if_empty(); + for seg in segments { + path.push(seg); + } + } + Ok(url.to_string()) +} + +/// Split a slash-delimited path into non-empty segments. +pub fn path_segments(raw: &str) -> impl Iterator { + raw.trim_matches('/').split('/').filter(|s| !s.is_empty()) +} + +// ─── Auth ──────────────────────────────────────────────────── + +/// Build auth from username/password. Returns `None` if username is blank. +pub fn auth_from_credentials(username: &str, password: &str) -> WebDavAuth { + let user = username.trim(); + if user.is_empty() { + return None; + } + Some((user.to_string(), Some(password.to_string()))) +} + +/// Apply Basic-Auth to a request builder if auth is present. +fn apply_auth(builder: RequestBuilder, auth: &WebDavAuth) -> RequestBuilder { + match auth { + Some((user, pass)) => builder.basic_auth(user, pass.as_deref()), + None => builder, + } +} + +fn webdav_transport_error( + key: &'static str, + op_zh: &str, + op_en: &str, + target_url: &str, + err: &reqwest::Error, +) -> AppError { + let (zh_reason, en_reason) = if err.is_timeout() { + ("请求超时", "request timed out") + } else if err.is_connect() { + ("连接失败", "connection failed") + } else if err.is_request() { + ("请求构造失败", "request build failed") + } else { + ("网络请求失败", "network request failed") + }; + + let safe_url = redact_url(target_url); + AppError::localized( + key, + format!("WebDAV {op_zh}失败({zh_reason}): {safe_url}"), + format!("WebDAV {op_en} failed ({en_reason}): {safe_url}"), + ) +} + +// ─── HTTP operations ───────────────────────────────────────── + +/// Test WebDAV connectivity via PROPFIND Depth=0 on the base URL. +pub async fn test_connection(base_url: &str, auth: &WebDavAuth) -> Result<(), AppError> { + let url = parse_base_url(base_url)?; + let client = http_client::get(); + + let resp = apply_auth( + client + .request(method_propfind(), url) + .header("Depth", "0") + .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)), + auth, + ) + .send() + .await + .map_err(|e| { + webdav_transport_error( + "webdav.connection_failed", + "连接", + "connection", + base_url, + &e, + ) + })?; + + if resp.status().is_success() || resp.status() == StatusCode::MULTI_STATUS { + return Ok(()); + } + Err(webdav_status_error("PROPFIND", resp.status(), base_url)) +} + +/// Ensure a chain of remote directories exists. +/// +/// Uses optimistic MKCOL: try creating first, fall back to PROPFIND verification +/// on ambiguous responses. This halves the round-trips vs PROPFIND-first approach. +pub async fn ensure_remote_directories( + base_url: &str, + segments: &[String], + auth: &WebDavAuth, +) -> Result<(), AppError> { + if segments.is_empty() { + return Ok(()); + } + let client = http_client::get(); + + for depth in 1..=segments.len() { + let prefix = &segments[..depth]; + let url = build_remote_url(base_url, prefix)?; + let dir_url = if url.ends_with('/') { + url + } else { + format!("{url}/") + }; + + let resp = apply_auth( + client + .request(method_mkcol(), &dir_url) + .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)), + auth, + ) + .send() + .await + .map_err(|e| { + webdav_transport_error( + "webdav.mkcol_failed", + "MKCOL 请求", + "MKCOL request", + &dir_url, + &e, + ) + })?; + + let status = resp.status(); + match status { + s if s == StatusCode::CREATED || s.is_success() => { + log::info!("[WebDAV] MKCOL ok: {}", redact_url(&dir_url)); + } + // 405 commonly means "already exists" on many WebDAV servers + StatusCode::METHOD_NOT_ALLOWED => {} + // Ambiguous — verify directory actually exists via PROPFIND + s if s == StatusCode::CONFLICT || s.is_redirection() => { + if !propfind_exists(&client, &dir_url, auth).await? { + return Err(webdav_status_error("MKCOL", status, &dir_url)); + } + } + _ => { + return Err(webdav_status_error("MKCOL", status, &dir_url)); + } + } + } + Ok(()) +} + +/// PUT bytes to a remote WebDAV URL. +pub async fn put_bytes( + url: &str, + auth: &WebDavAuth, + bytes: Vec, + content_type: &str, +) -> Result<(), AppError> { + let client = http_client::get(); + let resp = apply_auth( + client + .put(url) + .header("Content-Type", content_type) + .body(bytes) + .timeout(Duration::from_secs(TRANSFER_TIMEOUT_SECS)), + auth, + ) + .send() + .await + .map_err(|e| { + webdav_transport_error( + "webdav.put_failed", + "PUT 请求", + "PUT request", + url, + &e, + ) + })?; + + if resp.status().is_success() { + return Ok(()); + } + Err(webdav_status_error("PUT", resp.status(), url)) +} + +/// GET bytes from a remote WebDAV URL. Returns `None` on 404. +/// +/// On success returns `(body_bytes, optional_etag)`. +pub async fn get_bytes( + url: &str, + auth: &WebDavAuth, +) -> Result, Option)>, AppError> { + let client = http_client::get(); + let resp = apply_auth( + client + .get(url) + .timeout(Duration::from_secs(TRANSFER_TIMEOUT_SECS)), + auth, + ) + .send() + .await + .map_err(|e| { + webdav_transport_error( + "webdav.get_failed", + "GET 请求", + "GET request", + url, + &e, + ) + })?; + + if resp.status() == StatusCode::NOT_FOUND { + return Ok(None); + } + if !resp.status().is_success() { + return Err(webdav_status_error("GET", resp.status(), url)); + } + let etag = resp + .headers() + .get("etag") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + let bytes = resp + .bytes() + .await + .map_err(|e| { + AppError::localized( + "webdav.response_read_failed", + format!("读取 WebDAV 响应失败: {e}"), + format!("Failed to read WebDAV response: {e}"), + ) + })?; + Ok(Some((bytes.to_vec(), etag))) +} + +/// HEAD request to retrieve the ETag. Returns `None` on 404. +pub async fn head_etag(url: &str, auth: &WebDavAuth) -> Result, AppError> { + let client = http_client::get(); + let resp = apply_auth( + client + .head(url) + .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)), + auth, + ) + .send() + .await + .map_err(|e| { + webdav_transport_error( + "webdav.head_failed", + "HEAD 请求", + "HEAD request", + url, + &e, + ) + })?; + + if resp.status() == StatusCode::NOT_FOUND { + return Ok(None); + } + if !resp.status().is_success() { + return Err(webdav_status_error("HEAD", resp.status(), url)); + } + Ok(resp + .headers() + .get("etag") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string())) +} + +// ─── Internal helpers ──────────────────────────────────────── + +/// PROPFIND Depth=0 to check if a remote resource exists. +async fn propfind_exists( + client: &reqwest::Client, + url: &str, + auth: &WebDavAuth, +) -> Result { + let resp = apply_auth( + client + .request(method_propfind(), url) + .header("Depth", "0") + .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)), + auth, + ) + .send() + .await; + match resp { + Ok(r) => Ok(r.status().is_success() || r.status() == StatusCode::MULTI_STATUS), + Err(e) => { + log::warn!( + "[WebDAV] PROPFIND check failed for {}: {e}", + redact_url(url) + ); + Ok(false) + } + } +} + +// ─── Service detection & error helpers ─────────────────────── + +/// Check if a URL points to Jianguoyun (坚果云). +pub fn is_jianguoyun(url: &str) -> bool { + Url::parse(url) + .ok() + .and_then(|u| u.host_str().map(|h| h.to_lowercase())) + .map(|host| host.contains("jianguoyun.com") || host.contains("nutstore")) + .unwrap_or(false) +} + +/// Build an `AppError` with service-specific hints for WebDAV failures. +pub fn webdav_status_error(op: &str, status: StatusCode, url: &str) -> AppError { + let safe_url = redact_url(url); + let mut zh = format!("WebDAV {op} 失败: {status} ({safe_url})"); + let mut en = format!("WebDAV {op} failed: {status} ({safe_url})"); + let jgy = is_jianguoyun(url); + + if matches!(status, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN) { + if jgy { + zh.push_str( + "。坚果云请使用「第三方应用密码」,并确认地址指向 /dav/ 下的目录。", + ); + en.push_str( + ". For Jianguoyun, use an app-specific password and ensure the URL points under /dav/.", + ); + } else { + zh.push_str("。请检查 WebDAV 用户名、密码及目录读写权限。"); + en.push_str(". Please check WebDAV username/password and directory permissions."); + } + } else if jgy && (status == StatusCode::NOT_FOUND || status.is_redirection()) { + zh.push_str("。坚果云常见原因:地址不在 /dav/ 可写目录下。"); + en.push_str(". Common Jianguoyun cause: URL is outside a writable /dav/ directory."); + } else if op == "MKCOL" && status == StatusCode::CONFLICT { + if jgy { + zh.push_str( + "。坚果云不允许自动创建顶层文件夹,请先在网页端手动创建后重试。", + ); + en.push_str( + ". Jianguoyun does not allow creating top-level folders automatically; create it manually first.", + ); + } else { + zh.push_str("。请确认上级目录存在。"); + en.push_str(". Please ensure the parent directory exists."); + } + } + + AppError::localized("webdav.http.status", zh, en) +} + +fn redact_url(raw: &str) -> String { + match Url::parse(raw) { + Ok(mut parsed) => { + let _ = parsed.set_username(""); + let _ = parsed.set_password(None); + + let mut out = format!("{}://", parsed.scheme()); + if let Some(host) = parsed.host_str() { + out.push_str(host); + } + if let Some(port) = parsed.port() { + out.push(':'); + out.push_str(&port.to_string()); + } + out.push_str(parsed.path()); + + let mut keys: Vec = parsed.query_pairs().map(|(k, _)| k.into_owned()).collect(); + keys.sort(); + keys.dedup(); + if !keys.is_empty() { + out.push_str("?[keys:"); + out.push_str(&keys.join(",")); + out.push(']'); + } + out + } + Err(_) => raw.split('?').next().unwrap_or(raw).to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_remote_url_encodes_path_segments() { + let url = build_remote_url( + "https://dav.example.com/remote.php/dav/files/demo/", + &[ + "cc switch-sync".to_string(), + "v2".to_string(), + "default profile".to_string(), + "manifest.json".to_string(), + ], + ) + .unwrap(); + assert_eq!( + url, + "https://dav.example.com/remote.php/dav/files/demo/cc%20switch-sync/v2/default%20profile/manifest.json" + ); + assert!(!url.contains("//cc"), "should not have double-slash"); + } + + #[test] + fn is_jianguoyun_detects_correctly() { + assert!(is_jianguoyun("https://dav.jianguoyun.com/dav")); + assert!(is_jianguoyun("https://dav.jianguoyun.com/dav/folder")); + assert!(!is_jianguoyun("https://nextcloud.example.com/dav")); + } + + #[test] + fn path_segments_splits_correctly() { + let segs: Vec<_> = path_segments("/a/b/c/").collect(); + assert_eq!(segs, vec!["a", "b", "c"]); + + let segs: Vec<_> = path_segments("single").collect(); + assert_eq!(segs, vec!["single"]); + + let segs: Vec<_> = path_segments("").collect(); + assert!(segs.is_empty()); + } + + #[test] + fn auth_from_credentials_trims_and_rejects_blank() { + assert!(auth_from_credentials(" ", "pass").is_none()); + let auth = auth_from_credentials(" user ", "pass"); + assert_eq!(auth, Some(("user".to_string(), Some("pass".to_string())))); + } + + #[test] + fn redact_url_hides_credentials_and_query_values() { + let redacted = redact_url("https://alice:secret@example.com:8443/dav?token=abc&foo=1"); + assert_eq!( + redacted, + "https://example.com:8443/dav?[keys:foo,token]" + ); + assert!(!redacted.contains("secret")); + } +} diff --git a/src-tauri/src/services/webdav_sync.rs b/src-tauri/src/services/webdav_sync.rs new file mode 100644 index 000000000..99d787d26 --- /dev/null +++ b/src-tauri/src/services/webdav_sync.rs @@ -0,0 +1,649 @@ +//! WebDAV v2 sync protocol layer. +//! +//! Implements manifest-based synchronization on top of the HTTP transport +//! primitives in [`super::webdav`]. Artifact set: `db.sql` + `skills.zip`. + +use std::collections::BTreeMap; +use std::fs; +use std::process::Command; + +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use tempfile::tempdir; + +use crate::error::AppError; +use crate::services::webdav::{ + auth_from_credentials, build_remote_url, ensure_remote_directories, get_bytes, head_etag, + path_segments, put_bytes, test_connection, WebDavAuth, +}; +use crate::settings::{update_webdav_sync_status, WebDavSyncSettings, WebDavSyncStatus}; + +mod archive; +use archive::{ + backup_current_skills, restore_skills_from_backup, restore_skills_zip, zip_skills_ssot, +}; + +// ─── Protocol constants ────────────────────────────────────── + +const PROTOCOL_FORMAT: &str = "cc-switch-webdav-sync"; +const PROTOCOL_VERSION: u32 = 2; +const REMOTE_DB_SQL: &str = "db.sql"; +const REMOTE_SKILLS_ZIP: &str = "skills.zip"; +const REMOTE_MANIFEST: &str = "manifest.json"; +const MAX_DEVICE_NAME_LEN: usize = 64; + +fn localized(key: &'static str, zh: impl Into, en: impl Into) -> AppError { + AppError::localized(key, zh, en) +} + +fn io_context_localized( + _key: &'static str, + zh: impl Into, + en: impl Into, + source: std::io::Error, +) -> AppError { + let zh_msg = zh.into(); + let en_msg = en.into(); + AppError::IoContext { + context: format!("{zh_msg} ({en_msg})"), + source, + } +} + +// ─── Types ─────────────────────────────────────────────────── + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SyncManifest { + format: String, + version: u32, + device_name: String, + created_at: String, + artifacts: BTreeMap, + snapshot_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ArtifactMeta { + sha256: String, + size: u64, +} + +struct LocalSnapshot { + db_sql: Vec, + skills_zip: Vec, + manifest_bytes: Vec, + manifest_hash: String, +} + +// ─── Public API ────────────────────────────────────────────── + +/// Check WebDAV connectivity and ensure remote directory structure. +pub async fn check_connection(settings: &WebDavSyncSettings) -> Result<(), AppError> { + settings.validate()?; + let auth = auth_for(settings); + test_connection(&settings.base_url, &auth).await?; + let dir_segs = remote_dir_segments(settings); + ensure_remote_directories(&settings.base_url, &dir_segs, &auth).await?; + Ok(()) +} + +/// Upload local snapshot (db + skills) to remote. +pub async fn upload( + db: &crate::database::Database, + settings: &mut WebDavSyncSettings, +) -> Result { + settings.validate()?; + let auth = auth_for(settings); + let dir_segs = remote_dir_segments(settings); + ensure_remote_directories(&settings.base_url, &dir_segs, &auth).await?; + + let snapshot = build_local_snapshot(db, settings)?; + + // Upload order: artifacts first, manifest last (best-effort consistency) + let db_url = remote_file_url(settings, REMOTE_DB_SQL)?; + put_bytes(&db_url, &auth, snapshot.db_sql, "application/sql").await?; + + let skills_url = remote_file_url(settings, REMOTE_SKILLS_ZIP)?; + put_bytes(&skills_url, &auth, snapshot.skills_zip, "application/zip").await?; + + let manifest_url = remote_file_url(settings, REMOTE_MANIFEST)?; + put_bytes( + &manifest_url, + &auth, + snapshot.manifest_bytes, + "application/json", + ) + .await?; + + // Fetch etag (best-effort, don't fail the upload) + let etag = match head_etag(&manifest_url, &auth).await { + Ok(e) => e, + Err(e) => { + log::debug!("[WebDAV] Failed to fetch ETag after upload: {e}"); + None + } + }; + + let _persisted = persist_sync_success_best_effort( + settings, + snapshot.manifest_hash, + etag, + persist_sync_success, + ); + Ok(serde_json::json!({ "status": "uploaded" })) +} + +/// Download remote snapshot and apply to local database + skills. +pub async fn download( + db: &crate::database::Database, + settings: &mut WebDavSyncSettings, +) -> Result { + settings.validate()?; + let auth = auth_for(settings); + + let manifest_url = remote_file_url(settings, REMOTE_MANIFEST)?; + let (manifest_bytes, etag) = get_bytes(&manifest_url, &auth).await?.ok_or_else(|| { + localized( + "webdav.sync.remote_empty", + "远端没有可下载的同步数据", + "No downloadable sync data found on the remote.", + ) + })?; + + let manifest: SyncManifest = + serde_json::from_slice(&manifest_bytes).map_err(|e| AppError::Json { + path: REMOTE_MANIFEST.to_string(), + source: e, + })?; + + validate_manifest_compat(&manifest)?; + + // Download and verify artifacts + let db_sql = download_and_verify(settings, &auth, REMOTE_DB_SQL, &manifest.artifacts).await?; + let skills_zip = + download_and_verify(settings, &auth, REMOTE_SKILLS_ZIP, &manifest.artifacts).await?; + + // Apply snapshot + apply_snapshot(db, &db_sql, &skills_zip)?; + + let manifest_hash = sha256_hex(&manifest_bytes); + let _persisted = + persist_sync_success_best_effort(settings, manifest_hash, etag, persist_sync_success); + Ok(serde_json::json!({ "status": "downloaded" })) +} + +/// Fetch remote manifest info without downloading artifacts. +pub async fn fetch_remote_info(settings: &WebDavSyncSettings) -> Result, AppError> { + settings.validate()?; + let auth = auth_for(settings); + let manifest_url = remote_file_url(settings, REMOTE_MANIFEST)?; + + let Some((bytes, _)) = get_bytes(&manifest_url, &auth).await? else { + return Ok(None); + }; + + let manifest: SyncManifest = serde_json::from_slice(&bytes).map_err(|e| AppError::Json { + path: REMOTE_MANIFEST.to_string(), + source: e, + })?; + + let compatible = validate_manifest_compat(&manifest).is_ok(); + + let payload = serde_json::json!({ + "deviceName": manifest.device_name, + "createdAt": manifest.created_at, + "snapshotId": manifest.snapshot_id, + "version": manifest.version, + "compatible": compatible, + "artifacts": manifest.artifacts.keys().collect::>(), + }); + + Ok(Some(payload)) +} + +// ─── Sync status persistence (I3: deduplicated) ───────────── + +fn persist_sync_success( + settings: &mut WebDavSyncSettings, + manifest_hash: String, + etag: Option, +) -> Result<(), AppError> { + let status = WebDavSyncStatus { + last_sync_at: Some(Utc::now().timestamp()), + last_error: None, + last_local_manifest_hash: Some(manifest_hash.clone()), + last_remote_manifest_hash: Some(manifest_hash), + last_remote_etag: etag, + }; + settings.status = status.clone(); + update_webdav_sync_status(status) +} + +fn persist_sync_success_best_effort( + settings: &mut WebDavSyncSettings, + manifest_hash: String, + etag: Option, + persist_fn: F, +) -> bool +where + F: FnOnce(&mut WebDavSyncSettings, String, Option) -> Result<(), AppError>, +{ + match persist_fn(settings, manifest_hash, etag) { + Ok(()) => true, + Err(err) => { + log::warn!("[WebDAV] Persist sync status failed, keep operation success: {err}"); + false + } + } +} + +// ─── Snapshot building ─────────────────────────────────────── + +fn build_local_snapshot( + db: &crate::database::Database, + _settings: &WebDavSyncSettings, +) -> Result { + // Export database to SQL string + let sql_string = db.export_sql_string()?; + let db_sql = sql_string.into_bytes(); + + // Pack skills into deterministic ZIP + let tmp = tempdir().map_err(|e| { + io_context_localized( + "webdav.sync.snapshot_tmpdir_failed", + "创建 WebDAV 快照临时目录失败", + "Failed to create temporary directory for WebDAV snapshot", + e, + ) + })?; + let skills_zip_path = tmp.path().join(REMOTE_SKILLS_ZIP); + zip_skills_ssot(&skills_zip_path)?; + let skills_zip = fs::read(&skills_zip_path).map_err(|e| AppError::io(&skills_zip_path, e))?; + + // Build artifact map and compute hashes + let mut artifacts = BTreeMap::new(); + artifacts.insert( + REMOTE_DB_SQL.to_string(), + ArtifactMeta { + sha256: sha256_hex(&db_sql), + size: db_sql.len() as u64, + }, + ); + artifacts.insert( + REMOTE_SKILLS_ZIP.to_string(), + ArtifactMeta { + sha256: sha256_hex(&skills_zip), + size: skills_zip.len() as u64, + }, + ); + + let snapshot_id = compute_snapshot_id(&artifacts); + let manifest = SyncManifest { + format: PROTOCOL_FORMAT.to_string(), + version: PROTOCOL_VERSION, + device_name: detect_system_device_name().unwrap_or_else(|| "Unknown Device".to_string()), + created_at: Utc::now().to_rfc3339(), + artifacts, + snapshot_id, + }; + let manifest_bytes = + serde_json::to_vec_pretty(&manifest).map_err(|e| AppError::JsonSerialize { source: e })?; + let manifest_hash = sha256_hex(&manifest_bytes); + + Ok(LocalSnapshot { + db_sql, + skills_zip, + manifest_bytes, + manifest_hash, + }) +} + +/// Compute a deterministic snapshot identity from artifact hashes. +/// +/// BTreeMap iteration order is sorted by key, ensuring stability. +fn compute_snapshot_id(artifacts: &BTreeMap) -> String { + let parts: Vec = artifacts + .iter() + .map(|(name, meta)| format!("{}:{}", name, meta.sha256)) + .collect(); + sha256_hex(parts.join("|").as_bytes()) +} + +fn sha256_hex(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + format!("{:x}", hasher.finalize()) +} + +fn detect_system_device_name() -> Option { + let env_name = [ + "CC_SWITCH_DEVICE_NAME", + "COMPUTERNAME", + "HOSTNAME", + ] + .iter() + .filter_map(|key| std::env::var(key).ok()) + .find_map(|value| normalize_device_name(&value)); + + if env_name.is_some() { + return env_name; + } + + let output = Command::new("hostname").output().ok()?; + if !output.status.success() { + return None; + } + let hostname = String::from_utf8(output.stdout).ok()?; + normalize_device_name(&hostname) +} + +fn normalize_device_name(raw: &str) -> Option { + let compact = raw.chars().fold(String::with_capacity(raw.len()), |mut acc, ch| { + if ch.is_whitespace() { + acc.push(' '); + } else if !ch.is_control() { + acc.push(ch); + } + acc + }); + let normalized = compact.split_whitespace().collect::>().join(" "); + let trimmed = normalized.trim(); + if trimmed.is_empty() { + return None; + } + + let limited = trimmed.chars().take(MAX_DEVICE_NAME_LEN).collect::(); + if limited.is_empty() { + None + } else { + Some(limited) + } +} + +fn validate_manifest_compat(manifest: &SyncManifest) -> Result<(), AppError> { + if manifest.format != PROTOCOL_FORMAT { + return Err(localized( + "webdav.sync.manifest_format_incompatible", + format!("远端 manifest 格式不兼容: {}", manifest.format), + format!( + "Remote manifest format is incompatible: {}", + manifest.format + ), + )); + } + if manifest.version != PROTOCOL_VERSION { + return Err(localized( + "webdav.sync.manifest_version_incompatible", + format!( + "远端 manifest 协议版本不兼容: v{} (本地 v{PROTOCOL_VERSION})", + manifest.version + ), + format!( + "Remote manifest protocol version is incompatible: v{} (local v{PROTOCOL_VERSION})", + manifest.version + ), + )); + } + Ok(()) +} + +// ─── Download & verify ─────────────────────────────────────── + +async fn download_and_verify( + settings: &WebDavSyncSettings, + auth: &WebDavAuth, + artifact_name: &str, + artifacts: &BTreeMap, +) -> Result, AppError> { + let meta = artifacts.get(artifact_name).ok_or_else(|| { + localized( + "webdav.sync.manifest_missing_artifact", + format!("manifest 中缺少 artifact: {artifact_name}"), + format!("Manifest missing artifact: {artifact_name}"), + ) + })?; + let url = remote_file_url(settings, artifact_name)?; + let (bytes, _) = get_bytes(&url, auth).await?.ok_or_else(|| { + localized( + "webdav.sync.remote_missing_artifact", + format!("远端缺少 artifact 文件: {artifact_name}"), + format!("Remote artifact file missing: {artifact_name}"), + ) + })?; + + // Quick size check before expensive hash + if bytes.len() as u64 != meta.size { + return Err(localized( + "webdav.sync.artifact_size_mismatch", + format!( + "artifact {artifact_name} 大小不匹配 (expected: {}, got: {})", + meta.size, + bytes.len(), + ), + format!( + "Artifact {artifact_name} size mismatch (expected: {}, got: {})", + meta.size, + bytes.len(), + ), + )); + } + + let actual_hash = sha256_hex(&bytes); + if actual_hash != meta.sha256 { + return Err(localized( + "webdav.sync.artifact_hash_mismatch", + format!( + "artifact {artifact_name} SHA256 校验失败 (expected: {}..., got: {}...)", + meta.sha256.get(..8).unwrap_or(&meta.sha256), + actual_hash.get(..8).unwrap_or(&actual_hash), + ), + format!( + "Artifact {artifact_name} SHA256 verification failed (expected: {}..., got: {}...)", + meta.sha256.get(..8).unwrap_or(&meta.sha256), + actual_hash.get(..8).unwrap_or(&actual_hash), + ), + )); + } + Ok(bytes) +} + +fn apply_snapshot( + db: &crate::database::Database, + db_sql: &[u8], + skills_zip: &[u8], +) -> Result<(), AppError> { + let sql_str = std::str::from_utf8(db_sql).map_err(|e| { + localized( + "webdav.sync.sql_not_utf8", + format!("SQL 非 UTF-8: {e}"), + format!("SQL is not valid UTF-8: {e}"), + ) + })?; + let skills_backup = backup_current_skills()?; + + // 先替换 skills,再导入数据库;若导入失败则回滚 skills,避免“半恢复”。 + restore_skills_zip(skills_zip)?; + + if let Err(db_err) = db.import_sql_string(sql_str) { + if let Err(rollback_err) = restore_skills_from_backup(&skills_backup) { + return Err(localized( + "webdav.sync.db_import_and_rollback_failed", + format!("导入数据库失败: {db_err}; 同时回滚 Skills 失败: {rollback_err}"), + format!( + "Database import failed: {db_err}; skills rollback also failed: {rollback_err}" + ), + )); + } + return Err(db_err); + } + + Ok(()) +} + +// ─── Remote path helpers ───────────────────────────────────── + +fn remote_dir_segments(settings: &WebDavSyncSettings) -> Vec { + let mut segs = Vec::new(); + segs.extend(path_segments(&settings.remote_root).map(str::to_string)); + segs.push(format!("v{PROTOCOL_VERSION}")); + segs.extend(path_segments(&settings.profile).map(str::to_string)); + segs +} + +fn remote_file_url(settings: &WebDavSyncSettings, file_name: &str) -> Result { + let mut segs = remote_dir_segments(settings); + segs.extend(path_segments(file_name).map(str::to_string)); + build_remote_url(&settings.base_url, &segs) +} + +fn auth_for(settings: &WebDavSyncSettings) -> WebDavAuth { + auth_from_credentials(&settings.username, &settings.password) +} + +// ─── Tests ─────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn artifact(sha256: &str, size: u64) -> ArtifactMeta { + ArtifactMeta { + sha256: sha256.to_string(), + size, + } + } + + #[test] + fn snapshot_id_is_stable() { + let mut artifacts = BTreeMap::new(); + artifacts.insert("db.sql".to_string(), artifact("abc123", 100)); + artifacts.insert("skills.zip".to_string(), artifact("def456", 200)); + + let id1 = compute_snapshot_id(&artifacts); + let id2 = compute_snapshot_id(&artifacts); + assert_eq!(id1, id2); + } + + #[test] + fn snapshot_id_changes_with_artifacts() { + let mut a1 = BTreeMap::new(); + a1.insert("db.sql".to_string(), artifact("hash-a", 1)); + + let mut a2 = BTreeMap::new(); + a2.insert("db.sql".to_string(), artifact("hash-b", 1)); + + assert_ne!(compute_snapshot_id(&a1), compute_snapshot_id(&a2)); + } + + #[test] + fn remote_dir_segments_uses_v2() { + let settings = WebDavSyncSettings { + remote_root: "cc-switch-sync".to_string(), + profile: "default".to_string(), + ..WebDavSyncSettings::default() + }; + let segs = remote_dir_segments(&settings); + assert_eq!(segs, vec!["cc-switch-sync", "v2", "default"]); + } + + #[test] + fn sha256_hex_is_correct() { + let hash = sha256_hex(b"hello"); + assert_eq!( + hash, + "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + ); + } + + #[test] + fn persist_best_effort_returns_true_on_success() { + let mut settings = WebDavSyncSettings::default(); + let ok = persist_sync_success_best_effort( + &mut settings, + "hash".to_string(), + Some("etag".to_string()), + |_settings, _hash, _etag| Ok(()), + ); + assert!(ok); + } + + #[test] + fn persist_best_effort_returns_false_on_error() { + let mut settings = WebDavSyncSettings::default(); + let ok = persist_sync_success_best_effort( + &mut settings, + "hash".to_string(), + None, + |_settings, _hash, _etag| Err(AppError::Config("boom".to_string())), + ); + assert!(!ok); + } + + fn manifest_with(format: &str, version: u32) -> SyncManifest { + let mut artifacts = BTreeMap::new(); + artifacts.insert("db.sql".to_string(), artifact("abc", 1)); + artifacts.insert("skills.zip".to_string(), artifact("def", 2)); + SyncManifest { + format: format.to_string(), + version, + device_name: "My MacBook".to_string(), + created_at: "2026-02-12T00:00:00Z".to_string(), + artifacts, + snapshot_id: "snap-1".to_string(), + } + } + + #[test] + fn validate_manifest_compat_accepts_supported_manifest() { + let manifest = manifest_with(PROTOCOL_FORMAT, PROTOCOL_VERSION); + assert!(validate_manifest_compat(&manifest).is_ok()); + } + + #[test] + fn validate_manifest_compat_rejects_wrong_format() { + let manifest = manifest_with("other-format", PROTOCOL_VERSION); + assert!(validate_manifest_compat(&manifest).is_err()); + } + + #[test] + fn validate_manifest_compat_rejects_wrong_version() { + let manifest = manifest_with(PROTOCOL_FORMAT, PROTOCOL_VERSION + 1); + assert!(validate_manifest_compat(&manifest).is_err()); + } + + #[test] + fn normalize_device_name_returns_none_for_blank_input() { + assert_eq!(normalize_device_name(" \n\t "), None); + } + + #[test] + fn normalize_device_name_collapses_whitespace_and_drops_control_chars() { + assert_eq!( + normalize_device_name(" Mac\tBook \n Pro\u{0007} "), + Some("Mac Book Pro".to_string()) + ); + } + + #[test] + fn normalize_device_name_truncates_to_max_len() { + let long = "a".repeat(80); + assert_eq!(normalize_device_name(&long).map(|s| s.len()), Some(64)); + } + + #[test] + fn manifest_serialization_uses_device_name_only() { + let manifest = manifest_with(PROTOCOL_FORMAT, PROTOCOL_VERSION); + let value = serde_json::to_value(&manifest).expect("serialize manifest"); + assert!( + value.get("deviceName").is_some(), + "manifest should contain deviceName" + ); + assert!( + value.get("deviceId").is_none(), + "manifest should not contain deviceId" + ); + } +} diff --git a/src-tauri/src/services/webdav_sync/archive.rs b/src-tauri/src/services/webdav_sync/archive.rs new file mode 100644 index 000000000..2058429e1 --- /dev/null +++ b/src-tauri/src/services/webdav_sync/archive.rs @@ -0,0 +1,346 @@ +use std::collections::HashSet; +use std::fs; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use tempfile::{tempdir, TempDir}; +use zip::write::SimpleFileOptions; +use zip::DateTime; + +use crate::error::AppError; +use crate::services::skill::SkillService; + +use super::{io_context_localized, localized, REMOTE_SKILLS_ZIP}; + +/// Maximum total bytes allowed during zip extraction (512 MB). +const MAX_EXTRACT_BYTES: u64 = 512 * 1024 * 1024; +/// Maximum number of entries allowed in a zip archive. +const MAX_EXTRACT_ENTRIES: usize = 10_000; + +pub(super) struct SkillsBackup { + _tmp: TempDir, + backup_dir: PathBuf, + ssot_path: PathBuf, + existed: bool, +} + +pub(super) fn zip_skills_ssot(dest_path: &Path) -> Result<(), AppError> { + let source = SkillService::get_ssot_dir().map_err(|e| { + localized( + "webdav.sync.skills_ssot_dir_failed", + format!("获取 Skills SSOT 目录失败: {e}"), + format!("Failed to resolve Skills SSOT directory: {e}"), + ) + })?; + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; + } + + let file = fs::File::create(dest_path).map_err(|e| AppError::io(dest_path, e))?; + let mut writer = zip::ZipWriter::new(file); + let options = SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated) + .last_modified_time(DateTime::default()); + + if source.exists() { + let canonical_root = fs::canonicalize(&source).unwrap_or_else(|_| source.clone()); + let mut visited = HashSet::new(); + mark_visited_dir(&canonical_root, &mut visited)?; + zip_dir_recursive( + &canonical_root, + &canonical_root, + &mut writer, + options, + &mut visited, + )?; + } + + writer.finish().map_err(|e| { + localized( + "webdav.sync.skills_zip_write_failed", + format!("写入 skills.zip 失败: {e}"), + format!("Failed to write skills.zip: {e}"), + ) + })?; + Ok(()) +} + +pub(super) fn restore_skills_zip(raw: &[u8]) -> Result<(), AppError> { + let tmp = tempdir().map_err(|e| { + io_context_localized( + "webdav.sync.skills_extract_tmpdir_failed", + "创建 skills 解压临时目录失败", + "Failed to create temporary directory for skills extraction", + e, + ) + })?; + let zip_path = tmp.path().join(REMOTE_SKILLS_ZIP); + fs::write(&zip_path, raw).map_err(|e| AppError::io(&zip_path, e))?; + + let file = fs::File::open(&zip_path).map_err(|e| AppError::io(&zip_path, e))?; + let mut archive = zip::ZipArchive::new(file).map_err(|e| { + localized( + "webdav.sync.skills_zip_parse_failed", + format!("解析 skills.zip 失败: {e}"), + format!("Failed to parse skills.zip: {e}"), + ) + })?; + + let extracted = tmp.path().join("skills-extracted"); + fs::create_dir_all(&extracted).map_err(|e| AppError::io(&extracted, e))?; + + if archive.len() > MAX_EXTRACT_ENTRIES { + return Err(localized( + "webdav.sync.skills_zip_too_many_entries", + format!("skills.zip 条目数过多({}),上限 {MAX_EXTRACT_ENTRIES}", archive.len()), + format!("skills.zip has too many entries ({}), limit is {MAX_EXTRACT_ENTRIES}", archive.len()), + )); + } + + let mut total_bytes: u64 = 0; + for idx in 0..archive.len() { + let mut entry = archive.by_index(idx).map_err(|e| { + localized( + "webdav.sync.skills_zip_entry_read_failed", + format!("读取 ZIP 项失败: {e}"), + format!("Failed to read ZIP entry: {e}"), + ) + })?; + let Some(safe_name) = entry.enclosed_name() else { + continue; + }; + let out_path = extracted.join(safe_name); + if entry.is_dir() { + fs::create_dir_all(&out_path).map_err(|e| AppError::io(&out_path, e))?; + continue; + } + if let Some(parent) = out_path.parent() { + fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; + } + let mut out = fs::File::create(&out_path).map_err(|e| AppError::io(&out_path, e))?; + let written = std::io::copy(&mut entry, &mut out).map_err(|e| AppError::io(&out_path, e))?; + total_bytes += written; + if total_bytes > MAX_EXTRACT_BYTES { + return Err(localized( + "webdav.sync.skills_zip_too_large", + format!("skills.zip 解压后体积超过上限({} MB)", MAX_EXTRACT_BYTES / 1024 / 1024), + format!("skills.zip extracted size exceeds limit ({} MB)", MAX_EXTRACT_BYTES / 1024 / 1024), + )); + } + } + + let ssot = SkillService::get_ssot_dir().map_err(|e| { + localized( + "webdav.sync.skills_ssot_dir_failed", + format!("获取 Skills SSOT 目录失败: {e}"), + format!("Failed to resolve Skills SSOT directory: {e}"), + ) + })?; + let bak = ssot.with_extension("bak"); + + if ssot.exists() { + if bak.exists() { + let _ = fs::remove_dir_all(&bak); + } + fs::rename(&ssot, &bak).map_err(|e| AppError::io(&ssot, e))?; + } + + if let Err(e) = copy_dir_recursive(&extracted, &ssot) { + if bak.exists() { + let _ = fs::remove_dir_all(&ssot); + let _ = fs::rename(&bak, &ssot); + } + return Err(e); + } + + let _ = fs::remove_dir_all(&bak); + Ok(()) +} + +pub(super) fn backup_current_skills() -> Result { + let ssot = SkillService::get_ssot_dir().map_err(|e| { + localized( + "webdav.sync.skills_ssot_dir_failed", + format!("获取 Skills SSOT 目录失败: {e}"), + format!("Failed to resolve Skills SSOT directory: {e}"), + ) + })?; + let tmp = tempdir().map_err(|e| { + io_context_localized( + "webdav.sync.skills_backup_tmpdir_failed", + "创建 skills 备份临时目录失败", + "Failed to create temporary directory for skills backup", + e, + ) + })?; + let backup_dir = tmp.path().join("skills-backup"); + + let existed = ssot.exists(); + if existed { + copy_dir_recursive(&ssot, &backup_dir)?; + } + + Ok(SkillsBackup { + _tmp: tmp, + backup_dir, + ssot_path: ssot, + existed, + }) +} + +pub(super) fn restore_skills_from_backup(backup: &SkillsBackup) -> Result<(), AppError> { + if backup.ssot_path.exists() { + fs::remove_dir_all(&backup.ssot_path).map_err(|e| AppError::io(&backup.ssot_path, e))?; + } + + if backup.existed { + copy_dir_recursive(&backup.backup_dir, &backup.ssot_path)?; + } + + Ok(()) +} + +fn zip_dir_recursive( + root: &Path, + current: &Path, + writer: &mut zip::ZipWriter, + options: SimpleFileOptions, + visited: &mut HashSet, +) -> Result<(), AppError> { + let mut entries: Vec<_> = fs::read_dir(current) + .map_err(|e| AppError::io(current, e))? + .collect::, _>>() + .map_err(|e| AppError::io(current, e))?; + entries.sort_by_key(|e| e.file_name()); + + for entry in entries { + let path = entry.path(); + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + + if name_str.starts_with('.') { + continue; + } + + let real_path = match fs::canonicalize(&path) { + Ok(p) if p.starts_with(root) => p, + Ok(_) => { + log::warn!( + "[WebDAV] Skipping symlink outside skills root: {}", + path.display() + ); + continue; + } + Err(_) => path.clone(), + }; + + let rel = real_path + .strip_prefix(root) + .or_else(|_| path.strip_prefix(root)) + .map_err(|e| { + localized( + "webdav.sync.zip_relative_path_failed", + format!("生成 ZIP 相对路径失败: {e}"), + format!("Failed to build relative ZIP path: {e}"), + ) + })?; + let rel_str = rel.to_string_lossy().replace('\\', "/"); + + if real_path.is_dir() { + if !mark_visited_dir(&real_path, visited)? { + log::warn!( + "[WebDAV] Skipping already visited directory: {}", + real_path.display() + ); + continue; + } + writer + .add_directory(format!("{rel_str}/"), options) + .map_err(|e| { + localized( + "webdav.sync.zip_add_directory_failed", + format!("写入 ZIP 目录失败: {e}"), + format!("Failed to write ZIP directory entry: {e}"), + ) + })?; + zip_dir_recursive(root, &real_path, writer, options, visited)?; + } else { + writer.start_file(&rel_str, options).map_err(|e| { + localized( + "webdav.sync.zip_start_file_failed", + format!("写入 ZIP 文件头失败: {e}"), + format!("Failed to start ZIP file entry: {e}"), + ) + })?; + let mut file = fs::File::open(&real_path).map_err(|e| AppError::io(&real_path, e))?; + let mut buf = Vec::new(); + file.read_to_end(&mut buf) + .map_err(|e| AppError::io(&real_path, e))?; + writer.write_all(&buf).map_err(|e| { + localized( + "webdav.sync.zip_write_file_failed", + format!("写入 ZIP 文件内容失败: {e}"), + format!("Failed to write ZIP file content: {e}"), + ) + })?; + } + } + Ok(()) +} + +fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), AppError> { + let mut visited = HashSet::new(); + copy_dir_recursive_inner(src, dest, &mut visited) +} + +fn copy_dir_recursive_inner( + src: &Path, + dest: &Path, + visited: &mut HashSet, +) -> Result<(), AppError> { + if !src.exists() { + return Ok(()); + } + if !mark_visited_dir(src, visited)? { + log::warn!( + "[WebDAV] Skipping already visited copy path: {}", + src.display() + ); + return Ok(()); + } + fs::create_dir_all(dest).map_err(|e| AppError::io(dest, e))?; + for entry in fs::read_dir(src).map_err(|e| AppError::io(src, e))? { + let entry = entry.map_err(|e| AppError::io(src, e))?; + let path = entry.path(); + let dest_path = dest.join(entry.file_name()); + if path.is_dir() { + copy_dir_recursive_inner(&path, &dest_path, visited)?; + } else { + fs::copy(&path, &dest_path).map_err(|e| AppError::io(&dest_path, e))?; + } + } + Ok(()) +} + +fn mark_visited_dir(path: &Path, visited: &mut HashSet) -> Result { + let canonical = fs::canonicalize(path).map_err(|e| AppError::io(path, e))?; + Ok(visited.insert(canonical)) +} + +#[cfg(test)] +mod tests { + use super::mark_visited_dir; + use std::collections::HashSet; + use tempfile::tempdir; + + #[test] + fn mark_visited_dir_tracks_canonical_duplicates() { + let temp = tempdir().expect("tempdir"); + let dir = temp.path().join("skills"); + std::fs::create_dir_all(&dir).expect("create dir"); + + let mut visited = HashSet::new(); + assert!(mark_visited_dir(&dir, &mut visited).expect("first visit")); + assert!(!mark_visited_dir(&dir, &mut visited).expect("second visit")); + } +} diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index f0e2ab8f5..e1ae3b213 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use std::fs; +use std::io::Write; use std::path::PathBuf; use std::sync::{OnceLock, RwLock}; @@ -58,6 +59,101 @@ impl VisibleApps { } } +/// WebDAV 同步状态(持久化同步进度信息) +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct WebDavSyncStatus { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_sync_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_error: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_remote_etag: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_local_manifest_hash: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_remote_manifest_hash: Option, +} + +fn default_remote_root() -> String { + "cc-switch-sync".to_string() +} +fn default_profile() -> String { + "default".to_string() +} + +/// WebDAV v2 同步设置 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WebDavSyncSettings { + #[serde(default)] + pub enabled: bool, + #[serde(default)] + pub base_url: String, + #[serde(default)] + pub username: String, + #[serde(default)] + pub password: String, + #[serde(default = "default_remote_root")] + pub remote_root: String, + #[serde(default = "default_profile")] + pub profile: String, + #[serde(default)] + pub status: WebDavSyncStatus, +} + +impl Default for WebDavSyncSettings { + fn default() -> Self { + Self { + enabled: false, + base_url: String::new(), + username: String::new(), + password: String::new(), + remote_root: default_remote_root(), + profile: default_profile(), + status: WebDavSyncStatus::default(), + } + } +} + +impl WebDavSyncSettings { + pub fn validate(&self) -> Result<(), crate::error::AppError> { + if self.base_url.trim().is_empty() { + return Err(crate::error::AppError::localized( + "webdav.base_url.required", + "WebDAV 地址不能为空", + "WebDAV URL is required.", + )); + } + if self.username.trim().is_empty() { + return Err(crate::error::AppError::localized( + "webdav.username.required", + "WebDAV 用户名不能为空", + "WebDAV username is required.", + )); + } + Ok(()) + } + + pub fn normalize(&mut self) { + self.base_url = self.base_url.trim().to_string(); + self.username = self.username.trim().to_string(); + self.remote_root = self.remote_root.trim().to_string(); + self.profile = self.profile.trim().to_string(); + if self.remote_root.is_empty() { + self.remote_root = default_remote_root(); + } + if self.profile.is_empty() { + self.profile = default_profile(); + } + } + + /// Returns true if all credential fields are blank (no config to persist). + fn is_empty(&self) -> bool { + self.base_url.is_empty() && self.username.is_empty() && self.password.is_empty() + } +} + /// 应用设置结构 /// /// 存储设备级别设置,保存在本地 `~/.cc-switch/settings.json`,不随数据库同步。 @@ -118,6 +214,14 @@ pub struct AppSettings { #[serde(default)] pub skill_sync_method: SyncMethod, + // ===== WebDAV 同步设置 ===== + #[serde(default, skip_serializing_if = "Option::is_none")] + pub webdav_sync: Option, + + // ===== WebDAV 备份设置(旧版,保留向后兼容)===== + #[serde(default, skip_serializing_if = "Option::is_none")] + pub webdav_backup: Option, + // ===== 终端设置 ===== /// 首选终端应用(可选,默认使用系统默认终端) /// - macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty" @@ -155,6 +259,8 @@ impl Default for AppSettings { current_provider_gemini: None, current_provider_opencode: None, skill_sync_method: SyncMethod::default(), + webdav_sync: None, + webdav_backup: None, preferred_terminal: None, } } @@ -205,6 +311,13 @@ impl AppSettings { .map(|s| s.trim()) .filter(|s| matches!(*s, "en" | "zh" | "ja")) .map(|s| s.to_string()); + + if let Some(sync) = &mut self.webdav_sync { + sync.normalize(); + if sync.is_empty() { + self.webdav_sync = None; + } + } } fn load_from_file() -> Self { @@ -245,7 +358,27 @@ fn save_settings_file(settings: &AppSettings) -> Result<(), AppError> { let json = serde_json::to_string_pretty(&normalized) .map_err(|e| AppError::JsonSerialize { source: e })?; - fs::write(&path, json).map_err(|e| AppError::io(&path, e))?; + #[cfg(unix)] + { + use std::fs::OpenOptions; + use std::os::unix::fs::OpenOptionsExt; + + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .mode(0o600) + .open(&path) + .map_err(|e| AppError::io(&path, e))?; + file.write_all(json.as_bytes()) + .map_err(|e| AppError::io(&path, e))?; + } + + #[cfg(not(unix))] + { + fs::write(&path, json).map_err(|e| AppError::io(&path, e))?; + } + Ok(()) } @@ -283,6 +416,15 @@ pub fn get_settings() -> AppSettings { .clone() } +pub fn get_settings_for_frontend() -> AppSettings { + let mut settings = get_settings(); + if let Some(sync) = &mut settings.webdav_sync { + sync.password.clear(); + } + settings.webdav_backup = None; + settings +} + pub fn update_settings(mut new_settings: AppSettings) -> Result<(), AppError> { new_settings.normalize_paths(); save_settings_file(&new_settings)?; @@ -295,6 +437,22 @@ pub fn update_settings(mut new_settings: AppSettings) -> Result<(), AppError> { Ok(()) } +fn mutate_settings(mutator: F) -> Result<(), AppError> +where + F: FnOnce(&mut AppSettings), +{ + let mut guard = settings_store().write().unwrap_or_else(|e| { + log::warn!("设置锁已毒化,使用恢复值: {e}"); + e.into_inner() + }); + let mut next = guard.clone(); + mutator(&mut next); + next.normalize_paths(); + save_settings_file(&next)?; + *guard = next; + Ok(()) +} + /// 从文件重新加载设置到内存缓存 /// 用于导入配置等场景,确保内存缓存与文件同步 pub fn reload_settings() -> Result<(), AppError> { @@ -433,3 +591,26 @@ pub fn get_preferred_terminal() -> Option { .preferred_terminal .clone() } + +// ===== WebDAV 同步设置管理函数 ===== + +/// 获取 WebDAV 同步设置 +pub fn get_webdav_sync_settings() -> Option { + settings_store().read().ok()?.webdav_sync.clone() +} + +/// 保存 WebDAV 同步设置 +pub fn set_webdav_sync_settings(settings: Option) -> Result<(), AppError> { + mutate_settings(|current| { + current.webdav_sync = settings; + }) +} + +/// 仅更新 WebDAV 同步状态,避免覆写 credentials/root/profile 等字段 +pub fn update_webdav_sync_status(status: WebDavSyncStatus) -> Result<(), AppError> { + mutate_settings(|current| { + if let Some(sync) = current.webdav_sync.as_mut() { + sync.status = status; + } + }) +} diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 6f339610f..41e212151 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -39,6 +39,7 @@ import { SkillSyncMethodSettings } from "@/components/settings/SkillSyncMethodSe import { TerminalSettings } from "@/components/settings/TerminalSettings"; import { DirectorySettings } from "@/components/settings/DirectorySettings"; import { ImportExportSection } from "@/components/settings/ImportExportSection"; +import { WebdavSyncSection } from "@/components/settings/WebdavSyncSection"; import { AboutSection } from "@/components/settings/AboutSection"; import { GlobalProxySettings } from "@/components/settings/GlobalProxySettings"; import { ProxyPanel } from "@/components/proxy"; @@ -595,6 +596,11 @@ export function SettingsPage({ onExport={exportConfig} onClear={clearSelection} /> +

+ +
diff --git a/src/components/settings/WebdavSyncSection.tsx b/src/components/settings/WebdavSyncSection.tsx new file mode 100644 index 000000000..1d4c48ab7 --- /dev/null +++ b/src/components/settings/WebdavSyncSection.tsx @@ -0,0 +1,775 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import type { ReactNode } from "react"; +import { + Link2, + UploadCloud, + DownloadCloud, + Loader2, + Save, + Check, + Info, + AlertTriangle, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { settingsApi } from "@/lib/api"; +import type { RemoteSnapshotInfo, WebDavSyncSettings } from "@/types"; + +// ─── WebDAV service presets ───────────────────────────────── + +interface WebDavPreset { + id: string; + label: string; + baseUrl: string; + hint: string; + matchPattern?: string; // substring match on URL +} + +const WEBDAV_PRESETS: WebDavPreset[] = [ + { + id: "jianguoyun", + label: "settings.webdavSync.presets.jianguoyun", + baseUrl: "https://dav.jianguoyun.com/dav/", + hint: "settings.webdavSync.presets.jianguoyunHint", + matchPattern: "jianguoyun.com", + }, + { + id: "nextcloud", + label: "settings.webdavSync.presets.nextcloud", + baseUrl: "https://your-server/remote.php/dav/files/USERNAME/", + hint: "settings.webdavSync.presets.nextcloudHint", + matchPattern: "remote.php/dav", + }, + { + id: "synology", + label: "settings.webdavSync.presets.synology", + baseUrl: "http://your-nas-ip:5005/", + hint: "settings.webdavSync.presets.synologyHint", + matchPattern: ":5005", + }, + { + id: "custom", + label: "settings.webdavSync.presets.custom", + baseUrl: "", + hint: "", + }, +]; + +/** Match a URL to one of the preset providers, or "custom". */ +function detectPreset(url: string): string { + if (!url) return "custom"; + for (const preset of WEBDAV_PRESETS) { + if (preset.matchPattern && url.includes(preset.matchPattern)) { + return preset.id; + } + } + return "custom"; +} + +/** Format an RFC 3339 date string for display; falls back to raw string. */ +function formatDate(rfc3339: string): string { + const d = new Date(rfc3339); + return Number.isNaN(d.getTime()) ? rfc3339 : d.toLocaleString(); +} + +// ─── Types ────────────────────────────────────────────────── + +type ActionState = + | "idle" + | "testing" + | "saving" + | "uploading" + | "downloading" + | "fetching_remote"; + +type DialogType = "upload" | "download" | null; + +interface WebdavSyncSectionProps { + config?: WebDavSyncSettings; +} + +// ─── ActionButton ─────────────────────────────────────────── + +/** Reusable button with loading spinner. */ +function ActionButton({ + actionState, + targetState, + alsoActiveFor, + icon: Icon, + activeLabel, + idleLabel, + disabled, + ...props +}: { + actionState: ActionState; + targetState: ActionState; + alsoActiveFor?: ActionState[]; + icon: LucideIcon; + activeLabel: ReactNode; + idleLabel: ReactNode; +} & Omit, "children">) { + const isActive = + actionState === targetState || + (alsoActiveFor?.includes(actionState) ?? false); + return ( + + ); +} + +// ─── Main component ───────────────────────────────────────── + +export function WebdavSyncSection({ config }: WebdavSyncSectionProps) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const [actionState, setActionState] = useState("idle"); + const [dirty, setDirty] = useState(false); + const [passwordTouched, setPasswordTouched] = useState(false); + const [justSaved, setJustSaved] = useState(false); + const justSavedTimerRef = useRef | null>(null); + + // Local form state — credentials are only persisted on explicit "Save". + const [form, setForm] = useState(() => ({ + baseUrl: config?.baseUrl ?? "", + username: config?.username ?? "", + password: config?.password ?? "", + remoteRoot: config?.remoteRoot ?? "cc-switch-sync", + profile: config?.profile ?? "default", + })); + + // Preset selector — derived from initial URL, updated on user selection + const [presetId, setPresetId] = useState(() => + detectPreset(config?.baseUrl ?? ""), + ); + + const activePreset = WEBDAV_PRESETS.find((p) => p.id === presetId); + + // Confirmation dialog state + const [dialogType, setDialogType] = useState(null); + const [remoteInfo, setRemoteInfo] = useState(null); + + const closeDialog = useCallback(() => { + setDialogType(null); + setRemoteInfo(null); + }, []); + + // Cleanup justSaved timer on unmount + useEffect(() => { + return () => { + if (justSavedTimerRef.current) clearTimeout(justSavedTimerRef.current); + }; + }, []); + + // Sync form when config is loaded/updated from backend, but not while user is editing + useEffect(() => { + if (!config || dirty) return; + setForm({ + baseUrl: config.baseUrl ?? "", + username: config.username ?? "", + password: config.password ?? "", + remoteRoot: config.remoteRoot ?? "cc-switch-sync", + profile: config.profile ?? "default", + }); + setPasswordTouched(false); + setPresetId(detectPreset(config.baseUrl ?? "")); + }, [config, dirty]); + + const updateField = useCallback((field: keyof typeof form, value: string) => { + setForm((prev) => ({ ...prev, [field]: value })); + if (field === "password") { + setPasswordTouched(true); + } + setDirty(true); + setJustSaved(false); + if (justSavedTimerRef.current) { + clearTimeout(justSavedTimerRef.current); + justSavedTimerRef.current = null; + } + }, []); + + const handlePresetChange = useCallback((id: string) => { + setPresetId(id); + const preset = WEBDAV_PRESETS.find((p) => p.id === id); + if (preset?.baseUrl) { + setForm((prev) => ({ ...prev, baseUrl: preset.baseUrl })); + setDirty(true); + setJustSaved(false); + if (justSavedTimerRef.current) { + clearTimeout(justSavedTimerRef.current); + justSavedTimerRef.current = null; + } + } + }, []); + + // When user edits the URL, check if it still matches the current preset on blur + const handleBaseUrlBlur = useCallback(() => { + if (presetId === "custom") return; + const detected = detectPreset(form.baseUrl); + if (detected !== presetId) { + setPresetId("custom"); + } + }, [form.baseUrl, presetId]); + + const buildSettings = useCallback((): WebDavSyncSettings | null => { + const baseUrl = form.baseUrl.trim(); + if (!baseUrl) return null; + return { + enabled: true, + baseUrl, + username: form.username.trim(), + password: form.password, + remoteRoot: form.remoteRoot.trim() || "cc-switch-sync", + profile: form.profile.trim() || "default", + }; + }, [form]); + + // ─── Handlers ─────────────────────────────────────────── + + const handleTest = useCallback(async () => { + const settings = buildSettings(); + if (!settings) { + toast.error(t("settings.webdavSync.missingUrl")); + return; + } + setActionState("testing"); + try { + await settingsApi.webdavTestConnection(settings, !passwordTouched); + toast.success(t("settings.webdavSync.testSuccess")); + } catch (error) { + toast.error( + t("settings.webdavSync.testFailed", { + error: (error as Error)?.message ?? String(error), + }), + ); + } finally { + setActionState("idle"); + } + }, [buildSettings, passwordTouched, t]); + + const handleSave = useCallback(async () => { + const settings = buildSettings(); + if (!settings) { + toast.error(t("settings.webdavSync.missingUrl")); + return; + } + setActionState("saving"); + try { + await settingsApi.webdavSyncSaveSettings(settings, passwordTouched); + setDirty(false); + setPasswordTouched(false); + // Show "saved" indicator for 2 seconds + setJustSaved(true); + if (justSavedTimerRef.current) clearTimeout(justSavedTimerRef.current); + justSavedTimerRef.current = setTimeout(() => { + setJustSaved(false); + justSavedTimerRef.current = null; + }, 2000); + await queryClient.invalidateQueries(); + } catch (error) { + toast.error( + t("settings.webdavSync.saveFailed", { + error: (error as Error)?.message ?? String(error), + }), + ); + setActionState("idle"); + return; + } + + // Auto-test connection after save + setActionState("testing"); + try { + await settingsApi.webdavTestConnection(settings, true); + toast.success(t("settings.webdavSync.saveAndTestSuccess")); + } catch (error) { + toast.warning( + t("settings.webdavSync.saveAndTestFailed", { + error: (error as Error)?.message ?? String(error), + }), + ); + } finally { + setActionState("idle"); + } + }, [buildSettings, passwordTouched, queryClient, t]); + + /** Fetch remote info, then open upload confirmation dialog. */ + const handleUploadClick = useCallback(async () => { + if (dirty) { + toast.error(t("settings.webdavSync.unsavedChanges")); + return; + } + setActionState("fetching_remote"); + try { + const info = await settingsApi.webdavSyncFetchRemoteInfo(); + if ("empty" in info) { + setRemoteInfo(null); + } else { + setRemoteInfo(info); + } + setDialogType("upload"); + } catch { + setRemoteInfo(null); + toast.error(t("settings.webdavSync.fetchRemoteFailed")); + setActionState("idle"); + return; + } + setActionState("idle"); + }, [dirty, t]); + + /** Actually perform the upload after user confirms. */ + const handleUploadConfirm = useCallback(async () => { + if (dirty) { + toast.error(t("settings.webdavSync.unsavedChanges")); + return; + } + closeDialog(); + setActionState("uploading"); + try { + await settingsApi.webdavSyncUpload(); + toast.success(t("settings.webdavSync.uploadSuccess")); + await queryClient.invalidateQueries(); + } catch (error) { + toast.error( + t("settings.webdavSync.uploadFailed", { + error: (error as Error)?.message ?? String(error), + }), + ); + } finally { + setActionState("idle"); + } + }, [closeDialog, dirty, queryClient, t]); + + /** Fetch remote info, then open download confirmation dialog. */ + const handleDownloadClick = useCallback(async () => { + if (dirty) { + toast.error(t("settings.webdavSync.unsavedChanges")); + return; + } + setActionState("fetching_remote"); + try { + const info = await settingsApi.webdavSyncFetchRemoteInfo(); + if ("empty" in info) { + toast.info(t("settings.webdavSync.noRemoteData")); + return; + } + if (!info.compatible) { + toast.error( + t("settings.webdavSync.incompatibleVersion", { + version: info.version, + }), + ); + return; + } + setRemoteInfo(info); + setDialogType("download"); + } catch (error) { + toast.error( + t("settings.webdavSync.downloadFailed", { + error: (error as Error)?.message ?? String(error), + }), + ); + } finally { + setActionState("idle"); + } + }, [dirty, t]); + + /** Actually perform the download after user confirms. */ + const handleDownloadConfirm = useCallback(async () => { + if (dirty) { + toast.error(t("settings.webdavSync.unsavedChanges")); + return; + } + closeDialog(); + setActionState("downloading"); + try { + await settingsApi.webdavSyncDownload(); + toast.success(t("settings.webdavSync.downloadSuccess")); + await queryClient.invalidateQueries(); + } catch (error) { + toast.error( + t("settings.webdavSync.downloadFailed", { + error: (error as Error)?.message ?? String(error), + }), + ); + } finally { + setActionState("idle"); + } + }, [closeDialog, dirty, queryClient, t]); + + // ─── Derived state ────────────────────────────────────── + + const isLoading = actionState !== "idle"; + const hasSavedConfig = Boolean( + config?.baseUrl?.trim() && config?.username?.trim(), + ); + + const lastSyncAt = config?.status?.lastSyncAt; + const lastSyncDisplay = lastSyncAt + ? new Date(lastSyncAt * 1000).toLocaleString() + : null; + + // ─── Render ───────────────────────────────────────────── + + return ( +
+
+

+ {t("settings.webdavSync.title")} +

+

+ {t("settings.webdavSync.description")} +

+
+ +
+ {/* Config fields */} +
+ {/* Service preset selector */} +
+ + +
+ + {/* Server URL */} +
+ + updateField("baseUrl", e.target.value)} + onBlur={handleBaseUrlBlur} + placeholder={t("settings.webdavSync.baseUrlPlaceholder")} + className="text-xs flex-1" + disabled={isLoading} + /> +
+ + {/* Username */} +
+ + updateField("username", e.target.value)} + placeholder={t("settings.webdavSync.usernamePlaceholder")} + className="text-xs flex-1" + disabled={isLoading} + /> +
+ + {/* Password */} +
+ + updateField("password", e.target.value)} + placeholder={t("settings.webdavSync.passwordPlaceholder")} + className="text-xs flex-1" + autoComplete="off" + disabled={isLoading} + /> +
+ + {/* Preset hint */} + {activePreset?.hint && ( +
+ + {t(activePreset.hint)} +
+ )} + + {/* Remote Root */} +
+ + updateField("remoteRoot", e.target.value)} + placeholder="cc-switch-sync" + className="text-xs flex-1" + disabled={isLoading} + /> +
+ + {/* Profile */} +
+ + updateField("profile", e.target.value)} + placeholder="default" + className="text-xs flex-1" + disabled={isLoading} + /> +
+
+ + {/* Last sync time */} + {lastSyncDisplay && ( +

+ {t("settings.webdavSync.lastSync", { time: lastSyncDisplay })} +

+ )} + + {/* Config buttons + save status */} +
+ + + + {/* Save status indicator */} + {dirty && ( + + + {t("settings.webdavSync.unsaved")} + + )} + {!dirty && justSaved && ( + + + {t("settings.webdavSync.saved")} + + )} +
+ + {/* Sync buttons */} +
+ + +
+ {!hasSavedConfig && ( +

+ {t("settings.webdavSync.saveBeforeSync")} +

+ )} +
+ + {/* ─── Upload confirmation dialog ──────────────────── */} + { + if (!open) closeDialog(); + }} + > + + + + + {t("settings.webdavSync.confirmUpload.title")} + + +
+

{t("settings.webdavSync.confirmUpload.content")}

+
    +
  • {t("settings.webdavSync.confirmUpload.dbItem")}
  • +
  • {t("settings.webdavSync.confirmUpload.skillsItem")}
  • +
+

+ {t("settings.webdavSync.confirmUpload.targetPath")} + {": "} + + /{form.remoteRoot.trim() || "cc-switch-sync"}/v2/ + {form.profile.trim() || "default"} + +

+ {remoteInfo && ( +
+

+ {t("settings.webdavSync.confirmUpload.existingData")} +

+
+
+ {t("settings.webdavSync.confirmUpload.deviceName")} +
+
+ + {remoteInfo.deviceName} + +
+
+ {t("settings.webdavSync.confirmUpload.createdAt")} +
+
{formatDate(remoteInfo.createdAt)}
+
+
+ )} + {remoteInfo && ( +

+ {t("settings.webdavSync.confirmUpload.warning")} +

+ )} +
+
+
+ + + + +
+
+ + {/* ─── Download confirmation dialog ────────────────── */} + { + if (!open) closeDialog(); + }} + > + + + + + {t("settings.webdavSync.confirmDownload.title")} + + +
+ {remoteInfo && ( +
+
+ {t("settings.webdavSync.confirmDownload.deviceName")} +
+
+ + {remoteInfo.deviceName} + +
+
+ {t("settings.webdavSync.confirmDownload.createdAt")} +
+
{formatDate(remoteInfo.createdAt)}
+
+ {t("settings.webdavSync.confirmDownload.artifacts")} +
+
{remoteInfo.artifacts.join(", ")}
+
+ )} +

+ {t("settings.webdavSync.confirmDownload.warning")} +

+
+
+
+ + + + +
+
+
+ ); +} diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 55d73d0e1..402da1682 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -135,9 +135,10 @@ export function useSettings(): UseSettingsResult { const sanitizedOpencodeDir = sanitizeDir( mergedSettings.opencodeConfigDir, ); + const { webdavSync: _ignoredWebdavSync, ...restSettings } = mergedSettings; const payload: Settings = { - ...mergedSettings, + ...restSettings, claudeConfigDir: sanitizedClaudeDir, codexConfigDir: sanitizedCodexDir, geminiConfigDir: sanitizedGeminiDir, @@ -251,9 +252,10 @@ export function useSettings(): UseSettingsResult { const previousCodexDir = sanitizeDir(data?.codexConfigDir); const previousGeminiDir = sanitizeDir(data?.geminiConfigDir); const previousOpencodeDir = sanitizeDir(data?.opencodeConfigDir); + const { webdavSync: _ignoredWebdavSync, ...restSettings } = mergedSettings; const payload: Settings = { - ...mergedSettings, + ...restSettings, claudeConfigDir: sanitizedClaudeDir, codexConfigDir: sanitizedCodexDir, geminiConfigDir: sanitizedGeminiDir, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7c65a6857..695a776fb 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -266,6 +266,77 @@ "selectFileFailed": "Please choose a valid SQL backup file", "configCorrupted": "SQL file may be corrupted or invalid", "backupId": "Backup ID", + "webdavSync": { + "title": "WebDAV Cloud Sync", + "description": "Sync database and skill configurations across devices via WebDAV.", + "baseUrl": "WebDAV Server URL", + "baseUrlPlaceholder": "https://dav.example.com/dav/", + "username": "WebDAV Account", + "usernamePlaceholder": "Email or username", + "password": "WebDAV Password", + "passwordPlaceholder": "App password", + "remoteRoot": "Remote Root Directory", + "profile": "Sync Profile Name", + "test": "Test Connection", + "testing": "Testing...", + "testSuccess": "Connection successful", + "testFailed": "Connection failed: {{error}}", + "save": "Save Config", + "saving": "Saving...", + "saveFailed": "Failed to save config: {{error}}", + "upload": "Upload to Cloud", + "uploading": "Uploading...", + "uploadSuccess": "Uploaded to WebDAV", + "uploadFailed": "Upload failed: {{error}}", + "download": "Download from Cloud", + "downloading": "Downloading...", + "downloadSuccess": "Downloaded and restored from WebDAV", + "downloadFailed": "Download failed: {{error}}", + "lastSync": "Last sync: {{time}}", + "missingUrl": "Please enter the WebDAV server URL", + "presets": { + "label": "Provider", + "jianguoyun": "Jianguoyun", + "jianguoyunHint": "Generate an \"App Password\" in Jianguoyun security settings. Do not use your login password.", + "nextcloud": "Nextcloud", + "nextcloudHint": "Replace your-server with your Nextcloud domain and USERNAME with your username.", + "synology": "Synology NAS", + "synologyHint": "Install and enable the WebDAV Server package in Synology Package Center first.", + "custom": "Custom" + }, + "remoteRootDefault": "Default: cc-switch-sync", + "profileDefault": "Default: default", + "saveAndTestSuccess": "Config saved, connection OK", + "saveAndTestFailed": "Config saved, but connection test failed: {{error}}", + "noRemoteData": "No sync data found on the remote server", + "incompatibleVersion": "Remote data version incompatible (v{{version}}), current supports v2", + "unsaved": "Unsaved", + "saved": "Saved", + "unsavedChanges": "Please save config first", + "saveBeforeSync": "Save configuration first to enable upload/download.", + "fetchingRemote": "Fetching remote info...", + "fetchRemoteFailed": "Failed to fetch remote info. Please check configuration and network.", + "confirmDownload": { + "title": "Restore from Cloud", + "deviceName": "Uploaded by", + "createdAt": "Uploaded at", + "artifacts": "Contents", + "warning": "This will overwrite all local data and skill configurations", + "confirm": "Confirm Restore" + }, + "confirmUpload": { + "title": "Upload to Cloud", + "content": "The following will be synced to the WebDAV server:", + "dbItem": "Database (all provider configs and data)", + "skillsItem": "Skills (all custom skills)", + "targetPath": "Target path", + "existingData": "Existing cloud data", + "deviceName": "Uploaded by", + "createdAt": "Uploaded at", + "warning": "This will overwrite existing sync data on the remote server", + "confirm": "Confirm Upload" + } + }, "autoReload": "Data refreshed", "languageOptionChinese": "中文", "languageOptionEnglish": "English", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index f8c9243fe..825e11a6d 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -266,6 +266,77 @@ "selectFileFailed": "有効な SQL バックアップファイルを選択してください", "configCorrupted": "SQL ファイルが壊れているか形式が無効な可能性があります", "backupId": "バックアップ ID", + "webdavSync": { + "title": "WebDAV クラウド同期", + "description": "WebDAV を使ってデバイス間でデータベースとスキル設定を同期します。", + "baseUrl": "WebDAV サーバー URL", + "baseUrlPlaceholder": "https://example.com/remote.php/dav/files/user", + "username": "ユーザー名", + "usernamePlaceholder": "メールアドレスまたはユーザー名", + "password": "パスワード", + "passwordPlaceholder": "アプリパスワード", + "remoteRoot": "リモートルートディレクトリ", + "profile": "同期プロファイル名", + "test": "接続テスト", + "testing": "テスト中...", + "testSuccess": "接続成功", + "testFailed": "接続失敗:{{error}}", + "save": "設定を保存", + "saving": "保存中...", + "saveFailed": "設定の保存に失敗しました:{{error}}", + "upload": "クラウドにアップロード", + "uploading": "アップロード中...", + "uploadSuccess": "WebDAV にアップロードしました", + "uploadFailed": "アップロードに失敗しました:{{error}}", + "download": "クラウドからダウンロード", + "downloading": "ダウンロード中...", + "downloadSuccess": "WebDAV からダウンロード・復元しました", + "downloadFailed": "ダウンロードに失敗しました:{{error}}", + "lastSync": "前回の同期:{{time}}", + "missingUrl": "WebDAV サーバー URL を入力してください", + "presets": { + "label": "サービス", + "jianguoyun": "坚果云", + "jianguoyunHint": "坚果云の「セキュリティ設定」で「サードパーティアプリパスワード」を生成してください。ログインパスワードは使用しないでください。", + "nextcloud": "Nextcloud", + "nextcloudHint": "your-server を Nextcloud サーバーのアドレスに、USERNAME をユーザー名に置き換えてください。", + "synology": "Synology NAS", + "synologyHint": "Synology の「パッケージセンター」で WebDAV Server パッケージをインストール・有効化してください。", + "custom": "カスタム" + }, + "remoteRootDefault": "デフォルト: cc-switch-sync", + "profileDefault": "デフォルト: default", + "saveAndTestSuccess": "設定を保存しました。接続正常です", + "saveAndTestFailed": "設定を保存しましたが、接続テストに失敗しました:{{error}}", + "noRemoteData": "クラウドに同期データが見つかりません", + "incompatibleVersion": "リモートデータのバージョンに互換性がありません(v{{version}})。現在 v2 をサポートしています", + "unsaved": "未保存", + "saved": "保存済み", + "unsavedChanges": "先に設定を保存してください", + "saveBeforeSync": "アップロード/ダウンロードを有効にするには、先に設定を保存してください。", + "fetchingRemote": "リモート情報を取得中...", + "fetchRemoteFailed": "リモート情報の取得に失敗しました。設定とネットワークを確認してください。", + "confirmDownload": { + "title": "クラウドから復元", + "deviceName": "アップロード元", + "createdAt": "アップロード日時", + "artifacts": "内容", + "warning": "ローカルのすべてのデータとスキル設定が上書きされます", + "confirm": "復元を実行" + }, + "confirmUpload": { + "title": "クラウドにアップロード", + "content": "以下の内容を WebDAV サーバーに同期します:", + "dbItem": "データベース(すべてのプロバイダー設定とデータ)", + "skillsItem": "スキル(すべてのカスタムスキル)", + "targetPath": "保存先パス", + "existingData": "クラウドの既存データ", + "deviceName": "アップロード元", + "createdAt": "アップロード日時", + "warning": "リモートの既存同期データが上書きされます", + "confirm": "アップロードを実行" + } + }, "autoReload": "データを更新しました", "languageOptionChinese": "中文", "languageOptionEnglish": "English", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index a5f54bd4e..c044c40e7 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -266,6 +266,77 @@ "selectFileFailed": "请选择有效的 SQL 备份文件", "configCorrupted": "SQL 文件可能已损坏或格式不正确", "backupId": "备份ID", + "webdavSync": { + "title": "WebDAV 云同步", + "description": "通过 WebDAV 在多设备间同步数据库和技能配置。", + "baseUrl": "WebDAV 服务器地址", + "baseUrlPlaceholder": "https://dav.jianguoyun.com/dav/", + "username": "WebDAV 账户", + "usernamePlaceholder": "邮箱账号", + "password": "WebDAV 密码", + "passwordPlaceholder": "应用密码(坚果云请使用「第三方应用密码」)", + "remoteRoot": "远程根目录", + "profile": "同步配置名", + "test": "测试连接", + "testing": "测试中...", + "testSuccess": "连接成功", + "testFailed": "连接失败:{{error}}", + "save": "保存配置", + "saving": "保存中...", + "saveFailed": "保存配置失败:{{error}}", + "upload": "上传到云端", + "uploading": "上传中...", + "uploadSuccess": "已上传到 WebDAV", + "uploadFailed": "上传失败:{{error}}", + "download": "从云端下载", + "downloading": "下载中...", + "downloadSuccess": "已从 WebDAV 下载并恢复", + "downloadFailed": "下载失败:{{error}}", + "lastSync": "上次同步:{{time}}", + "missingUrl": "请填写 WebDAV 服务器地址", + "presets": { + "label": "服务商", + "jianguoyun": "坚果云", + "jianguoyunHint": "请在坚果云「安全选项」中生成「第三方应用密码」,不要使用登录密码。", + "nextcloud": "Nextcloud", + "nextcloudHint": "请将 your-server 替换为你的 Nextcloud 服务器地址,USERNAME 替换为你的用户名。", + "synology": "群晖 NAS", + "synologyHint": "请先在群晖「套件中心」安装并启用 WebDAV Server 套件。", + "custom": "自定义" + }, + "remoteRootDefault": "默认: cc-switch-sync", + "profileDefault": "默认: default", + "saveAndTestSuccess": "配置已保存,连接正常", + "saveAndTestFailed": "配置已保存,但连接测试失败:{{error}}", + "noRemoteData": "云端没有找到同步数据", + "incompatibleVersion": "远端数据版本不兼容(v{{version}}),当前支持 v2", + "unsaved": "未保存", + "saved": "已保存", + "unsavedChanges": "请先保存配置", + "saveBeforeSync": "请先保存配置,再使用上传/下载。", + "fetchingRemote": "获取远端信息...", + "fetchRemoteFailed": "获取远端信息失败,请检查配置和网络后重试。", + "confirmDownload": { + "title": "从云端恢复", + "deviceName": "上传设备", + "createdAt": "上传时间", + "artifacts": "包含内容", + "warning": "恢复将覆盖本地所有数据和技能配置", + "confirm": "确认恢复" + }, + "confirmUpload": { + "title": "上传到云端", + "content": "将同步以下内容到 WebDAV 服务器:", + "dbItem": "数据库(所有 Provider 配置和数据)", + "skillsItem": "技能包(所有自定义技能)", + "targetPath": "目标路径", + "existingData": "云端已有数据", + "deviceName": "上传设备", + "createdAt": "上传时间", + "warning": "将覆盖云端已有的同步数据", + "confirm": "确认上传" + } + }, "autoReload": "数据已刷新", "languageOptionChinese": "中文", "languageOptionEnglish": "English", diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index cd8de3666..6b791659d 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -1,5 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; -import type { Settings } from "@/types"; +import type { Settings, WebDavSyncSettings, RemoteSnapshotInfo } from "@/types"; import type { AppId } from "./types"; export interface ConfigTransferResult { @@ -9,6 +9,15 @@ export interface ConfigTransferResult { backupId?: string; } +export interface WebDavTestResult { + success: boolean; + message?: string; +} + +export interface WebDavSyncResult { + status: string; +} + export const settingsApi = { async get(): Promise { return await invoke("get_settings"); @@ -93,6 +102,42 @@ export const settingsApi = { return await invoke("import_config_from_file", { filePath }); }, + // ─── WebDAV v2 sync ─────────────────────────────────────── + + async webdavTestConnection( + settings: WebDavSyncSettings, + preserveEmptyPassword = true, + ): Promise { + return await invoke("webdav_test_connection", { + settings, + preserveEmptyPassword, + }); + }, + + async webdavSyncUpload(): Promise { + return await invoke("webdav_sync_upload"); + }, + + async webdavSyncDownload(): Promise { + return await invoke("webdav_sync_download"); + }, + + async webdavSyncSaveSettings( + settings: WebDavSyncSettings, + passwordTouched = false, + ): Promise<{ success: boolean }> { + return await invoke("webdav_sync_save_settings", { + settings, + passwordTouched, + }); + }, + + async webdavSyncFetchRemoteInfo(): Promise< + RemoteSnapshotInfo | { empty: true } + > { + return await invoke("webdav_sync_fetch_remote_info"); + }, + async syncCurrentProvidersLive(): Promise { const result = (await invoke("sync_current_providers_live")) as { success?: boolean; diff --git a/src/lib/schemas/settings.ts b/src/lib/schemas/settings.ts index 74db80585..d226ef34c 100644 --- a/src/lib/schemas/settings.ts +++ b/src/lib/schemas/settings.ts @@ -28,6 +28,27 @@ export const settingsSchema = z.object({ // Skill 同步设置 skillSyncMethod: z.enum(["auto", "symlink", "copy"]).optional(), + + // WebDAV v2 同步设置(通过专用命令保存,schema 仅用于读取) + webdavSync: z + .object({ + enabled: z.boolean().optional(), + baseUrl: z.string().trim().optional().or(z.literal("")), + username: z.string().trim().optional().or(z.literal("")), + password: z.string().optional(), + remoteRoot: z.string().trim().optional().or(z.literal("")), + profile: z.string().trim().optional().or(z.literal("")), + status: z + .object({ + lastSyncAt: z.number().nullable().optional(), + lastError: z.string().nullable().optional(), + lastRemoteEtag: z.string().nullable().optional(), + lastLocalManifestHash: z.string().nullable().optional(), + lastRemoteManifestHash: z.string().nullable().optional(), + }) + .optional(), + }) + .optional(), }); export type SettingsFormData = z.infer; diff --git a/src/types.ts b/src/types.ts index 94af23ea7..5d219c4f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -162,6 +162,36 @@ export interface VisibleApps { opencode: boolean; } +// WebDAV v2 同步状态 +export interface WebDavSyncStatus { + lastSyncAt?: number | null; + lastError?: string | null; + lastRemoteEtag?: string | null; + lastLocalManifestHash?: string | null; + lastRemoteManifestHash?: string | null; +} + +// WebDAV v2 同步配置 +export interface WebDavSyncSettings { + enabled?: boolean; + baseUrl?: string; + username?: string; + password?: string; + remoteRoot?: string; + profile?: string; + status?: WebDavSyncStatus; +} + +// 远端快照信息(下载前预览) +export interface RemoteSnapshotInfo { + deviceName: string; + createdAt: string; + snapshotId: string; + version: number; + compatible: boolean; + artifacts: string[]; +} + // 应用设置类型(用于设置对话框与 Tauri API) // 存储在本地 ~/.cc-switch/settings.json,不随数据库同步 export interface Settings { @@ -206,6 +236,9 @@ export interface Settings { // Skill 同步方式:auto(默认,优先 symlink)、symlink、copy skillSyncMethod?: SkillSyncMethod; + // ===== WebDAV v2 同步设置 ===== + webdavSync?: WebDavSyncSettings; + // ===== 终端设置 ===== // 首选终端应用(可选,默认使用系统默认终端) // macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty" diff --git a/tests/components/SettingsDialog.test.tsx b/tests/components/SettingsDialog.test.tsx index 2e8015843..ec29f17fd 100644 --- a/tests/components/SettingsDialog.test.tsx +++ b/tests/components/SettingsDialog.test.tsx @@ -1,7 +1,8 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import "@testing-library/jest-dom"; -import { createContext, useContext } from "react"; +import { createContext, useContext, type ComponentProps } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { SettingsPage } from "@/components/settings/SettingsPage"; const toastSuccessMock = vi.fn(); @@ -156,6 +157,7 @@ vi.mock("@/components/ui/dialog", () => ({ DialogHeader: ({ children }: any) =>
{children}
, DialogFooter: ({ children }: any) =>
{children}
, DialogTitle: ({ children }: any) =>

{children}

, + DialogDescription: ({ children }: any) =>
{children}
, })); vi.mock("@/components/ui/tabs", () => { @@ -235,8 +237,29 @@ vi.mock("@/components/settings/AboutSection", () => ({ AboutSection: ({ isPortable }: any) =>
about:{String(isPortable)}
, })); +vi.mock("@/components/settings/WebdavSyncSection", () => ({ + WebdavSyncSection: ({ config }: any) => ( +
webdav-sync-section:{config?.baseUrl ?? "none"}
+ ), +})); + let settingsApi: any; +const renderSettingsPage = ( + props?: Partial>, +) => { + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }); + return render( + + + , + ); +}; + describe("SettingsPage Component", () => { beforeEach(async () => { tMock.mockImplementation((key: string) => key); @@ -263,7 +286,7 @@ describe("SettingsPage Component", () => { it("should not render form content when loading", () => { settingsMock = createSettingsMock({ settings: null, isLoading: true }); - render(); + renderSettingsPage(); expect(screen.queryByText("language:zh")).not.toBeInTheDocument(); // 加载状态下显示 spinner 而不是表单内容 @@ -271,13 +294,24 @@ describe("SettingsPage Component", () => { }); it("should reset import/export status when dialog transitions to open", () => { + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }); const { rerender } = render( - , + + + , ); importExportMock.resetStatus.mockClear(); - rerender(); + rerender( + + + , + ); expect(importExportMock.resetStatus).toHaveBeenCalledTimes(1); }); @@ -289,7 +323,7 @@ describe("SettingsPage Component", () => { selectedFile: "/tmp/config.json", }); - render(); + renderSettingsPage({ onOpenChange }); expect(screen.getByText("language:zh")).toBeInTheDocument(); expect(screen.getByText("theme-settings")).toBeInTheDocument(); @@ -306,6 +340,7 @@ describe("SettingsPage Component", () => { fireEvent.click(screen.getByText("settings.tabAdvanced")); fireEvent.click(screen.getByText("settings.advanced.data.title")); + expect(screen.getByText("webdav-sync-section:none")).toBeInTheDocument(); // 有文件时,点击导入按钮执行 importConfig fireEvent.click( @@ -326,13 +361,7 @@ describe("SettingsPage Component", () => { it("should pass onImportSuccess callback to useImportExport hook", async () => { const onImportSuccess = vi.fn(); - render( - , - ); + renderSettingsPage({ onImportSuccess }); expect(useImportExportSpy).toHaveBeenCalledWith( expect.objectContaining({ onImportSuccess }), @@ -349,7 +378,7 @@ describe("SettingsPage Component", () => { const onOpenChange = vi.fn(); importExportMock = createImportExportMock(); - render(); + renderSettingsPage({ onOpenChange }); // 保存按钮在 advanced tab 中 fireEvent.click(screen.getByText("settings.tabAdvanced")); @@ -370,7 +399,7 @@ describe("SettingsPage Component", () => { saveSettings: vi.fn().mockResolvedValue({ requiresRestart: true }), }); - render(); + renderSettingsPage(); expect( await screen.findByText("settings.restartRequired"), @@ -390,7 +419,7 @@ describe("SettingsPage Component", () => { const onOpenChange = vi.fn(); settingsMock = createSettingsMock({ requiresRestart: true }); - render(); + renderSettingsPage({ onOpenChange }); expect( await screen.findByText("settings.restartRequired"), @@ -409,7 +438,7 @@ describe("SettingsPage Component", () => { }); it("should trigger directory management callbacks inside advanced tab", () => { - render(); + renderSettingsPage(); fireEvent.click(screen.getByText("settings.tabAdvanced")); fireEvent.click(screen.getByText("settings.advanced.configDir.title")); diff --git a/tests/components/WebdavSyncSection.test.tsx b/tests/components/WebdavSyncSection.test.tsx new file mode 100644 index 000000000..27a487a7b --- /dev/null +++ b/tests/components/WebdavSyncSection.test.tsx @@ -0,0 +1,370 @@ +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import "@testing-library/jest-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { WebdavSyncSection } from "@/components/settings/WebdavSyncSection"; +import type { WebDavSyncSettings } from "@/types"; + +const toastSuccessMock = vi.fn(); +const toastErrorMock = vi.fn(); +const toastWarningMock = vi.fn(); +const toastInfoMock = vi.fn(); + +vi.mock("sonner", () => ({ + toast: { + success: (...args: unknown[]) => toastSuccessMock(...args), + error: (...args: unknown[]) => toastErrorMock(...args), + warning: (...args: unknown[]) => toastWarningMock(...args), + info: (...args: unknown[]) => toastInfoMock(...args), + }, +})); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock("@/components/ui/button", () => ({ + Button: ({ children, ...props }: any) => , +})); + +vi.mock("@/components/ui/input", () => ({ + Input: (props: any) => , +})); + +vi.mock("@/components/ui/select", () => ({ + Select: ({ value, onValueChange, children }: any) => ( + + ), + SelectTrigger: ({ children }: any) => <>{children}, + SelectValue: () => null, + SelectContent: ({ children }: any) => <>{children}, + SelectItem: ({ value, children }: any) => ( + + ), +})); + +vi.mock("@/components/ui/dialog", () => ({ + Dialog: ({ open, children }: any) => (open ?
{children}
: null), + DialogContent: ({ children }: any) =>
{children}
, + DialogDescription: ({ children }: any) =>
{children}
, + DialogFooter: ({ children }: any) =>
{children}
, + DialogHeader: ({ children }: any) =>
{children}
, + DialogTitle: ({ children }: any) =>

{children}

, +})); + +const { settingsApiMock } = vi.hoisted(() => ({ + settingsApiMock: { + webdavTestConnection: vi.fn(), + webdavSyncSaveSettings: vi.fn(), + webdavSyncFetchRemoteInfo: vi.fn(), + webdavSyncUpload: vi.fn(), + webdavSyncDownload: vi.fn(), + }, +})); + +vi.mock("@/lib/api", () => ({ + settingsApi: settingsApiMock, +})); + +const baseConfig: WebDavSyncSettings = { + enabled: true, + baseUrl: "https://dav.example.com/dav/", + username: "alice", + password: "secret", + remoteRoot: "cc-switch-sync", + profile: "default", + status: {}, +}; + +function renderSection(config?: WebDavSyncSettings) { + const client = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + return render( + + + , + ); +} + +describe("WebdavSyncSection", () => { + beforeEach(() => { + toastSuccessMock.mockReset(); + toastErrorMock.mockReset(); + toastWarningMock.mockReset(); + toastInfoMock.mockReset(); + settingsApiMock.webdavTestConnection.mockReset(); + settingsApiMock.webdavSyncSaveSettings.mockReset(); + settingsApiMock.webdavSyncFetchRemoteInfo.mockReset(); + settingsApiMock.webdavSyncUpload.mockReset(); + settingsApiMock.webdavSyncDownload.mockReset(); + + settingsApiMock.webdavSyncSaveSettings.mockResolvedValue({ success: true }); + settingsApiMock.webdavTestConnection.mockResolvedValue({ + success: true, + message: "ok", + }); + settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValue({ + deviceName: "My MacBook", + createdAt: "2026-02-01T10:00:00Z", + snapshotId: "snapshot-1", + version: 2, + compatible: true, + artifacts: ["db.sql", "skills.zip"], + }); + settingsApiMock.webdavSyncUpload.mockResolvedValue({ status: "uploaded" }); + settingsApiMock.webdavSyncDownload.mockResolvedValue({ status: "downloaded" }); + }); + + it("shows validation error when saving without base url", async () => { + renderSection({ ...baseConfig, baseUrl: "" }); + + fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" })); + + expect(toastErrorMock).toHaveBeenCalledWith("settings.webdavSync.missingUrl"); + expect(settingsApiMock.webdavSyncSaveSettings).not.toHaveBeenCalled(); + }); + + it("saves settings and auto tests connection", async () => { + renderSection(baseConfig); + + fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" })); + + await waitFor(() => { + expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledTimes(1); + }); + expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: "https://dav.example.com/dav/", + username: "alice", + password: "secret", + }), + false, + ); + await waitFor(() => { + expect(settingsApiMock.webdavTestConnection).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: "https://dav.example.com/dav/", + }), + true, + ); + }); + expect(toastSuccessMock).toHaveBeenCalledWith( + "settings.webdavSync.saveAndTestSuccess", + ); + }); + + it("blocks upload when there are unsaved changes", async () => { + renderSection(baseConfig); + + fireEvent.change( + screen.getByPlaceholderText("settings.webdavSync.usernamePlaceholder"), + { target: { value: "bob" } }, + ); + fireEvent.click( + screen.getByRole("button", { name: "settings.webdavSync.upload" }), + ); + + expect(toastErrorMock).toHaveBeenCalledWith( + "settings.webdavSync.unsavedChanges", + ); + expect(settingsApiMock.webdavSyncFetchRemoteInfo).not.toHaveBeenCalled(); + }); + + it("disables sync buttons until config is saved", () => { + renderSection(undefined); + + const uploadButton = screen.getByRole("button", { + name: "settings.webdavSync.upload", + }); + const downloadButton = screen.getByRole("button", { + name: "settings.webdavSync.download", + }); + + expect(uploadButton).toBeDisabled(); + expect(downloadButton).toBeDisabled(); + expect(settingsApiMock.webdavSyncFetchRemoteInfo).not.toHaveBeenCalled(); + }); + + it("fetches remote info and uploads after confirmation", async () => { + renderSection(baseConfig); + + fireEvent.click( + screen.getByRole("button", { name: "settings.webdavSync.upload" }), + ); + + await waitFor(() => { + expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); + }); + + fireEvent.click( + screen.getByRole("button", { + name: "settings.webdavSync.confirmUpload.confirm", + }), + ); + + await waitFor(() => { + expect(settingsApiMock.webdavSyncUpload).toHaveBeenCalledTimes(1); + }); + expect(toastSuccessMock).toHaveBeenCalledWith( + "settings.webdavSync.uploadSuccess", + ); + }); + + it("blocks upload confirmation if form changes after dialog opens", async () => { + renderSection(baseConfig); + + fireEvent.click( + screen.getByRole("button", { name: "settings.webdavSync.upload" }), + ); + await waitFor(() => { + expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); + }); + + fireEvent.change(screen.getByPlaceholderText("cc-switch-sync"), { + target: { value: "new-root" }, + }); + fireEvent.click( + screen.getByRole("button", { + name: "settings.webdavSync.confirmUpload.confirm", + }), + ); + + await waitFor(() => { + expect(toastErrorMock).toHaveBeenCalledWith( + "settings.webdavSync.unsavedChanges", + ); + }); + expect(settingsApiMock.webdavSyncUpload).not.toHaveBeenCalled(); + }); + + it("fetches remote info and downloads after confirmation", async () => { + renderSection(baseConfig); + + fireEvent.click( + screen.getByRole("button", { name: "settings.webdavSync.download" }), + ); + + await waitFor(() => { + expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); + }); + + fireEvent.click( + screen.getByRole("button", { + name: "settings.webdavSync.confirmDownload.confirm", + }), + ); + + await waitFor(() => { + expect(settingsApiMock.webdavSyncDownload).toHaveBeenCalledTimes(1); + }); + expect(toastSuccessMock).toHaveBeenCalledWith( + "settings.webdavSync.downloadSuccess", + ); + }); + + it("blocks download confirmation if form changes after dialog opens", async () => { + renderSection(baseConfig); + + fireEvent.click( + screen.getByRole("button", { name: "settings.webdavSync.download" }), + ); + await waitFor(() => { + expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); + }); + + fireEvent.change(screen.getByPlaceholderText("default"), { + target: { value: "new-profile" }, + }); + fireEvent.click( + screen.getByRole("button", { + name: "settings.webdavSync.confirmDownload.confirm", + }), + ); + + await waitFor(() => { + expect(toastErrorMock).toHaveBeenCalledWith( + "settings.webdavSync.unsavedChanges", + ); + }); + expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled(); + }); + + it("shows info when no remote snapshot is found for download", async () => { + settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValueOnce({ empty: true }); + renderSection(baseConfig); + + fireEvent.click( + screen.getByRole("button", { name: "settings.webdavSync.download" }), + ); + + await waitFor(() => { + expect(toastInfoMock).toHaveBeenCalledWith("settings.webdavSync.noRemoteData"); + }); + expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled(); + }); + + it("blocks download when remote snapshot is incompatible", async () => { + settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValueOnce({ + deviceName: "Legacy Machine", + createdAt: "2025-01-01T00:00:00Z", + snapshotId: "legacy-snapshot", + version: 1, + compatible: false, + artifacts: ["db.sql"], + }); + renderSection(baseConfig); + + fireEvent.click( + screen.getByRole("button", { name: "settings.webdavSync.download" }), + ); + + await waitFor(() => { + expect(toastErrorMock).toHaveBeenCalledWith( + "settings.webdavSync.incompatibleVersion", + ); + }); + expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled(); + expect( + screen.queryByRole("button", { + name: "settings.webdavSync.confirmDownload.confirm", + }), + ).not.toBeInTheDocument(); + }); + + it("shows error when download fails after confirmation", async () => { + settingsApiMock.webdavSyncDownload.mockRejectedValueOnce(new Error("boom")); + renderSection(baseConfig); + + fireEvent.click( + screen.getByRole("button", { name: "settings.webdavSync.download" }), + ); + await waitFor(() => { + expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); + }); + + fireEvent.click( + screen.getByRole("button", { + name: "settings.webdavSync.confirmDownload.confirm", + }), + ); + + await waitFor(() => { + expect(toastErrorMock).toHaveBeenCalledWith( + "settings.webdavSync.downloadFailed", + ); + }); + }); +}); diff --git a/tests/integration/SettingsDialog.test.tsx b/tests/integration/SettingsDialog.test.tsx index 0772e4a8c..be1a689f8 100644 --- a/tests/integration/SettingsDialog.test.tsx +++ b/tests/integration/SettingsDialog.test.tsx @@ -28,6 +28,7 @@ vi.mock("@/components/ui/dialog", () => ({ DialogHeader: ({ children }: any) =>
{children}
, DialogFooter: ({ children }: any) =>
{children}
, DialogTitle: ({ children }: any) =>

{children}

, + DialogDescription: ({ children }: any) =>
{children}
, })); const TabsContext = React.createContext<{ From 9c34d04def9cd4eb04afa7209f90c588f1f200c6 Mon Sep 17 00:00:00 2001 From: Dex Miller Date: Sat, 14 Feb 2026 15:29:54 +0800 Subject: [PATCH 08/51] fix(mcp): add missing OpenCode checkbox in MCP add/edit form (#1026) Fixes https://github.com/farion1231/cc-switch/issues/1020 --- src/components/mcp/McpFormModal.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index 5904e49d7..93d12745d 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -561,6 +561,22 @@ const McpFormModal: React.FC = ({ {t("mcp.unifiedPanel.apps.gemini")} + +
+ + setEnabledApps({ ...enabledApps, opencode: checked }) + } + /> + +
From 7f46a0b9104cfd356475e737887afbc469d7174a Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:30:32 +0800 Subject: [PATCH 09/51] feat(core): add OpenClaw to AppType enum and database schema - Add OpenClaw variant to AppType enum - Update is_additive_mode() to return true for OpenClaw - Update McpApps, SkillApps, McpRoot, PromptRoot structs - Add database migration v5 to v6 for enabled_openclaw columns - Update mcp_servers and skills table definitions --- src-tauri/src/app_config.rs | 51 ++++++++++++++++++++++++---- src-tauri/src/database/dao/mcp.rs | 9 +++-- src-tauri/src/database/dao/skills.rs | 19 ++++++----- src-tauri/src/database/mod.rs | 2 +- src-tauri/src/database/schema.rs | 34 ++++++++++++++++++- 5 files changed, 95 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 530f6a798..d53e7f38e 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -15,6 +15,8 @@ pub struct McpApps { pub gemini: bool, #[serde(default)] pub opencode: bool, + #[serde(default)] + pub openclaw: bool, } impl McpApps { @@ -25,6 +27,7 @@ impl McpApps { AppType::Codex => self.codex, AppType::Gemini => self.gemini, AppType::OpenCode => self.opencode, + AppType::OpenClaw => self.openclaw, } } @@ -35,6 +38,7 @@ impl McpApps { AppType::Codex => self.codex = enabled, AppType::Gemini => self.gemini = enabled, AppType::OpenCode => self.opencode = enabled, + AppType::OpenClaw => self.openclaw = enabled, } } @@ -53,12 +57,15 @@ impl McpApps { if self.opencode { apps.push(AppType::OpenCode); } + if self.openclaw { + apps.push(AppType::OpenClaw); + } apps } /// 检查是否所有应用都未启用 pub fn is_empty(&self) -> bool { - !self.claude && !self.codex && !self.gemini && !self.opencode + !self.claude && !self.codex && !self.gemini && !self.opencode && !self.openclaw } } @@ -73,6 +80,8 @@ pub struct SkillApps { pub gemini: bool, #[serde(default)] pub opencode: bool, + #[serde(default)] + pub openclaw: bool, } impl SkillApps { @@ -83,6 +92,7 @@ impl SkillApps { AppType::Codex => self.codex, AppType::Gemini => self.gemini, AppType::OpenCode => self.opencode, + AppType::OpenClaw => self.openclaw, } } @@ -93,6 +103,7 @@ impl SkillApps { AppType::Codex => self.codex = enabled, AppType::Gemini => self.gemini = enabled, AppType::OpenCode => self.opencode = enabled, + AppType::OpenClaw => self.openclaw = enabled, } } @@ -111,12 +122,15 @@ impl SkillApps { if self.opencode { apps.push(AppType::OpenCode); } + if self.openclaw { + apps.push(AppType::OpenClaw); + } apps } /// 检查是否所有应用都未启用 pub fn is_empty(&self) -> bool { - !self.claude && !self.codex && !self.gemini && !self.opencode + !self.claude && !self.codex && !self.gemini && !self.opencode && !self.openclaw } /// 仅启用指定应用(其他应用设为禁用) @@ -222,6 +236,9 @@ pub struct McpRoot { /// OpenCode MCP 配置(v4.0.0+,实际使用 opencode.json) #[serde(default, skip_serializing_if = "McpConfig::is_empty")] pub opencode: McpConfig, + /// OpenClaw MCP 配置(v4.1.0+,实际使用 openclaw.json) + #[serde(default, skip_serializing_if = "McpConfig::is_empty")] + pub openclaw: McpConfig, } impl Default for McpRoot { @@ -234,6 +251,7 @@ impl Default for McpRoot { codex: McpConfig::default(), gemini: McpConfig::default(), opencode: McpConfig::default(), + openclaw: McpConfig::default(), } } } @@ -256,6 +274,8 @@ pub struct PromptRoot { pub gemini: PromptConfig, #[serde(default)] pub opencode: PromptConfig, + #[serde(default)] + pub openclaw: PromptConfig, } use crate::config::{copy_file, get_app_config_dir, get_app_config_path, write_json_file}; @@ -271,6 +291,7 @@ pub enum AppType { Codex, Gemini, OpenCode, + OpenClaw, } impl AppType { @@ -280,15 +301,16 @@ impl AppType { AppType::Codex => "codex", AppType::Gemini => "gemini", AppType::OpenCode => "opencode", + AppType::OpenClaw => "openclaw", } } /// Check if this app uses additive mode /// /// - Switch mode (false): Only the current provider is written to live config (Claude, Codex, Gemini) - /// - Additive mode (true): All providers are written to live config (OpenCode) + /// - Additive mode (true): All providers are written to live config (OpenCode, OpenClaw) pub fn is_additive_mode(&self) -> bool { - matches!(self, AppType::OpenCode) + matches!(self, AppType::OpenCode | AppType::OpenClaw) } /// Return an iterator over all app types @@ -298,6 +320,7 @@ impl AppType { AppType::Codex, AppType::Gemini, AppType::OpenCode, + AppType::OpenClaw, ] .into_iter() } @@ -313,10 +336,11 @@ impl FromStr for AppType { "codex" => Ok(AppType::Codex), "gemini" => Ok(AppType::Gemini), "opencode" => Ok(AppType::OpenCode), + "openclaw" => Ok(AppType::OpenClaw), other => Err(AppError::localized( "unsupported_app", - format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode。"), - format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode."), + format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode, openclaw。"), + format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode, openclaw."), )), } } @@ -336,6 +360,9 @@ pub struct CommonConfigSnippets { #[serde(default, skip_serializing_if = "Option::is_none")] pub opencode: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub openclaw: Option, } impl CommonConfigSnippets { @@ -346,6 +373,7 @@ impl CommonConfigSnippets { AppType::Codex => self.codex.as_ref(), AppType::Gemini => self.gemini.as_ref(), AppType::OpenCode => self.opencode.as_ref(), + AppType::OpenClaw => self.openclaw.as_ref(), } } @@ -356,6 +384,7 @@ impl CommonConfigSnippets { AppType::Codex => self.codex = snippet, AppType::Gemini => self.gemini = snippet, AppType::OpenCode => self.opencode = snippet, + AppType::OpenClaw => self.openclaw = snippet, } } } @@ -396,6 +425,7 @@ impl Default for MultiAppConfig { apps.insert("codex".to_string(), ProviderManager::default()); apps.insert("gemini".to_string(), ProviderManager::default()); apps.insert("opencode".to_string(), ProviderManager::default()); + apps.insert("openclaw".to_string(), ProviderManager::default()); Self { version: 2, @@ -555,6 +585,7 @@ impl MultiAppConfig { AppType::Codex => &self.mcp.codex, AppType::Gemini => &self.mcp.gemini, AppType::OpenCode => &self.mcp.opencode, + AppType::OpenClaw => &self.mcp.openclaw, } } @@ -565,6 +596,7 @@ impl MultiAppConfig { AppType::Codex => &mut self.mcp.codex, AppType::Gemini => &mut self.mcp.gemini, AppType::OpenCode => &mut self.mcp.opencode, + AppType::OpenClaw => &mut self.mcp.openclaw, } } @@ -579,6 +611,7 @@ impl MultiAppConfig { Self::auto_import_prompt_if_exists(&mut config, AppType::Codex)?; Self::auto_import_prompt_if_exists(&mut config, AppType::Gemini)?; Self::auto_import_prompt_if_exists(&mut config, AppType::OpenCode)?; + Self::auto_import_prompt_if_exists(&mut config, AppType::OpenClaw)?; Ok(config) } @@ -599,6 +632,7 @@ impl MultiAppConfig { || !self.prompts.codex.prompts.is_empty() || !self.prompts.gemini.prompts.is_empty() || !self.prompts.opencode.prompts.is_empty() + || !self.prompts.openclaw.prompts.is_empty() { return Ok(false); } @@ -611,6 +645,7 @@ impl MultiAppConfig { AppType::Codex, AppType::Gemini, AppType::OpenCode, + AppType::OpenClaw, ] { // 复用已有的单应用导入逻辑 if Self::auto_import_prompt_if_exists(self, app)? { @@ -681,6 +716,7 @@ impl MultiAppConfig { AppType::Codex => &mut config.prompts.codex.prompts, AppType::Gemini => &mut config.prompts.gemini.prompts, AppType::OpenCode => &mut config.prompts.opencode.prompts, + AppType::OpenClaw => &mut config.prompts.openclaw.prompts, }; prompts.insert(id, prompt); @@ -709,12 +745,13 @@ impl MultiAppConfig { let mut conflicts = Vec::new(); // 收集所有应用的 MCP - for app in [AppType::Claude, AppType::Codex, AppType::Gemini] { + for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] { let old_servers = match app { AppType::Claude => &self.mcp.claude.servers, AppType::Codex => &self.mcp.codex.servers, AppType::Gemini => &self.mcp.gemini.servers, AppType::OpenCode => &self.mcp.opencode.servers, + AppType::OpenClaw => continue, // OpenClaw MCP is still in development, skip }; for (id, entry) in old_servers { diff --git a/src-tauri/src/database/dao/mcp.rs b/src-tauri/src/database/dao/mcp.rs index d5c60163f..52d73ef87 100644 --- a/src-tauri/src/database/dao/mcp.rs +++ b/src-tauri/src/database/dao/mcp.rs @@ -13,7 +13,7 @@ impl Database { pub fn get_all_mcp_servers(&self) -> Result, AppError> { let conn = lock_conn!(self.conn); let mut stmt = conn.prepare( - "SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode + "SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw FROM mcp_servers ORDER BY name ASC, id ASC" ).map_err(|e| AppError::Database(e.to_string()))?; @@ -31,6 +31,7 @@ impl Database { let enabled_codex: bool = row.get(8)?; let enabled_gemini: bool = row.get(9)?; let enabled_opencode: bool = row.get(10)?; + let enabled_openclaw: bool = row.get(11)?; let server = serde_json::from_str(&server_config_str).unwrap_or_default(); let tags = serde_json::from_str(&tags_str).unwrap_or_default(); @@ -46,6 +47,7 @@ impl Database { codex: enabled_codex, gemini: enabled_gemini, opencode: enabled_opencode, + openclaw: enabled_openclaw, }, description, homepage, @@ -70,8 +72,8 @@ impl Database { conn.execute( "INSERT OR REPLACE INTO mcp_servers ( id, name, server_config, description, homepage, docs, tags, - enabled_claude, enabled_codex, enabled_gemini, enabled_opencode - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", params![ server.id, server.name, @@ -87,6 +89,7 @@ impl Database { server.apps.codex, server.apps.gemini, server.apps.opencode, + server.apps.openclaw, ], ) .map_err(|e| AppError::Database(e.to_string()))?; diff --git a/src-tauri/src/database/dao/skills.rs b/src-tauri/src/database/dao/skills.rs index 2254cb061..f29e89a63 100644 --- a/src-tauri/src/database/dao/skills.rs +++ b/src-tauri/src/database/dao/skills.rs @@ -22,7 +22,7 @@ impl Database { let mut stmt = conn .prepare( "SELECT id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at FROM skills ORDER BY name ASC", ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -43,8 +43,9 @@ impl Database { codex: row.get(9)?, gemini: row.get(10)?, opencode: row.get(11)?, + openclaw: row.get(12)?, }, - installed_at: row.get(12)?, + installed_at: row.get(13)?, }) }) .map_err(|e| AppError::Database(e.to_string()))?; @@ -63,7 +64,7 @@ impl Database { let mut stmt = conn .prepare( "SELECT id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at FROM skills WHERE id = ?1", ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -83,8 +84,9 @@ impl Database { codex: row.get(9)?, gemini: row.get(10)?, opencode: row.get(11)?, + openclaw: row.get(12)?, }, - installed_at: row.get(12)?, + installed_at: row.get(13)?, }) }); @@ -101,8 +103,8 @@ impl Database { conn.execute( "INSERT OR REPLACE INTO skills (id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", params![ skill.id, skill.name, @@ -116,6 +118,7 @@ impl Database { skill.apps.codex, skill.apps.gemini, skill.apps.opencode, + skill.apps.openclaw, skill.installed_at, ], ) @@ -145,8 +148,8 @@ impl Database { let conn = lock_conn!(self.conn); let affected = conn .execute( - "UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4 WHERE id = ?5", - params![apps.claude, apps.codex, apps.gemini, apps.opencode, id], + "UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4, enabled_openclaw = ?5 WHERE id = ?6", + params![apps.claude, apps.codex, apps.gemini, apps.opencode, apps.openclaw, id], ) .map_err(|e| AppError::Database(e.to_string()))?; Ok(affected > 0) diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 41c26e978..352593fd6 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -48,7 +48,7 @@ const DB_BACKUP_RETAIN: usize = 10; /// 当前 Schema 版本号 /// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑 -pub(crate) const SCHEMA_VERSION: i32 = 5; +pub(crate) const SCHEMA_VERSION: i32 = 6; /// 安全地序列化 JSON,避免 unwrap panic pub(crate) fn to_json_string(value: &T) -> Result { diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index bca3c2de3..924939125 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -58,7 +58,8 @@ impl Database { id TEXT PRIMARY KEY, name TEXT NOT NULL, server_config TEXT NOT NULL, description TEXT, homepage TEXT, docs TEXT, tags TEXT NOT NULL DEFAULT '[]', enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0, - enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0 + enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0, + enabled_openclaw BOOLEAN NOT NULL DEFAULT 0 )", [], ) @@ -86,6 +87,7 @@ impl Database { enabled_codex BOOLEAN NOT NULL DEFAULT 0, enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0, + enabled_openclaw BOOLEAN NOT NULL DEFAULT 0, installed_at INTEGER NOT NULL DEFAULT 0 )", [], @@ -360,6 +362,11 @@ impl Database { Self::migrate_v4_to_v5(conn)?; Self::set_user_version(conn, 5)?; } + 5 => { + log::info!("迁移数据库从 v5 到 v6(OpenClaw 支持)"); + Self::migrate_v5_to_v6(conn)?; + Self::set_user_version(conn, 6)?; + } _ => { return Err(AppError::Database(format!( "未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}" @@ -852,6 +859,7 @@ impl Database { enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0, enabled_gemini BOOLEAN NOT NULL DEFAULT 0, + enabled_openclaw BOOLEAN NOT NULL DEFAULT 0, installed_at INTEGER NOT NULL DEFAULT 0 )", [], @@ -914,6 +922,30 @@ impl Database { Ok(()) } + /// v5 -> v6 迁移:添加 OpenClaw 支持 + /// + /// 为 mcp_servers 和 skills 表添加 enabled_openclaw 列。 + fn migrate_v5_to_v6(conn: &Connection) -> Result<(), AppError> { + // 为 mcp_servers 表添加 enabled_openclaw 列 + Self::add_column_if_missing( + conn, + "mcp_servers", + "enabled_openclaw", + "BOOLEAN NOT NULL DEFAULT 0", + )?; + + // 为 skills 表添加 enabled_openclaw 列 + Self::add_column_if_missing( + conn, + "skills", + "enabled_openclaw", + "BOOLEAN NOT NULL DEFAULT 0", + )?; + + log::info!("v5 -> v6 迁移完成:已添加 OpenClaw 支持"); + Ok(()) + } + /// 插入默认模型定价数据 /// 格式: (model_id, display_name, input, output, cache_read, cache_creation) /// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致 From 63fafd6608478a7672a4afaf3ac2eda82621d3f2 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:30:47 +0800 Subject: [PATCH 10/51] feat(openclaw): add OpenClaw configuration module - Create openclaw_config.rs for reading/writing OpenClaw config - Support JSON5 format (~/.openclaw/openclaw.json) - Implement provider CRUD operations for additive mode - Add json5 dependency to Cargo.toml --- src-tauri/Cargo.lock | 61 ++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/openclaw_config.rs | 326 +++++++++++++++++++++++++++++++ 3 files changed, 388 insertions(+) create mode 100644 src-tauri/src/openclaw_config.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0950d9063..bd5fd8c38 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -714,6 +714,7 @@ dependencies = [ "futures", "hyper", "indexmap 2.11.4", + "json5", "log", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -2521,6 +2522,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonptr" version = "0.6.3" @@ -3405,6 +3417,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.8.0" @@ -5888,6 +5943,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 12e288837..9547eb342 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -62,6 +62,7 @@ indexmap = { version = "2", features = ["serde"] } rust_decimal = "1.33" uuid = { version = "1.11", features = ["v4"] } sha2 = "0.10" +json5 = "0.4" [target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] tauri-plugin-single-instance = "2" diff --git a/src-tauri/src/openclaw_config.rs b/src-tauri/src/openclaw_config.rs new file mode 100644 index 000000000..5a30d2565 --- /dev/null +++ b/src-tauri/src/openclaw_config.rs @@ -0,0 +1,326 @@ +//! OpenClaw 配置文件读写模块 +//! +//! 处理 `~/.openclaw/openclaw.json` 配置文件的读写操作(JSON5 格式)。 +//! OpenClaw 使用累加式供应商管理,所有供应商配置共存于同一配置文件中。 +//! +//! ## 配置文件格式 +//! +//! ```json5 +//! { +//! // 模型供应商配置(映射为 CC Switch 的"供应商") +//! models: { +//! mode: "merge", +//! providers: { +//! "custom-provider": { +//! baseUrl: "https://api.example.com/v1", +//! apiKey: "${API_KEY}", +//! api: "openai-completions", +//! models: [{ id: "model-id", name: "Model Name" }] +//! } +//! } +//! }, +//! // 环境变量配置 +//! env: { +//! ANTHROPIC_API_KEY: "sk-...", +//! vars: { ... } +//! }, +//! // Agent 默认模型配置 +//! agents: { +//! defaults: { +//! model: { +//! primary: "provider/model", +//! fallbacks: ["provider2/model2"] +//! } +//! } +//! } +//! } +//! ``` + +use crate::config::write_json_file; +use crate::error::AppError; +use crate::settings::get_openclaw_override_dir; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; +use std::path::PathBuf; + +// ============================================================================ +// Path Functions +// ============================================================================ + +/// 获取 OpenClaw 配置目录 +/// +/// 默认路径: `~/.openclaw/` +/// 可通过 settings.openclaw_config_dir 覆盖 +pub fn get_openclaw_dir() -> PathBuf { + if let Some(override_dir) = get_openclaw_override_dir() { + return override_dir; + } + + // 所有平台统一使用 ~/.openclaw + dirs::home_dir() + .map(|h| h.join(".openclaw")) + .unwrap_or_else(|| PathBuf::from(".openclaw")) +} + +/// 获取 OpenClaw 配置文件路径 +/// +/// 返回 `~/.openclaw/openclaw.json` +pub fn get_openclaw_config_path() -> PathBuf { + get_openclaw_dir().join("openclaw.json") +} + +/// 获取 OpenClaw Skills 目录路径 +/// +/// 返回 `~/.openclaw/skills/` +#[allow(dead_code)] +pub fn get_openclaw_skills_dir() -> PathBuf { + get_openclaw_dir().join("skills") +} + +// ============================================================================ +// Type Definitions +// ============================================================================ + +/// OpenClaw 供应商配置(对应 models.providers 中的条目) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenClawProviderConfig { + /// API 基础 URL + #[serde(skip_serializing_if = "Option::is_none")] + pub base_url: Option, + + /// API Key(支持环境变量引用 ${VAR_NAME}) + #[serde(skip_serializing_if = "Option::is_none")] + pub api_key: Option, + + /// API 类型(如 "openai-completions", "anthropic" 等) + #[serde(skip_serializing_if = "Option::is_none")] + pub api: Option, + + /// 支持的模型列表 + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub models: Vec, + + /// 其他自定义字段(保留原始配置) + #[serde(flatten)] + pub extra: Map, +} + +/// OpenClaw 模型条目 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawModelEntry { + /// 模型 ID + pub id: String, + + /// 模型显示名称 + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// 其他自定义字段 + #[serde(flatten)] + pub extra: Map, +} + +// ============================================================================ +// Core Read/Write Functions +// ============================================================================ + +/// 读取 OpenClaw 配置文件 +/// +/// 支持 JSON5 格式,返回完整的配置 JSON 对象 +pub fn read_openclaw_config() -> Result { + let path = get_openclaw_config_path(); + + if !path.exists() { + // Return empty config structure + return Ok(json!({ + "models": { + "mode": "merge", + "providers": {} + } + })); + } + + let content = std::fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))?; + + // 尝试 JSON5 解析(支持注释和尾随逗号) + json5::from_str(&content).map_err(|e| { + AppError::Config(format!( + "Failed to parse OpenClaw config as JSON5: {}", + e + )) + }) +} + +/// 写入 OpenClaw 配置文件(原子写入) +/// +/// 使用标准 JSON 格式写入(JSON5 是 JSON 的超集) +pub fn write_openclaw_config(config: &Value) -> Result<(), AppError> { + let path = get_openclaw_config_path(); + + // 确保目录存在 + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; + } + + // 复用统一的原子写入逻辑 + write_json_file(&path, config)?; + + log::debug!("OpenClaw config written to {path:?}"); + Ok(()) +} + +// ============================================================================ +// Provider Functions (Untyped - for raw JSON operations) +// ============================================================================ + +/// 获取所有供应商配置(原始 JSON) +/// +/// 从 `models.providers` 读取 +pub fn get_providers() -> Result, AppError> { + let config = read_openclaw_config()?; + Ok(config + .get("models") + .and_then(|m| m.get("providers")) + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_default()) +} + +/// 设置供应商配置(原始 JSON) +/// +/// 写入到 `models.providers` +pub fn set_provider(id: &str, provider_config: Value) -> Result<(), AppError> { + let mut full_config = read_openclaw_config()?; + + // 确保 models 结构存在 + if full_config.get("models").is_none() { + full_config["models"] = json!({ + "mode": "merge", + "providers": {} + }); + } + + // 确保 providers 对象存在 + if full_config["models"].get("providers").is_none() { + full_config["models"]["providers"] = json!({}); + } + + // 设置供应商 + if let Some(providers) = full_config["models"] + .get_mut("providers") + .and_then(|v| v.as_object_mut()) + { + providers.insert(id.to_string(), provider_config); + } + + write_openclaw_config(&full_config) +} + +/// 删除供应商配置 +pub fn remove_provider(id: &str) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + if let Some(providers) = config + .get_mut("models") + .and_then(|m| m.get_mut("providers")) + .and_then(|v| v.as_object_mut()) + { + providers.remove(id); + } + + write_openclaw_config(&config) +} + +// ============================================================================ +// Provider Functions (Typed) +// ============================================================================ + +/// 获取所有供应商配置(类型化) +pub fn get_typed_providers() -> Result, AppError> { + let providers = get_providers()?; + let mut result = IndexMap::new(); + + for (id, value) in providers { + match serde_json::from_value::(value.clone()) { + Ok(config) => { + result.insert(id, config); + } + Err(e) => { + log::warn!("Failed to parse OpenClaw provider '{id}': {e}"); + // Skip invalid providers but continue + } + } + } + + Ok(result) +} + +/// 设置供应商配置(类型化) +pub fn set_typed_provider(id: &str, config: &OpenClawProviderConfig) -> Result<(), AppError> { + let value = serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?; + set_provider(id, value) +} + +// ============================================================================ +// Environment Variables +// ============================================================================ + +/// 获取环境变量配置 +#[allow(dead_code)] +pub fn get_env_vars() -> Result, AppError> { + let config = read_openclaw_config()?; + Ok(config + .get("env") + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_default()) +} + +/// 设置环境变量 +#[allow(dead_code)] +pub fn set_env_var(key: &str, value: &str) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + if config.get("env").is_none() { + config["env"] = json!({}); + } + + if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) { + env.insert(key.to_string(), Value::String(value.to_string())); + } + + write_openclaw_config(&config) +} + +// ============================================================================ +// MCP Functions (Reserved for future use) +// ============================================================================ + +/// 获取所有 MCP 服务器配置 +/// +/// OpenClaw MCP 配置在 `agents.list[].mcp.servers` +/// 由于 OpenClaw MCP 支持仍在开发中(Issue #4834),暂时返回空 +#[allow(dead_code)] +pub fn get_mcp_servers() -> Result, AppError> { + // OpenClaw MCP support is under development + // Return empty for now + Ok(Map::new()) +} + +/// 设置 MCP 服务器配置 +#[allow(dead_code)] +pub fn set_mcp_server(_id: &str, _config: Value) -> Result<(), AppError> { + // OpenClaw MCP support is under development + log::warn!("OpenClaw MCP support is not yet implemented"); + Ok(()) +} + +/// 删除 MCP 服务器配置 +#[allow(dead_code)] +pub fn remove_mcp_server(_id: &str) -> Result<(), AppError> { + // OpenClaw MCP support is under development + log::warn!("OpenClaw MCP support is not yet implemented"); + Ok(()) +} From 433c86b2d3c5add1eab48a3083e7e9651eb9f096 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:31:11 +0800 Subject: [PATCH 11/51] feat(provider): add OpenClaw provider service support - Add import_openclaw_providers_from_live() function - Add remove_openclaw_provider_from_live() function - Update write_live_snapshot() for OpenClaw - Add openclaw fields to VisibleApps and AppSettings - Add get_openclaw_override_dir() function --- src-tauri/src/services/provider/live.rs | 139 ++++++++++++++++++ src-tauri/src/services/provider/mod.rs | 180 +++++++++++++++++------- src-tauri/src/settings.rs | 28 ++++ 3 files changed, 294 insertions(+), 53 deletions(-) diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 52437e6dd..b9997bb33 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -191,6 +191,45 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re } } } + AppType::OpenClaw => { + // OpenClaw uses additive mode - write provider to config + use crate::openclaw_config; + use crate::openclaw_config::OpenClawProviderConfig; + + // Convert settings_config to OpenClawProviderConfig + let openclaw_config_result = + serde_json::from_value::(provider.settings_config.clone()); + + match openclaw_config_result { + Ok(config) => { + openclaw_config::set_typed_provider(&provider.id, &config)?; + log::info!("OpenClaw provider '{}' written to live config", provider.id); + } + Err(e) => { + log::warn!( + "Failed to parse OpenClaw provider config for '{}': {}", + provider.id, + e + ); + // Try to write as raw JSON if it looks valid + if provider.settings_config.get("baseUrl").is_some() + || provider.settings_config.get("api").is_some() + || provider.settings_config.get("models").is_some() + { + openclaw_config::set_provider(&provider.id, provider.settings_config.clone())?; + log::info!( + "OpenClaw provider '{}' written as raw JSON to live config", + provider.id + ); + } else { + log::error!( + "OpenClaw provider '{}' has invalid config structure, skipping write", + provider.id + ); + } + } + } + } } Ok(()) } @@ -340,6 +379,21 @@ pub fn read_live_settings(app_type: AppType) -> Result { let config = read_opencode_config()?; Ok(config) } + AppType::OpenClaw => { + use crate::openclaw_config::{get_openclaw_config_path, read_openclaw_config}; + + let config_path = get_openclaw_config_path(); + if !config_path.exists() { + return Err(AppError::localized( + "openclaw.config.missing", + "OpenClaw 配置文件不存在", + "OpenClaw configuration file not found", + )); + } + + let config = read_openclaw_config()?; + Ok(config) + } } } @@ -433,6 +487,23 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result { + // OpenClaw uses additive mode - import from live is not the same pattern + use crate::openclaw_config::{get_openclaw_config_path, read_openclaw_config}; + + let config_path = get_openclaw_config_path(); + if !config_path.exists() { + return Err(AppError::localized( + "openclaw.live.missing", + "OpenClaw 配置文件不存在", + "OpenClaw configuration file is missing", + )); + } + + // For OpenClaw, we return the full config - but note that OpenClaw + // uses additive mode, so importing defaults works differently + read_openclaw_config()? + } }; let mut provider = Provider::with_id( @@ -609,3 +680,71 @@ pub fn import_opencode_providers_from_live(state: &AppState) -> Result Result { + use crate::openclaw_config; + + let providers = openclaw_config::get_typed_providers()?; + if providers.is_empty() { + return Ok(0); + } + + let mut imported = 0; + let existing = state.db.get_all_providers("openclaw")?; + + for (id, config) in providers { + // Skip if already exists in database + if existing.contains_key(&id) { + log::debug!("OpenClaw provider '{id}' already exists in database, skipping"); + continue; + } + + // Convert to Value for settings_config + let settings_config = match serde_json::to_value(&config) { + Ok(v) => v, + Err(e) => { + log::warn!("Failed to serialize OpenClaw provider '{id}': {e}"); + continue; + } + }; + + // Determine display name: use first model name if available, otherwise use id + let display_name = config + .models + .first() + .and_then(|m| m.name.clone()) + .unwrap_or_else(|| id.clone()); + + // Create provider + let provider = Provider::with_id(id.clone(), display_name, settings_config, None); + + // Save to database + if let Err(e) = state.db.save_provider("openclaw", &provider) { + log::warn!("Failed to import OpenClaw provider '{id}': {e}"); + continue; + } + + imported += 1; + log::info!("Imported OpenClaw provider '{id}' from live config"); + } + + Ok(imported) +} + +/// Remove an OpenClaw provider from live config +/// +/// This removes a specific provider from ~/.openclaw/openclaw.json +/// without affecting other providers in the file. +pub fn remove_openclaw_provider_from_live(provider_id: &str) -> Result<(), AppError> { + use crate::openclaw_config; + + openclaw_config::remove_provider(provider_id)?; + log::info!("OpenClaw provider '{provider_id}' removed from live config"); + + Ok(()) +} diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index c5f6abef7..aa163ce3b 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -21,8 +21,8 @@ use crate::store::AppState; // Re-export sub-module functions for external access pub use live::{ - import_default_config, import_opencode_providers_from_live, read_live_settings, - sync_current_to_live, + import_default_config, import_openclaw_providers_from_live, import_opencode_providers_from_live, + read_live_settings, sync_current_to_live, }; // Internal re-exports (pub(crate)) @@ -30,7 +30,9 @@ pub(crate) use live::sanitize_claude_settings_for_live; pub(crate) use live::write_live_snapshot; // Internal re-exports -use live::{remove_opencode_provider_from_live, write_gemini_live}; +use live::{ + remove_openclaw_provider_from_live, remove_opencode_provider_from_live, write_gemini_live, +}; use usage::validate_usage_script; /// Provider business logic service @@ -142,10 +144,10 @@ impl ProviderService { /// 优先从本地 settings 读取,验证后 fallback 到数据库的 is_current 字段。 /// 这确保了云同步场景下多设备可以独立选择供应商,且返回的 ID 一定有效。 /// - /// 对于 OpenCode(累加模式),不存在"当前供应商"概念,直接返回空字符串。 + /// 对于累加模式应用(OpenCode, OpenClaw),不存在"当前供应商"概念,直接返回空字符串。 pub fn current(state: &AppState, app_type: AppType) -> Result { - // OpenCode uses additive mode - no "current" provider concept - if matches!(app_type, AppType::OpenCode) { + // Additive mode apps have no "current" provider concept + if app_type.is_additive_mode() { return Ok(String::new()); } crate::settings::get_effective_current_provider(&state.db, &app_type) @@ -162,10 +164,12 @@ impl ProviderService { // Save to database state.db.save_provider(app_type.as_str(), &provider)?; - // OpenCode uses additive mode - always write to live config - if matches!(app_type, AppType::OpenCode) { + // Additive mode apps (OpenCode, OpenClaw) - always write to live config + if app_type.is_additive_mode() { // OMO providers use exclusive mode and write to dedicated config file. - if provider.category.as_deref() == Some("omo") { + if matches!(app_type, AppType::OpenCode) + && provider.category.as_deref() == Some("omo") + { // Do not auto-enable newly added OMO providers. // Users must explicitly switch/apply an OMO provider to activate it. return Ok(true); @@ -201,9 +205,11 @@ impl ProviderService { // Save to database state.db.save_provider(app_type.as_str(), &provider)?; - // OpenCode uses additive mode - always update in live config - if matches!(app_type, AppType::OpenCode) { - if provider.category.as_deref() == Some("omo") { + // Additive mode apps (OpenCode, OpenClaw) - always update in live config + if app_type.is_additive_mode() { + if matches!(app_type, AppType::OpenCode) + && provider.category.as_deref() == Some("omo") + { let is_omo_current = state .db .is_omo_provider_current(app_type.as_str(), &provider.id)?; @@ -253,43 +259,48 @@ impl ProviderService { /// Delete a provider /// /// 同时检查本地 settings 和数据库的当前供应商,防止删除任一端正在使用的供应商。 - /// 对于 OpenCode(累加模式),可以随时删除任意供应商,同时从 live 配置中移除。 + /// 对于累加模式应用(OpenCode, OpenClaw),可以随时删除任意供应商,同时从 live 配置中移除。 pub fn delete(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> { - // OpenCode uses additive mode - no current provider concept - if matches!(app_type, AppType::OpenCode) { - let is_omo = state - .db - .get_provider_by_id(id, app_type.as_str())? - .and_then(|p| p.category) - .as_deref() - == Some("omo"); - - if is_omo { - let was_current = state.db.is_omo_provider_current(app_type.as_str(), id)?; - let omo_count = state + // Additive mode apps - no current provider concept + if app_type.is_additive_mode() { + if matches!(app_type, AppType::OpenCode) { + let is_omo = state .db - .get_all_providers(app_type.as_str())? - .values() - .filter(|p| p.category.as_deref() == Some("omo")) - .count(); + .get_provider_by_id(id, app_type.as_str())? + .and_then(|p| p.category) + .as_deref() + == Some("omo"); - if omo_count <= 1 && was_current { - return Err(AppError::Message( - "无法删除当前启用的最后一个 OMO 配置,请先停用".to_string(), - )); - } + if is_omo { + let was_current = state.db.is_omo_provider_current(app_type.as_str(), id)?; + let omo_count = state + .db + .get_all_providers(app_type.as_str())? + .values() + .filter(|p| p.category.as_deref() == Some("omo")) + .count(); - state.db.delete_provider(app_type.as_str(), id)?; - if was_current { - crate::services::OmoService::delete_config_file()?; + if omo_count <= 1 && was_current { + return Err(AppError::Message( + "无法删除当前启用的最后一个 OMO 配置,请先停用".to_string(), + )); + } + + state.db.delete_provider(app_type.as_str(), id)?; + if was_current { + crate::services::OmoService::delete_config_file()?; + } + return Ok(()); } - return Ok(()); } - // Remove from database state.db.delete_provider(app_type.as_str(), id)?; // Also remove from live config - remove_opencode_provider_from_live(id)?; + match app_type { + AppType::OpenCode => remove_opencode_provider_from_live(id)?, + AppType::OpenClaw => remove_openclaw_provider_from_live(id)?, + _ => {} // Should not reach here + } return Ok(()); } @@ -306,7 +317,7 @@ impl ProviderService { state.db.delete_provider(app_type.as_str(), id) } - /// Remove provider from live config only (for additive mode apps like OpenCode) + /// Remove provider from live config only (for additive mode apps like OpenCode, OpenClaw) /// /// Does NOT delete from database - provider remains in the list. /// This is used when user wants to "remove" a provider from active config @@ -338,7 +349,9 @@ impl ProviderService { remove_opencode_provider_from_live(id)?; } } - // Future: add other additive mode apps here + AppType::OpenClaw => { + remove_openclaw_provider_from_live(id)?; + } _ => { return Err(AppError::Message(format!( "App {} does not support remove from live config", @@ -454,22 +467,25 @@ impl ProviderService { // Use effective current provider (validated existence) to ensure backfill targets valid provider let current_id = crate::settings::get_effective_current_provider(&state.db, &app_type)?; - match (current_id, matches!(app_type, AppType::OpenCode)) { - (Some(current_id), false) if current_id != id => { - // Only backfill when switching to a different provider. - if let Ok(live_config) = read_live_settings(app_type.clone()) { - if let Some(mut current_provider) = providers.get(¤t_id).cloned() { - current_provider.settings_config = live_config; - // Ignore backfill failure, don't affect switch flow. - let _ = state.db.save_provider(app_type.as_str(), ¤t_provider); + if let Some(current_id) = current_id { + if current_id != id { + // Additive mode apps - all providers coexist in the same file, + // no backfill needed (backfill is for exclusive mode apps like Claude/Codex/Gemini) + if !app_type.is_additive_mode() { + // Only backfill when switching to a different provider + if let Ok(live_config) = read_live_settings(app_type.clone()) { + if let Some(mut current_provider) = providers.get(¤t_id).cloned() { + current_provider.settings_config = live_config; + // Ignore backfill failure, don't affect switch flow + let _ = state.db.save_provider(app_type.as_str(), ¤t_provider); + } } } } - _ => {} } - // OpenCode uses additive mode - skip setting is_current (no such concept) - if !matches!(app_type, AppType::OpenCode) { + // Additive mode apps skip setting is_current (no such concept) + if !app_type.is_additive_mode() { // Update local settings (device-level, takes priority) crate::settings::set_current_provider(&app_type, Some(id))?; @@ -515,6 +531,7 @@ impl ProviderService { AppType::Codex => Self::extract_codex_common_config(&provider.settings_config), AppType::Gemini => Self::extract_gemini_common_config(&provider.settings_config), AppType::OpenCode => Self::extract_opencode_common_config(&provider.settings_config), + AppType::OpenClaw => Self::extract_openclaw_common_config(&provider.settings_config), } } @@ -528,6 +545,7 @@ impl ProviderService { AppType::Codex => Self::extract_codex_common_config(settings_config), AppType::Gemini => Self::extract_gemini_common_config(settings_config), AppType::OpenCode => Self::extract_opencode_common_config(settings_config), + AppType::OpenClaw => Self::extract_openclaw_common_config(settings_config), } } @@ -684,6 +702,27 @@ impl ProviderService { .map_err(|e| AppError::Message(format!("Serialization failed: {e}"))) } + /// Extract common config for OpenClaw (JSON format) + fn extract_openclaw_common_config(settings: &Value) -> Result { + // OpenClaw uses a different config structure with baseUrl, apiKey, api, models + // For common config, we exclude provider-specific fields like apiKey + let mut config = settings.clone(); + + // Remove provider-specific fields + if let Some(obj) = config.as_object_mut() { + obj.remove("apiKey"); + obj.remove("baseUrl"); + // Keep api and models as they might be common + } + + if config.is_null() || (config.is_object() && config.as_object().unwrap().is_empty()) { + return Ok("{}".to_string()); + } + + serde_json::to_string_pretty(&config) + .map_err(|e| AppError::Message(format!("Serialization failed: {e}"))) + } + /// Import default configuration from live files (re-export) /// /// Returns `Ok(true)` if imported, `Ok(false)` if skipped. @@ -861,6 +900,17 @@ impl ProviderService { )); } } + AppType::OpenClaw => { + // OpenClaw uses config structure: { baseUrl, apiKey, api, models } + // Basic validation - must be an object + if !provider.settings_config.is_object() { + return Err(AppError::localized( + "provider.openclaw.settings.not_object", + "OpenClaw 配置必须是 JSON 对象", + "OpenClaw configuration must be a JSON object", + )); + } + } } // Validate and clean UsageScript configuration (common for all app types) @@ -1032,6 +1082,30 @@ impl ProviderService { Ok((api_key, base_url)) } + AppType::OpenClaw => { + // OpenClaw uses apiKey and baseUrl directly on the object + let api_key = provider + .settings_config + .get("apiKey") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + AppError::localized( + "provider.openclaw.api_key.missing", + "缺少 API Key", + "API key is missing", + ) + })? + .to_string(); + + let base_url = provider + .settings_config + .get("baseUrl") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + Ok((api_key, base_url)) + } } } } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index e1ae3b213..1735cac9a 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -34,6 +34,8 @@ pub struct VisibleApps { pub gemini: bool, #[serde(default = "default_true")] pub opencode: bool, + #[serde(default = "default_true")] + pub openclaw: bool, } impl Default for VisibleApps { @@ -43,6 +45,7 @@ impl Default for VisibleApps { codex: true, gemini: true, opencode: true, + openclaw: true, } } } @@ -55,6 +58,7 @@ impl VisibleApps { AppType::Codex => self.codex, AppType::Gemini => self.gemini, AppType::OpenCode => self.opencode, + AppType::OpenClaw => self.openclaw, } } } @@ -194,6 +198,8 @@ pub struct AppSettings { pub gemini_config_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub opencode_config_dir: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub openclaw_config_dir: Option, // ===== 当前供应商 ID(设备级)===== /// 当前 Claude 供应商 ID(本地存储,优先于数据库 is_current) @@ -208,6 +214,9 @@ pub struct AppSettings { /// 当前 OpenCode 供应商 ID(本地存储,对 OpenCode 可能无意义,但保持结构一致) #[serde(default, skip_serializing_if = "Option::is_none")] pub current_provider_opencode: Option, + /// 当前 OpenClaw 供应商 ID(本地存储,对 OpenClaw 可能无意义,但保持结构一致) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_provider_openclaw: Option, // ===== Skill 同步设置 ===== /// Skill 同步方式:auto(默认,优先 symlink)、symlink、copy @@ -254,10 +263,12 @@ impl Default for AppSettings { codex_config_dir: None, gemini_config_dir: None, opencode_config_dir: None, + openclaw_config_dir: None, current_provider_claude: None, current_provider_codex: None, current_provider_gemini: None, current_provider_opencode: None, + current_provider_openclaw: None, skill_sync_method: SyncMethod::default(), webdav_sync: None, webdav_backup: None, @@ -305,6 +316,13 @@ impl AppSettings { .filter(|s| !s.is_empty()) .map(|s| s.to_string()); + self.openclaw_config_dir = self + .openclaw_config_dir + .as_ref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + self.language = self .language .as_ref() @@ -497,6 +515,14 @@ pub fn get_opencode_override_dir() -> Option { .map(|p| resolve_override_path(p)) } +pub fn get_openclaw_override_dir() -> Option { + let settings = settings_store().read().ok()?; + settings + .openclaw_config_dir + .as_ref() + .map(|p| resolve_override_path(p)) +} + // ===== 当前供应商管理函数 ===== /// 获取指定应用类型的当前供应商 ID(从本地 settings 读取) @@ -510,6 +536,7 @@ pub fn get_current_provider(app_type: &AppType) -> Option { AppType::Codex => settings.current_provider_codex.clone(), AppType::Gemini => settings.current_provider_gemini.clone(), AppType::OpenCode => settings.current_provider_opencode.clone(), + AppType::OpenClaw => settings.current_provider_openclaw.clone(), } } @@ -525,6 +552,7 @@ pub fn set_current_provider(app_type: &AppType, id: Option<&str>) -> Result<(), AppType::Codex => settings.current_provider_codex = id.map(|s| s.to_string()), AppType::Gemini => settings.current_provider_gemini = id.map(|s| s.to_string()), AppType::OpenCode => settings.current_provider_opencode = id.map(|s| s.to_string()), + AppType::OpenClaw => settings.current_provider_openclaw = id.map(|s| s.to_string()), } update_settings(settings) From e85878e95a6961049571281d24cb10ae1aceca91 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:31:21 +0800 Subject: [PATCH 12/51] feat(services): add OpenClaw branches to backend services - Add OpenClaw branches to proxy service (not supported) - Add OpenClaw to MCP service (skip sync, MCP still in development) - Add OpenClaw skills directory path - Update ProxyTakeoverStatus with openclaw field - Add OpenClaw to stream check (not supported) --- src-tauri/src/proxy/providers/mod.rs | 8 ++++++ src-tauri/src/proxy/types.rs | 2 ++ src-tauri/src/services/config.rs | 4 +++ src-tauri/src/services/mcp.rs | 9 ++++++ src-tauri/src/services/proxy.rs | 38 ++++++++++++++++++++++++++ src-tauri/src/services/skill.rs | 8 ++++++ src-tauri/src/services/stream_check.rs | 28 +++++++++++++++++++ 7 files changed, 97 insertions(+) diff --git a/src-tauri/src/proxy/providers/mod.rs b/src-tauri/src/proxy/providers/mod.rs index 61be1087e..a4bb3de76 100644 --- a/src-tauri/src/proxy/providers/mod.rs +++ b/src-tauri/src/proxy/providers/mod.rs @@ -136,6 +136,10 @@ impl ProviderType { // OpenCode doesn't support proxy, but return a default type for completeness ProviderType::Codex // Fallback to Codex-like type } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy, but return a default type for completeness + ProviderType::Codex // Fallback to Codex-like type + } } } @@ -184,6 +188,10 @@ pub fn get_adapter(app_type: &AppType) -> Box { // OpenCode doesn't support proxy, fallback to Codex adapter Box::new(CodexAdapter::new()) } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy, fallback to Codex adapter + Box::new(CodexAdapter::new()) + } } } diff --git a/src-tauri/src/proxy/types.rs b/src-tauri/src/proxy/types.rs index cf8dd7c8c..5118c2bbe 100644 --- a/src-tauri/src/proxy/types.rs +++ b/src-tauri/src/proxy/types.rs @@ -113,6 +113,8 @@ pub struct ProxyTakeoverStatus { pub claude: bool, pub codex: bool, pub gemini: bool, + pub opencode: bool, + pub openclaw: bool, } /// API 格式类型(预留,当前不需要格式转换) diff --git a/src-tauri/src/services/config.rs b/src-tauri/src/services/config.rs index a226e93ae..d354cccb1 100644 --- a/src-tauri/src/services/config.rs +++ b/src-tauri/src/services/config.rs @@ -126,6 +126,10 @@ impl ConfigService { // OpenCode uses additive mode, no live sync needed // OpenCode providers are managed directly in the config file } + AppType::OpenClaw => { + // OpenClaw uses additive mode, no live sync needed + // OpenClaw providers are managed directly in the config file + } } Ok(()) diff --git a/src-tauri/src/services/mcp.rs b/src-tauri/src/services/mcp.rs index 8d1211bbc..1080480b3 100644 --- a/src-tauri/src/services/mcp.rs +++ b/src-tauri/src/services/mcp.rs @@ -123,6 +123,11 @@ impl McpService { &server.server, )?; } + AppType::OpenClaw => { + // OpenClaw MCP support is still in development (Issue #4834) + // Skip for now + log::debug!("OpenClaw MCP support is still in development, skipping sync"); + } } Ok(()) } @@ -148,6 +153,10 @@ impl McpService { AppType::OpenCode => { mcp::remove_server_from_opencode(id)?; } + AppType::OpenClaw => { + // OpenClaw MCP support is still in development + log::debug!("OpenClaw MCP support is still in development, skipping remove"); + } } Ok(()) } diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 0f429b030..5325f4af1 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -210,11 +210,16 @@ impl ProxyService { .await .map(|c| c.enabled) .unwrap_or(false); + // OpenCode and OpenClaw don't support proxy features, always return false + let opencode_enabled = false; + let openclaw_enabled = false; Ok(ProxyTakeoverStatus { claude: claude_enabled, codex: codex_enabled, gemini: gemini_enabled, + opencode: opencode_enabled, + openclaw: openclaw_enabled, }) } @@ -372,6 +377,10 @@ impl ProxyService { // OpenCode doesn't support proxy features return Err("OpenCode 不支持代理功能".to_string()); } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy features + return Err("OpenClaw 不支持代理功能".to_string()); + } }; self.sync_live_config_to_provider(app_type, &live_config) @@ -588,6 +597,9 @@ impl ProxyService { AppType::OpenCode => { // OpenCode doesn't support proxy features, skip silently } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy features, skip silently + } } Ok(()) @@ -770,6 +782,10 @@ impl ProxyService { // OpenCode doesn't support proxy features return Err("OpenCode 不支持代理功能".to_string()); } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy features + return Err("OpenClaw 不支持代理功能".to_string()); + } }; let json_str = serde_json::to_string(&config) @@ -982,6 +998,10 @@ impl ProxyService { // OpenCode doesn't support proxy features return Err("OpenCode 不支持代理功能".to_string()); } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy features + return Err("OpenClaw 不支持代理功能".to_string()); + } } Ok(()) @@ -1068,6 +1088,9 @@ impl ProxyService { AppType::OpenCode => { // OpenCode doesn't support proxy features, skip silently } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy features, skip silently + } } Ok(()) @@ -1103,6 +1126,9 @@ impl ProxyService { AppType::OpenCode => { // OpenCode doesn't support proxy features, skip silently } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy features, skip silently + } } Ok(()) @@ -1186,6 +1212,10 @@ impl ProxyService { // OpenCode doesn't support proxy features Err("OpenCode 不支持代理功能".to_string()) } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy features + Err("OpenClaw 不支持代理功能".to_string()) + } } } @@ -1207,6 +1237,10 @@ impl ProxyService { // OpenCode doesn't support proxy takeover false } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy takeover + false + } } } @@ -1250,6 +1284,10 @@ impl ProxyService { // OpenCode doesn't support proxy features Ok(()) } + AppType::OpenClaw => { + // OpenClaw doesn't support proxy features + Ok(()) + } } } diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index 4a2e671ef..f02136075 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -230,6 +230,11 @@ impl SkillService { return Ok(custom.join("skills")); } } + AppType::OpenClaw => { + if let Some(custom) = crate::settings::get_openclaw_override_dir() { + return Ok(custom.join("skills")); + } + } } // 默认路径:回退到用户主目录下的标准位置 @@ -244,6 +249,7 @@ impl SkillService { AppType::Codex => home.join(".codex").join("skills"), AppType::Gemini => home.join(".gemini").join("skills"), AppType::OpenCode => home.join(".config").join("opencode").join("skills"), + AppType::OpenClaw => home.join(".openclaw").join("skills"), }) } @@ -562,6 +568,7 @@ impl SkillService { AppType::Codex => "codex", AppType::Gemini => "gemini", AppType::OpenCode => "opencode", + AppType::OpenClaw => "openclaw", }; unmanaged @@ -611,6 +618,7 @@ impl SkillService { AppType::Codex => "codex", AppType::Gemini => "gemini", AppType::OpenCode => "opencode", + AppType::OpenClaw => "openclaw", }; found_in.push(app_str.to_string()); } diff --git a/src-tauri/src/services/stream_check.rs b/src-tauri/src/services/stream_check.rs index c7afaa55e..b5bf88887 100644 --- a/src-tauri/src/services/stream_check.rs +++ b/src-tauri/src/services/stream_check.rs @@ -240,6 +240,14 @@ impl StreamCheckService { "OpenCode does not support health check yet", )); } + AppType::OpenClaw => { + // OpenClaw doesn't support stream check yet + return Err(AppError::localized( + "openclaw_no_stream_check", + "OpenClaw 暂不支持健康检查", + "OpenClaw does not support health check yet", + )); + } }; let response_time = start.elapsed().as_millis() as u64; @@ -567,6 +575,11 @@ impl StreamCheckService { // Try to extract first model from the models object Self::extract_opencode_model(provider).unwrap_or_else(|| "gpt-4o".to_string()) } + AppType::OpenClaw => { + // OpenClaw uses models array in settings_config + // Try to extract first model from the models array + Self::extract_openclaw_model(provider).unwrap_or_else(|| "gpt-4o".to_string()) + } } } @@ -580,6 +593,21 @@ impl StreamCheckService { models.keys().next().map(|s| s.to_string()) } + fn extract_openclaw_model(provider: &Provider) -> Option { + // OpenClaw uses models array: [{ "id": "model-id", "name": "Model Name" }] + let models = provider + .settings_config + .get("models") + .and_then(|m| m.as_array())?; + + // Return the first model ID from the models array + models + .first() + .and_then(|m| m.get("id")) + .and_then(|id| id.as_str()) + .map(|s| s.to_string()) + } + fn extract_env_model(provider: &Provider, key: &str) -> Option { provider .settings_config From 6952360f70ca23eceb5b5845b746c1a81ea1710b Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:31:32 +0800 Subject: [PATCH 13/51] feat(backend): add OpenClaw support to commands and deeplinks - Register openclaw_config module in lib.rs - Add OpenClaw commands and provider import on startup - Add OpenClaw branches to config and provider commands - Add build_openclaw_settings() for deeplink imports - Update MCP handlers with openclaw field in McpApps - Add OpenClaw prompt file path --- src-tauri/src/commands/config.rs | 11 +++++++++++ src-tauri/src/commands/provider.rs | 24 ++++++++++++++++++++++++ src-tauri/src/deeplink/mcp.rs | 2 ++ src-tauri/src/deeplink/provider.rs | 30 ++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 15 +++++++++++++++ src-tauri/src/mcp/claude.rs | 1 + src-tauri/src/mcp/codex.rs | 1 + src-tauri/src/mcp/gemini.rs | 1 + src-tauri/src/mcp/opencode.rs | 1 + src-tauri/src/prompt_files.rs | 3 +++ 10 files changed, 89 insertions(+) diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index c70939e04..87430b46f 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -59,6 +59,15 @@ pub async fn get_config_status(app: String) -> Result { Ok(ConfigStatus { exists, path }) } + AppType::OpenClaw => { + let config_path = crate::openclaw_config::get_openclaw_config_path(); + let exists = config_path.exists(); + let path = crate::openclaw_config::get_openclaw_dir() + .to_string_lossy() + .to_string(); + + Ok(ConfigStatus { exists, path }) + } } } @@ -74,6 +83,7 @@ pub async fn get_config_dir(app: String) -> Result { AppType::Codex => codex_config::get_codex_config_dir(), AppType::Gemini => crate::gemini_config::get_gemini_dir(), AppType::OpenCode => crate::opencode_config::get_opencode_dir(), + AppType::OpenClaw => crate::openclaw_config::get_openclaw_dir(), }; Ok(dir.to_string_lossy().to_string()) @@ -86,6 +96,7 @@ pub async fn open_config_folder(handle: AppHandle, app: String) -> Result codex_config::get_codex_config_dir(), AppType::Gemini => crate::gemini_config::get_gemini_dir(), AppType::OpenCode => crate::opencode_config::get_opencode_dir(), + AppType::OpenClaw => crate::openclaw_config::get_openclaw_dir(), }; if !config_dir.exists() { diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 47de928f5..70ce9048e 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -318,3 +318,27 @@ pub fn get_opencode_live_provider_ids() -> Result, String> { .map(|providers| providers.keys().cloned().collect()) .map_err(|e| e.to_string()) } + +// ============================================================================ +// OpenClaw 专属命令 +// ============================================================================ + +/// 从 OpenClaw live 配置导入供应商到数据库 +/// +/// 这是 OpenClaw 特有的功能,因为 OpenClaw 使用累加模式, +/// 用户可能已经在 openclaw.json 中配置了供应商。 +#[tauri::command] +pub fn import_openclaw_providers_from_live(state: State<'_, AppState>) -> Result { + crate::services::provider::import_openclaw_providers_from_live(state.inner()) + .map_err(|e| e.to_string()) +} + +/// 获取 OpenClaw live 配置中的供应商 ID 列表 +/// +/// 用于前端判断供应商是否已添加到 openclaw.json +#[tauri::command] +pub fn get_openclaw_live_provider_ids() -> Result, String> { + crate::openclaw_config::get_providers() + .map(|providers| providers.keys().cloned().collect()) + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/deeplink/mcp.rs b/src-tauri/src/deeplink/mcp.rs index 04d3f11b9..67f3d92cb 100644 --- a/src-tauri/src/deeplink/mcp.rs +++ b/src-tauri/src/deeplink/mcp.rs @@ -167,6 +167,7 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result { codex: false, gemini: false, opencode: false, + openclaw: false, }; for app in apps_str.split(',') { @@ -175,6 +176,7 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result { "codex" => apps.codex = true, "gemini" => apps.gemini = true, "opencode" => apps.opencode = true, + "openclaw" => apps.openclaw = true, other => { return Err(AppError::InvalidInput(format!( "Invalid app in 'apps': {other}" diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index ec75d55e5..0528a2e49 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -146,6 +146,7 @@ pub(crate) fn build_provider_from_request( AppType::Codex => build_codex_settings(request), AppType::Gemini => build_gemini_settings(request), AppType::OpenCode => build_opencode_settings(request), + AppType::OpenClaw => build_openclaw_settings(request), }; // Build usage script configuration if provided @@ -391,6 +392,35 @@ fn build_opencode_settings(request: &DeepLinkImportRequest) -> serde_json::Value }) } +fn build_openclaw_settings(request: &DeepLinkImportRequest) -> serde_json::Value { + let endpoint = get_primary_endpoint(request); + + // Build OpenClaw provider config + // Format: { baseUrl, apiKey, api, models } + let mut config = serde_json::Map::new(); + + if !endpoint.is_empty() { + config.insert("baseUrl".to_string(), json!(endpoint)); + } + + if let Some(api_key) = &request.api_key { + config.insert("apiKey".to_string(), json!(api_key)); + } + + // Default to OpenAI-compatible API + config.insert("api".to_string(), json!("openai-completions")); + + // Build models array + if let Some(model) = &request.model { + config.insert( + "models".to_string(), + json!([{ "id": model, "name": model }]), + ); + } + + json!(config) +} + // ============================================================================= // Config Merge Logic // ============================================================================= diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ef47fdaee..dc36d60d9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -14,6 +14,7 @@ mod gemini_mcp; mod init_status; mod mcp; mod opencode_config; +mod openclaw_config; mod panic_hook; mod prompt; mod prompt_files; @@ -525,6 +526,17 @@ pub fn run() { } } + // 2.3 OpenClaw 供应商导入(累加式模式,需特殊处理) + // OpenClaw 与 OpenCode 类似:配置文件中可同时存在多个供应商 + // 需要遍历 models.providers 字段下的每个供应商并导入 + match crate::services::provider::import_openclaw_providers_from_live(&app_state) { + Ok(count) if count > 0 => { + log::info!("✓ Imported {count} OpenClaw provider(s) from live config"); + } + Ok(_) => log::debug!("○ No OpenClaw providers found to import"), + Err(e) => log::debug!("○ Failed to import OpenClaw providers: {e}"), + } + // 3. 导入 MCP 服务器配置(表空时触发) if app_state.db.is_mcp_table_empty().unwrap_or(false) { log::info!("MCP table empty, importing from live configurations..."); @@ -992,6 +1004,9 @@ pub fn run() { // OpenCode specific commands::import_opencode_providers_from_live, commands::get_opencode_live_provider_ids, + // OpenClaw specific + commands::import_openclaw_providers_from_live, + commands::get_openclaw_live_provider_ids, // Global upstream proxy commands::get_global_proxy_url, commands::set_global_proxy_url, diff --git a/src-tauri/src/mcp/claude.rs b/src-tauri/src/mcp/claude.rs index 25d2f426a..3ba1a310b 100644 --- a/src-tauri/src/mcp/claude.rs +++ b/src-tauri/src/mcp/claude.rs @@ -92,6 +92,7 @@ pub fn import_from_claude(config: &mut MultiAppConfig) -> Result Result codex: true, gemini: false, opencode: false, + openclaw: false, }, description: None, homepage: None, diff --git a/src-tauri/src/mcp/gemini.rs b/src-tauri/src/mcp/gemini.rs index c8a7809f3..b2725da27 100644 --- a/src-tauri/src/mcp/gemini.rs +++ b/src-tauri/src/mcp/gemini.rs @@ -88,6 +88,7 @@ pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result Result Result { @@ -14,6 +15,7 @@ pub fn prompt_file_path(app: &AppType) -> Result { AppType::Codex => get_base_dir_with_fallback(get_codex_auth_path(), ".codex")?, AppType::Gemini => get_gemini_dir(), AppType::OpenCode => get_opencode_dir(), + AppType::OpenClaw => get_openclaw_dir(), }; let filename = match app { @@ -21,6 +23,7 @@ pub fn prompt_file_path(app: &AppType) -> Result { AppType::Codex => "AGENTS.md", AppType::Gemini => "GEMINI.md", AppType::OpenCode => "AGENTS.md", + AppType::OpenClaw => "AGENTS.md", // OpenClaw uses AGENTS.md for agent instructions }; Ok(base_dir.join(filename)) From 31ec5d73a631f67065cfb6b215229b6a16266690 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:31:40 +0800 Subject: [PATCH 14/51] feat(types): add OpenClaw TypeScript type definitions - Add OpenClawProviderConfig and OpenClawModel interfaces - Add openclaw to McpApps, VisibleApps, ProxyTakeoverStatus - Add "openclaw" to AppId union type - Add openclaw to SkillApps interface --- src/lib/api/skills.ts | 3 ++- src/lib/api/types.ts | 2 +- src/types.ts | 21 +++++++++++++++++++++ src/types/proxy.ts | 1 + 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/lib/api/skills.ts b/src/lib/api/skills.ts index e7a6e34f2..673cf7a34 100644 --- a/src/lib/api/skills.ts +++ b/src/lib/api/skills.ts @@ -2,7 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import type { AppId } from "@/lib/api/types"; -// ========== 类型定义 ========== +export type AppType = "claude" | "codex" | "gemini" | "opencode" | "openclaw"; /** Skill 应用启用状态 */ export interface SkillApps { @@ -10,6 +10,7 @@ export interface SkillApps { codex: boolean; gemini: boolean; opencode: boolean; + openclaw: boolean; } /** 已安装的 Skill(v3.10.0+ 统一结构) */ diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index 2e8e2b2ac..1fff721fc 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -1,2 +1,2 @@ // 前端统一使用 AppId 作为应用标识(与后端命令参数 `app` 一致) -export type AppId = "claude" | "codex" | "gemini" | "opencode"; +export type AppId = "claude" | "codex" | "gemini" | "opencode" | "openclaw"; diff --git a/src/types.ts b/src/types.ts index 5d219c4f5..0aaa90ac1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -160,6 +160,7 @@ export interface VisibleApps { codex: boolean; gemini: boolean; opencode: boolean; + openclaw: boolean; } // WebDAV v2 同步状态 @@ -287,6 +288,7 @@ export interface McpApps { codex: boolean; gemini: boolean; opencode: boolean; + openclaw: boolean; } // MCP 服务器条目(v3.7.0 统一结构) @@ -424,3 +426,22 @@ export interface OpenCodeMcpServerSpec { // 通用字段 enabled?: boolean; } + +// ============================================================================ +// OpenClaw 专属配置(v3.11.0+) +// ============================================================================ + +// OpenClaw 模型配置 +export interface OpenClawModel { + id: string; + name: string; +} + +// OpenClaw 供应商配置(settings_config 结构) +// 对应 OpenClaw 的 models.providers. 配置 +export interface OpenClawProviderConfig { + baseUrl?: string; // API 端点 + apiKey?: string; // API 密钥 + api?: string; // API 协议类型(如 "openai-completions"、"anthropic") + models?: OpenClawModel[]; // 可用模型列表 +} diff --git a/src/types/proxy.ts b/src/types/proxy.ts index 705c2288c..7aa6f42de 100644 --- a/src/types/proxy.ts +++ b/src/types/proxy.ts @@ -46,6 +46,7 @@ export interface ProxyTakeoverStatus { codex: boolean; gemini: boolean; opencode: boolean; + openclaw: boolean; } export interface ProviderHealth { From 545be56698682946aced1d5eaf3f2465c2a1feda Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:31:53 +0800 Subject: [PATCH 15/51] feat(presets): add OpenClaw provider presets - Create openclawProviderPresets.ts with provider templates - Include Chinese officials (DeepSeek, Zhipu, Qwen, Kimi, MiniMax) - Include aggregators (AiHubMix, DMXAPI, OpenRouter, ModelScope) - Include third party partners (PackyCode, Cubence, AIGoCode) - Include OpenAI Compatible custom template --- src/config/openclawProviderPresets.ts | 386 ++++++++++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 src/config/openclawProviderPresets.ts diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts new file mode 100644 index 000000000..91b3fbad3 --- /dev/null +++ b/src/config/openclawProviderPresets.ts @@ -0,0 +1,386 @@ +/** + * OpenClaw provider presets configuration + * OpenClaw uses models.providers structure with custom provider configs + */ +import type { ProviderCategory, OpenClawProviderConfig } from "../types"; +import type { PresetTheme, TemplateValueConfig } from "./claudeProviderPresets"; + +export interface OpenClawProviderPreset { + name: string; + websiteUrl: string; + apiKeyUrl?: string; + /** OpenClaw settings_config structure */ + settingsConfig: OpenClawProviderConfig; + isOfficial?: boolean; + isPartner?: boolean; + partnerPromotionKey?: string; + category?: ProviderCategory; + /** Template variable definitions */ + templateValues?: Record; + /** Visual theme config */ + theme?: PresetTheme; + /** Icon name */ + icon?: string; + /** Icon color */ + iconColor?: string; + /** Mark as custom template (for UI distinction) */ + isCustomTemplate?: boolean; +} + +/** + * OpenClaw API protocol options + */ +export const openclawApiProtocols = [ + { value: "openai-completions", label: "OpenAI Completions" }, + { value: "anthropic", label: "Anthropic" }, + { value: "google-generative-ai", label: "Google Generative AI" }, +] as const; + +/** + * OpenClaw provider presets list + */ +export const openclawProviderPresets: OpenClawProviderPreset[] = [ + // ========== Chinese Officials ========== + { + name: "DeepSeek", + websiteUrl: "https://platform.deepseek.com", + apiKeyUrl: "https://platform.deepseek.com/api_keys", + settingsConfig: { + baseUrl: "https://api.deepseek.com/v1", + apiKey: "", + api: "openai-completions", + models: [ + { id: "deepseek-chat", name: "DeepSeek V3.2" }, + { id: "deepseek-reasoner", name: "DeepSeek R1" }, + ], + }, + category: "cn_official", + icon: "deepseek", + iconColor: "#1E88E5", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + }, + { + name: "Zhipu GLM", + websiteUrl: "https://open.bigmodel.cn", + apiKeyUrl: "https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII", + settingsConfig: { + baseUrl: "https://open.bigmodel.cn/api/paas/v4", + apiKey: "", + api: "openai-completions", + models: [{ id: "glm-4.7", name: "GLM-4.7" }], + }, + category: "cn_official", + isPartner: true, + partnerPromotionKey: "zhipu", + icon: "zhipu", + iconColor: "#0F62FE", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: "https://open.bigmodel.cn/api/paas/v4", + defaultValue: "https://open.bigmodel.cn/api/paas/v4", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, + { + name: "Qwen Coder", + websiteUrl: "https://bailian.console.aliyun.com", + apiKeyUrl: "https://bailian.console.aliyun.com/#/api-key", + settingsConfig: { + baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", + apiKey: "", + api: "openai-completions", + models: [{ id: "qwen3-max", name: "Qwen3 Max" }], + }, + category: "cn_official", + icon: "qwen", + iconColor: "#FF6A00", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: "https://dashscope.aliyuncs.com/compatible-mode/v1", + defaultValue: "https://dashscope.aliyuncs.com/compatible-mode/v1", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + }, + { + name: "Kimi k2.5", + websiteUrl: "https://platform.moonshot.cn/console", + apiKeyUrl: "https://platform.moonshot.cn/console/api-keys", + settingsConfig: { + baseUrl: "https://api.moonshot.cn/v1", + apiKey: "", + api: "openai-completions", + models: [{ id: "kimi-k2.5", name: "Kimi K2.5" }], + }, + category: "cn_official", + icon: "kimi", + iconColor: "#6366F1", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: "https://api.moonshot.cn/v1", + defaultValue: "https://api.moonshot.cn/v1", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + }, + { + name: "MiniMax", + websiteUrl: "https://platform.minimaxi.com", + apiKeyUrl: "https://platform.minimaxi.com/subscribe/coding-plan", + settingsConfig: { + baseUrl: "https://api.minimaxi.com/v1", + apiKey: "", + api: "openai-completions", + models: [{ id: "MiniMax-M2.1", name: "MiniMax M2.1" }], + }, + category: "cn_official", + isPartner: true, + partnerPromotionKey: "minimax_cn", + theme: { + backgroundColor: "#f64551", + textColor: "#FFFFFF", + }, + icon: "minimax", + iconColor: "#FF6B6B", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, + + // ========== Aggregators ========== + { + name: "AiHubMix", + websiteUrl: "https://aihubmix.com", + apiKeyUrl: "https://aihubmix.com", + settingsConfig: { + baseUrl: "https://aihubmix.com/v1", + apiKey: "", + api: "anthropic", + models: [ + { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, + { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + ], + }, + category: "aggregator", + icon: "aihubmix", + iconColor: "#006FFB", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, + { + name: "DMXAPI", + websiteUrl: "https://www.dmxapi.cn", + apiKeyUrl: "https://www.dmxapi.cn", + settingsConfig: { + baseUrl: "https://www.dmxapi.cn/v1", + apiKey: "", + api: "anthropic", + models: [ + { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, + { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + ], + }, + category: "aggregator", + isPartner: true, + partnerPromotionKey: "dmxapi", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, + { + name: "OpenRouter", + websiteUrl: "https://openrouter.ai", + apiKeyUrl: "https://openrouter.ai/keys", + settingsConfig: { + baseUrl: "https://openrouter.ai/api/v1", + apiKey: "", + api: "openai-completions", + models: [ + { id: "anthropic/claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, + { id: "anthropic/claude-opus-4.5", name: "Claude Opus 4.5" }, + ], + }, + category: "aggregator", + icon: "openrouter", + iconColor: "#6566F1", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "sk-or-...", + editorValue: "", + }, + }, + }, + { + name: "ModelScope", + websiteUrl: "https://modelscope.cn", + apiKeyUrl: "https://modelscope.cn/my/myaccesstoken", + settingsConfig: { + baseUrl: "https://api-inference.modelscope.cn/v1", + apiKey: "", + api: "openai-completions", + models: [{ id: "ZhipuAI/GLM-4.7", name: "GLM-4.7" }], + }, + category: "aggregator", + icon: "modelscope", + iconColor: "#624AFF", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: "https://api-inference.modelscope.cn/v1", + defaultValue: "https://api-inference.modelscope.cn/v1", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, + + // ========== Third Party Partners ========== + { + name: "PackyCode", + websiteUrl: "https://www.packyapi.com", + apiKeyUrl: "https://www.packyapi.com/register?aff=cc-switch", + settingsConfig: { + baseUrl: "https://www.packyapi.com/v1", + apiKey: "", + api: "anthropic", + models: [ + { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, + { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + ], + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "packycode", + icon: "packycode", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, + { + name: "Cubence", + websiteUrl: "https://cubence.com", + apiKeyUrl: "https://cubence.com/signup?code=CCSWITCH&source=ccs", + settingsConfig: { + baseUrl: "https://api.cubence.com/v1", + apiKey: "", + api: "anthropic", + models: [ + { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, + { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + ], + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "cubence", + icon: "cubence", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, + { + name: "AIGoCode", + websiteUrl: "https://aigocode.com", + apiKeyUrl: "https://aigocode.com/invite/CC-SWITCH", + settingsConfig: { + baseUrl: "https://api.aigocode.com/v1", + apiKey: "", + api: "anthropic", + models: [ + { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, + { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + ], + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "aigocode", + icon: "aigocode", + iconColor: "#5B7FFF", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, + + // ========== Custom Template ========== + { + name: "OpenAI Compatible", + websiteUrl: "", + settingsConfig: { + baseUrl: "", + apiKey: "", + api: "openai-completions", + models: [], + }, + category: "custom", + isCustomTemplate: true, + icon: "generic", + iconColor: "#6B7280", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: "https://api.example.com/v1", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, +]; From fedb08e846c0c3e7f265057d8019384c3f7f1a5b Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:32:02 +0800 Subject: [PATCH 16/51] feat(ui): add OpenClaw support to frontend components - Update App.tsx with openclaw visibility and skills fallback - Add OpenClaw to AppSwitcher icon and display name maps - Update McpFormModal with openclaw in enabled apps - Update PromptFormModal/Panel with openclaw filename map - Update EndpointSpeedTest with openclaw timeout - Update AppVisibilitySettings with openclaw toggle - Update test state with openclaw defaults --- src/App.tsx | 4 +++- src/components/AppSwitcher.tsx | 2 ++ src/components/mcp/McpFormModal.tsx | 2 ++ src/components/prompts/PromptFormModal.tsx | 1 + src/components/prompts/PromptFormPanel.tsx | 1 + src/components/providers/forms/EndpointSpeedTest.tsx | 1 + .../providers/forms/hooks/useBaseUrlState.ts | 3 ++- src/components/settings/AppVisibilitySettings.tsx | 2 ++ tests/msw/state.ts | 12 ++++++++---- 9 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4f0d47785..713fc8e1e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -123,6 +123,7 @@ function App() { codex: true, gemini: true, opencode: true, + openclaw: true, }; const getFirstVisibleApp = (): AppId => { @@ -130,6 +131,7 @@ function App() { if (visibleApps.codex) return "codex"; if (visibleApps.gemini) return "gemini"; if (visibleApps.opencode) return "opencode"; + if (visibleApps.openclaw) return "openclaw"; return "claude"; // fallback }; @@ -586,7 +588,7 @@ function App() { return ( ); case "mcp": diff --git a/src/components/AppSwitcher.tsx b/src/components/AppSwitcher.tsx index b91c05273..717bb690c 100644 --- a/src/components/AppSwitcher.tsx +++ b/src/components/AppSwitcher.tsx @@ -29,12 +29,14 @@ export function AppSwitcher({ codex: "openai", gemini: "gemini", opencode: "opencode", + openclaw: "openclaw", }; const appDisplayName: Record = { claude: "Claude", codex: "Codex", gemini: "Gemini", opencode: "OpenCode", + openclaw: "OpenClaw", }; // Filter apps based on visibility settings (default all visible) diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index 93d12745d..70ab3b03a 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -66,6 +66,7 @@ const McpFormModal: React.FC = ({ codex: boolean; gemini: boolean; opencode: boolean; + openclaw: boolean; }>(() => { if (initialData?.apps) { return { ...initialData.apps }; @@ -75,6 +76,7 @@ const McpFormModal: React.FC = ({ codex: defaultEnabledApps.includes("codex"), gemini: defaultEnabledApps.includes("gemini"), opencode: defaultEnabledApps.includes("opencode"), + openclaw: defaultEnabledApps.includes("openclaw"), }; }); diff --git a/src/components/prompts/PromptFormModal.tsx b/src/components/prompts/PromptFormModal.tsx index 970265d06..195d856f4 100644 --- a/src/components/prompts/PromptFormModal.tsx +++ b/src/components/prompts/PromptFormModal.tsx @@ -35,6 +35,7 @@ const PromptFormModal: React.FC = ({ codex: "AGENTS.md", gemini: "GEMINI.md", opencode: "AGENTS.md", + openclaw: "AGENTS.md", }; const filename = filenameMap[appId]; const [name, setName] = useState(""); diff --git a/src/components/prompts/PromptFormPanel.tsx b/src/components/prompts/PromptFormPanel.tsx index 83a423209..c4481fa4c 100644 --- a/src/components/prompts/PromptFormPanel.tsx +++ b/src/components/prompts/PromptFormPanel.tsx @@ -29,6 +29,7 @@ const PromptFormPanel: React.FC = ({ codex: "AGENTS.md", gemini: "GEMINI.md", opencode: "AGENTS.md", + openclaw: "AGENTS.md", }; const filename = filenameMap[appId]; const [name, setName] = useState(""); diff --git a/src/components/providers/forms/EndpointSpeedTest.tsx b/src/components/providers/forms/EndpointSpeedTest.tsx index 1a0b39f99..2ede2f019 100644 --- a/src/components/providers/forms/EndpointSpeedTest.tsx +++ b/src/components/providers/forms/EndpointSpeedTest.tsx @@ -14,6 +14,7 @@ const ENDPOINT_TIMEOUT_SECS: Record = { claude: 8, gemini: 8, opencode: 8, + openclaw: 8, }; interface TestResult { diff --git a/src/components/providers/forms/hooks/useBaseUrlState.ts b/src/components/providers/forms/hooks/useBaseUrlState.ts index 75e39594a..037dfeb3b 100644 --- a/src/components/providers/forms/hooks/useBaseUrlState.ts +++ b/src/components/providers/forms/hooks/useBaseUrlState.ts @@ -4,9 +4,10 @@ import { setCodexBaseUrl as setCodexBaseUrlInConfig, } from "@/utils/providerConfigUtils"; import type { ProviderCategory } from "@/types"; +import type { AppId } from "@/lib/api"; interface UseBaseUrlStateProps { - appType: "claude" | "codex" | "gemini" | "opencode"; + appType: AppId; category: ProviderCategory | undefined; settingsConfig: string; codexConfig?: string; diff --git a/src/components/settings/AppVisibilitySettings.tsx b/src/components/settings/AppVisibilitySettings.tsx index 3ed80dc55..0c30472ce 100644 --- a/src/components/settings/AppVisibilitySettings.tsx +++ b/src/components/settings/AppVisibilitySettings.tsx @@ -20,6 +20,7 @@ const APP_CONFIG: Array<{ { id: "codex", icon: "openai", nameKey: "apps.codex" }, { id: "gemini", icon: "gemini", nameKey: "apps.gemini" }, { id: "opencode", icon: "opencode", nameKey: "apps.opencode" }, + { id: "openclaw", icon: "openclaw", nameKey: "apps.openclaw" }, ]; export function AppVisibilitySettings({ @@ -33,6 +34,7 @@ export function AppVisibilitySettings({ codex: true, gemini: true, opencode: true, + openclaw: true, }; // Count how many apps are currently visible diff --git a/tests/msw/state.ts b/tests/msw/state.ts index ac385a675..2db234840 100644 --- a/tests/msw/state.ts +++ b/tests/msw/state.ts @@ -58,6 +58,7 @@ const createDefaultProviders = (): ProvidersByApp => ({ }, }, opencode: {}, + openclaw: {}, }); const createDefaultCurrent = (): CurrentProviderState => ({ @@ -65,6 +66,7 @@ const createDefaultCurrent = (): CurrentProviderState => ({ codex: "codex-1", gemini: "gemini-1", opencode: "", + openclaw: "", }); let providers = createDefaultProviders(); @@ -84,7 +86,7 @@ let mcpConfigs: McpConfigState = { id: "sample", name: "Sample Claude Server", enabled: true, - apps: { claude: true, codex: false, gemini: false, opencode: false }, + apps: { claude: true, codex: false, gemini: false, opencode: false, openclaw: false }, server: { type: "stdio", command: "claude-server", @@ -96,7 +98,7 @@ let mcpConfigs: McpConfigState = { id: "httpServer", name: "HTTP Codex Server", enabled: false, - apps: { claude: false, codex: true, gemini: false, opencode: false }, + apps: { claude: false, codex: true, gemini: false, opencode: false, openclaw: false }, server: { type: "http", url: "http://localhost:3000", @@ -105,6 +107,7 @@ let mcpConfigs: McpConfigState = { }, gemini: {}, opencode: {}, + openclaw: {}, }; const cloneProviders = (value: ProvidersByApp) => @@ -128,7 +131,7 @@ export const resetProviderState = () => { id: "sample", name: "Sample Claude Server", enabled: true, - apps: { claude: true, codex: false, gemini: false, opencode: false }, + apps: { claude: true, codex: false, gemini: false, opencode: false, openclaw: false }, server: { type: "stdio", command: "claude-server", @@ -140,7 +143,7 @@ export const resetProviderState = () => { id: "httpServer", name: "HTTP Codex Server", enabled: false, - apps: { claude: false, codex: true, gemini: false, opencode: false }, + apps: { claude: false, codex: true, gemini: false, opencode: false, openclaw: false }, server: { type: "http", url: "http://localhost:3000", @@ -149,6 +152,7 @@ export const resetProviderState = () => { }, gemini: {}, opencode: {}, + openclaw: {}, }; }; From d56e0b0344d1efccbe2efa9782f4d7e7b5a80753 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 20:32:13 +0800 Subject: [PATCH 17/51] feat(i18n): add OpenClaw translations - Add "openclaw": "OpenClaw" to apps translations in zh.json - Add "openclaw": "OpenClaw" to apps translations in en.json - Add "openclaw": "OpenClaw" to apps translations in ja.json --- src/i18n/locales/en.json | 9 ++++++--- src/i18n/locales/ja.json | 9 ++++++--- src/i18n/locales/zh.json | 9 ++++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 695a776fb..137f2cf62 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -478,7 +478,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" }, "sessionManager": { "title": "Session Manager", @@ -926,7 +927,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" } }, "userLevelPath": "User-level MCP path", @@ -1241,7 +1243,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" }, "installFromZip": { "button": "Install from ZIP", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 825e11a6d..e458b6f87 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -478,7 +478,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" }, "sessionManager": { "title": "セッション管理", @@ -926,7 +927,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" } }, "userLevelPath": "ユーザーレベルの MCP パス", @@ -1239,7 +1241,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" }, "installFromZip": { "button": "ZIP からインストール", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index c044c40e7..82d36b804 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -478,7 +478,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" }, "sessionManager": { "title": "会话管理", @@ -926,7 +927,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" } }, "userLevelPath": "用户级 MCP 配置路径", @@ -1241,7 +1243,8 @@ "claude": "Claude", "codex": "Codex", "gemini": "Gemini", - "opencode": "OpenCode" + "opencode": "OpenCode", + "openclaw": "OpenClaw" }, "installFromZip": { "button": "从 ZIP 安装", From 28b125b34f324affbfc37fcb80ec8ad5fcbd7ebb Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 22:27:22 +0800 Subject: [PATCH 18/51] fix(openclaw): remove MCP/Skills/Prompts support from OpenClaw OpenClaw only needs provider management functionality, not MCP, Skills, or Prompts features. This commit removes the incorrectly added support: - Revert SCHEMA_VERSION from 6 to 5 (remove v5->v6 migration) - Remove enabled_openclaw field from mcp_servers and skills tables - Remove openclaw field from McpApps and SkillApps structs - Update DAO queries to exclude enabled_openclaw column - Fix frontend components and types to exclude openclaw from MCP/Prompts - Update all related test files --- src-tauri/src/app_config.rs | 22 ++++---------- src-tauri/src/database/dao/mcp.rs | 9 ++---- src-tauri/src/database/dao/skills.rs | 19 +++++------- src-tauri/src/database/mod.rs | 2 +- src-tauri/src/database/schema.rs | 34 +--------------------- src-tauri/src/deeplink/mcp.rs | 6 ++-- src-tauri/src/mcp/claude.rs | 1 - src-tauri/src/mcp/codex.rs | 1 - src-tauri/src/mcp/gemini.rs | 1 - src-tauri/src/mcp/opencode.rs | 1 - src/components/mcp/McpFormModal.tsx | 2 -- src/components/prompts/PromptFormModal.tsx | 5 ++-- src/components/prompts/PromptFormPanel.tsx | 5 ++-- src/types.ts | 1 - tests/msw/state.ts | 8 ++--- 15 files changed, 31 insertions(+), 86 deletions(-) diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index d53e7f38e..323f8a511 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -15,8 +15,6 @@ pub struct McpApps { pub gemini: bool, #[serde(default)] pub opencode: bool, - #[serde(default)] - pub openclaw: bool, } impl McpApps { @@ -27,7 +25,7 @@ impl McpApps { AppType::Codex => self.codex, AppType::Gemini => self.gemini, AppType::OpenCode => self.opencode, - AppType::OpenClaw => self.openclaw, + AppType::OpenClaw => false, // OpenClaw doesn't support MCP } } @@ -38,7 +36,7 @@ impl McpApps { AppType::Codex => self.codex = enabled, AppType::Gemini => self.gemini = enabled, AppType::OpenCode => self.opencode = enabled, - AppType::OpenClaw => self.openclaw = enabled, + AppType::OpenClaw => {} // OpenClaw doesn't support MCP, ignore } } @@ -57,15 +55,12 @@ impl McpApps { if self.opencode { apps.push(AppType::OpenCode); } - if self.openclaw { - apps.push(AppType::OpenClaw); - } apps } /// 检查是否所有应用都未启用 pub fn is_empty(&self) -> bool { - !self.claude && !self.codex && !self.gemini && !self.opencode && !self.openclaw + !self.claude && !self.codex && !self.gemini && !self.opencode } } @@ -80,8 +75,6 @@ pub struct SkillApps { pub gemini: bool, #[serde(default)] pub opencode: bool, - #[serde(default)] - pub openclaw: bool, } impl SkillApps { @@ -92,7 +85,7 @@ impl SkillApps { AppType::Codex => self.codex, AppType::Gemini => self.gemini, AppType::OpenCode => self.opencode, - AppType::OpenClaw => self.openclaw, + AppType::OpenClaw => false, // OpenClaw doesn't support Skills } } @@ -103,7 +96,7 @@ impl SkillApps { AppType::Codex => self.codex = enabled, AppType::Gemini => self.gemini = enabled, AppType::OpenCode => self.opencode = enabled, - AppType::OpenClaw => self.openclaw = enabled, + AppType::OpenClaw => {} // OpenClaw doesn't support Skills, ignore } } @@ -122,15 +115,12 @@ impl SkillApps { if self.opencode { apps.push(AppType::OpenCode); } - if self.openclaw { - apps.push(AppType::OpenClaw); - } apps } /// 检查是否所有应用都未启用 pub fn is_empty(&self) -> bool { - !self.claude && !self.codex && !self.gemini && !self.opencode && !self.openclaw + !self.claude && !self.codex && !self.gemini && !self.opencode } /// 仅启用指定应用(其他应用设为禁用) diff --git a/src-tauri/src/database/dao/mcp.rs b/src-tauri/src/database/dao/mcp.rs index 52d73ef87..d5c60163f 100644 --- a/src-tauri/src/database/dao/mcp.rs +++ b/src-tauri/src/database/dao/mcp.rs @@ -13,7 +13,7 @@ impl Database { pub fn get_all_mcp_servers(&self) -> Result, AppError> { let conn = lock_conn!(self.conn); let mut stmt = conn.prepare( - "SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw + "SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode FROM mcp_servers ORDER BY name ASC, id ASC" ).map_err(|e| AppError::Database(e.to_string()))?; @@ -31,7 +31,6 @@ impl Database { let enabled_codex: bool = row.get(8)?; let enabled_gemini: bool = row.get(9)?; let enabled_opencode: bool = row.get(10)?; - let enabled_openclaw: bool = row.get(11)?; let server = serde_json::from_str(&server_config_str).unwrap_or_default(); let tags = serde_json::from_str(&tags_str).unwrap_or_default(); @@ -47,7 +46,6 @@ impl Database { codex: enabled_codex, gemini: enabled_gemini, opencode: enabled_opencode, - openclaw: enabled_openclaw, }, description, homepage, @@ -72,8 +70,8 @@ impl Database { conn.execute( "INSERT OR REPLACE INTO mcp_servers ( id, name, server_config, description, homepage, docs, tags, - enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + enabled_claude, enabled_codex, enabled_gemini, enabled_opencode + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", params![ server.id, server.name, @@ -89,7 +87,6 @@ impl Database { server.apps.codex, server.apps.gemini, server.apps.opencode, - server.apps.openclaw, ], ) .map_err(|e| AppError::Database(e.to_string()))?; diff --git a/src-tauri/src/database/dao/skills.rs b/src-tauri/src/database/dao/skills.rs index f29e89a63..2254cb061 100644 --- a/src-tauri/src/database/dao/skills.rs +++ b/src-tauri/src/database/dao/skills.rs @@ -22,7 +22,7 @@ impl Database { let mut stmt = conn .prepare( "SELECT id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at FROM skills ORDER BY name ASC", ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -43,9 +43,8 @@ impl Database { codex: row.get(9)?, gemini: row.get(10)?, opencode: row.get(11)?, - openclaw: row.get(12)?, }, - installed_at: row.get(13)?, + installed_at: row.get(12)?, }) }) .map_err(|e| AppError::Database(e.to_string()))?; @@ -64,7 +63,7 @@ impl Database { let mut stmt = conn .prepare( "SELECT id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at FROM skills WHERE id = ?1", ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -84,9 +83,8 @@ impl Database { codex: row.get(9)?, gemini: row.get(10)?, opencode: row.get(11)?, - openclaw: row.get(12)?, }, - installed_at: row.get(13)?, + installed_at: row.get(12)?, }) }); @@ -103,8 +101,8 @@ impl Database { conn.execute( "INSERT OR REPLACE INTO skills (id, name, description, directory, repo_owner, repo_name, repo_branch, - readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, installed_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", params![ skill.id, skill.name, @@ -118,7 +116,6 @@ impl Database { skill.apps.codex, skill.apps.gemini, skill.apps.opencode, - skill.apps.openclaw, skill.installed_at, ], ) @@ -148,8 +145,8 @@ impl Database { let conn = lock_conn!(self.conn); let affected = conn .execute( - "UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4, enabled_openclaw = ?5 WHERE id = ?6", - params![apps.claude, apps.codex, apps.gemini, apps.opencode, apps.openclaw, id], + "UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4 WHERE id = ?5", + params![apps.claude, apps.codex, apps.gemini, apps.opencode, id], ) .map_err(|e| AppError::Database(e.to_string()))?; Ok(affected > 0) diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 352593fd6..41c26e978 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -48,7 +48,7 @@ const DB_BACKUP_RETAIN: usize = 10; /// 当前 Schema 版本号 /// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑 -pub(crate) const SCHEMA_VERSION: i32 = 6; +pub(crate) const SCHEMA_VERSION: i32 = 5; /// 安全地序列化 JSON,避免 unwrap panic pub(crate) fn to_json_string(value: &T) -> Result { diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 924939125..bca3c2de3 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -58,8 +58,7 @@ impl Database { id TEXT PRIMARY KEY, name TEXT NOT NULL, server_config TEXT NOT NULL, description TEXT, homepage TEXT, docs TEXT, tags TEXT NOT NULL DEFAULT '[]', enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0, - enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0, - enabled_openclaw BOOLEAN NOT NULL DEFAULT 0 + enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0 )", [], ) @@ -87,7 +86,6 @@ impl Database { enabled_codex BOOLEAN NOT NULL DEFAULT 0, enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0, - enabled_openclaw BOOLEAN NOT NULL DEFAULT 0, installed_at INTEGER NOT NULL DEFAULT 0 )", [], @@ -362,11 +360,6 @@ impl Database { Self::migrate_v4_to_v5(conn)?; Self::set_user_version(conn, 5)?; } - 5 => { - log::info!("迁移数据库从 v5 到 v6(OpenClaw 支持)"); - Self::migrate_v5_to_v6(conn)?; - Self::set_user_version(conn, 6)?; - } _ => { return Err(AppError::Database(format!( "未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}" @@ -859,7 +852,6 @@ impl Database { enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0, enabled_gemini BOOLEAN NOT NULL DEFAULT 0, - enabled_openclaw BOOLEAN NOT NULL DEFAULT 0, installed_at INTEGER NOT NULL DEFAULT 0 )", [], @@ -922,30 +914,6 @@ impl Database { Ok(()) } - /// v5 -> v6 迁移:添加 OpenClaw 支持 - /// - /// 为 mcp_servers 和 skills 表添加 enabled_openclaw 列。 - fn migrate_v5_to_v6(conn: &Connection) -> Result<(), AppError> { - // 为 mcp_servers 表添加 enabled_openclaw 列 - Self::add_column_if_missing( - conn, - "mcp_servers", - "enabled_openclaw", - "BOOLEAN NOT NULL DEFAULT 0", - )?; - - // 为 skills 表添加 enabled_openclaw 列 - Self::add_column_if_missing( - conn, - "skills", - "enabled_openclaw", - "BOOLEAN NOT NULL DEFAULT 0", - )?; - - log::info!("v5 -> v6 迁移完成:已添加 OpenClaw 支持"); - Ok(()) - } - /// 插入默认模型定价数据 /// 格式: (model_id, display_name, input, output, cache_read, cache_creation) /// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致 diff --git a/src-tauri/src/deeplink/mcp.rs b/src-tauri/src/deeplink/mcp.rs index 67f3d92cb..37df96579 100644 --- a/src-tauri/src/deeplink/mcp.rs +++ b/src-tauri/src/deeplink/mcp.rs @@ -167,7 +167,6 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result { codex: false, gemini: false, opencode: false, - openclaw: false, }; for app in apps_str.split(',') { @@ -176,7 +175,10 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result { "codex" => apps.codex = true, "gemini" => apps.gemini = true, "opencode" => apps.opencode = true, - "openclaw" => apps.openclaw = true, + "openclaw" => { + // OpenClaw doesn't support MCP, ignore silently + log::debug!("OpenClaw doesn't support MCP, ignoring in apps parameter"); + } other => { return Err(AppError::InvalidInput(format!( "Invalid app in 'apps': {other}" diff --git a/src-tauri/src/mcp/claude.rs b/src-tauri/src/mcp/claude.rs index 3ba1a310b..25d2f426a 100644 --- a/src-tauri/src/mcp/claude.rs +++ b/src-tauri/src/mcp/claude.rs @@ -92,7 +92,6 @@ pub fn import_from_claude(config: &mut MultiAppConfig) -> Result Result codex: true, gemini: false, opencode: false, - openclaw: false, }, description: None, homepage: None, diff --git a/src-tauri/src/mcp/gemini.rs b/src-tauri/src/mcp/gemini.rs index b2725da27..c8a7809f3 100644 --- a/src-tauri/src/mcp/gemini.rs +++ b/src-tauri/src/mcp/gemini.rs @@ -88,7 +88,6 @@ pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result Result = ({ codex: boolean; gemini: boolean; opencode: boolean; - openclaw: boolean; }>(() => { if (initialData?.apps) { return { ...initialData.apps }; @@ -76,7 +75,6 @@ const McpFormModal: React.FC = ({ codex: defaultEnabledApps.includes("codex"), gemini: defaultEnabledApps.includes("gemini"), opencode: defaultEnabledApps.includes("opencode"), - openclaw: defaultEnabledApps.includes("openclaw"), }; }); diff --git a/src/components/prompts/PromptFormModal.tsx b/src/components/prompts/PromptFormModal.tsx index 195d856f4..84e18d255 100644 --- a/src/components/prompts/PromptFormModal.tsx +++ b/src/components/prompts/PromptFormModal.tsx @@ -30,14 +30,13 @@ const PromptFormModal: React.FC = ({ }) => { const { t } = useTranslation(); const appName = t(`apps.${appId}`); - const filenameMap: Record = { + const filenameMap: Record, string> = { claude: "CLAUDE.md", codex: "AGENTS.md", gemini: "GEMINI.md", opencode: "AGENTS.md", - openclaw: "AGENTS.md", }; - const filename = filenameMap[appId]; + const filename = filenameMap[appId as Exclude]; const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [content, setContent] = useState(""); diff --git a/src/components/prompts/PromptFormPanel.tsx b/src/components/prompts/PromptFormPanel.tsx index c4481fa4c..fcdc458b5 100644 --- a/src/components/prompts/PromptFormPanel.tsx +++ b/src/components/prompts/PromptFormPanel.tsx @@ -24,14 +24,13 @@ const PromptFormPanel: React.FC = ({ }) => { const { t } = useTranslation(); const appName = t(`apps.${appId}`); - const filenameMap: Record = { + const filenameMap: Record, string> = { claude: "CLAUDE.md", codex: "AGENTS.md", gemini: "GEMINI.md", opencode: "AGENTS.md", - openclaw: "AGENTS.md", }; - const filename = filenameMap[appId]; + const filename = filenameMap[appId as Exclude]; const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [content, setContent] = useState(""); diff --git a/src/types.ts b/src/types.ts index 0aaa90ac1..dba9bace4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -288,7 +288,6 @@ export interface McpApps { codex: boolean; gemini: boolean; opencode: boolean; - openclaw: boolean; } // MCP 服务器条目(v3.7.0 统一结构) diff --git a/tests/msw/state.ts b/tests/msw/state.ts index 2db234840..bb08e101f 100644 --- a/tests/msw/state.ts +++ b/tests/msw/state.ts @@ -86,7 +86,7 @@ let mcpConfigs: McpConfigState = { id: "sample", name: "Sample Claude Server", enabled: true, - apps: { claude: true, codex: false, gemini: false, opencode: false, openclaw: false }, + apps: { claude: true, codex: false, gemini: false, opencode: false }, server: { type: "stdio", command: "claude-server", @@ -98,7 +98,7 @@ let mcpConfigs: McpConfigState = { id: "httpServer", name: "HTTP Codex Server", enabled: false, - apps: { claude: false, codex: true, gemini: false, opencode: false, openclaw: false }, + apps: { claude: false, codex: true, gemini: false, opencode: false }, server: { type: "http", url: "http://localhost:3000", @@ -131,7 +131,7 @@ export const resetProviderState = () => { id: "sample", name: "Sample Claude Server", enabled: true, - apps: { claude: true, codex: false, gemini: false, opencode: false, openclaw: false }, + apps: { claude: true, codex: false, gemini: false, opencode: false }, server: { type: "stdio", command: "claude-server", @@ -143,7 +143,7 @@ export const resetProviderState = () => { id: "httpServer", name: "HTTP Codex Server", enabled: false, - apps: { claude: false, codex: true, gemini: false, opencode: false, openclaw: false }, + apps: { claude: false, codex: true, gemini: false, opencode: false }, server: { type: "http", url: "http://localhost:3000", From 95391f19acffeec3aa84ba0a0aa3da6d6c066a66 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 22:28:38 +0800 Subject: [PATCH 19/51] fix(tests): add mcp_servers table to v4 migration test fixture Also includes OpenClaw icon that was added in previous commits. --- src-tauri/src/database/tests.rs | 9 +++++++++ src/icons/extracted/index.ts | 1 + src/icons/extracted/metadata.ts | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/src-tauri/src/database/tests.rs b/src-tauri/src/database/tests.rs index a690cd0ef..8e37b3d60 100644 --- a/src-tauri/src/database/tests.rs +++ b/src-tauri/src/database/tests.rs @@ -297,6 +297,15 @@ fn schema_migration_v4_adds_pricing_model_columns() { r#" CREATE TABLE proxy_config (app_type TEXT PRIMARY KEY); CREATE TABLE proxy_request_logs (request_id TEXT PRIMARY KEY, model TEXT NOT NULL); + CREATE TABLE mcp_servers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + server_config TEXT NOT NULL, + enabled_claude INTEGER NOT NULL DEFAULT 0, + enabled_codex INTEGER NOT NULL DEFAULT 0, + enabled_gemini INTEGER NOT NULL DEFAULT 0, + enabled_opencode INTEGER NOT NULL DEFAULT 0 + ); "#, ) .expect("seed v4 schema"); diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts index dcc9bb12d..c93ccd6bb 100644 --- a/src/icons/extracted/index.ts +++ b/src/icons/extracted/index.ts @@ -37,6 +37,7 @@ export const icons: Record = { notion: `Notion`, ollama: `Ollama`, openai: `OpenAI`, + openclaw: `OpenClaw`, packycode: `PackyCode`, palm: `PaLM`, perplexity: `Perplexity`, diff --git a/src/icons/extracted/metadata.ts b/src/icons/extracted/metadata.ts index 0c103d028..6b2c8a8a1 100644 --- a/src/icons/extracted/metadata.ts +++ b/src/icons/extracted/metadata.ts @@ -247,6 +247,13 @@ export const iconMetadata: Record = { keywords: ["gpt", "chatgpt"], defaultColor: "currentColor", }, + openclaw: { + name: "openclaw", + displayName: "OpenClaw", + category: "ai-provider", + keywords: ["openclaw", "lobster", "claw"], + defaultColor: "#ff4f40", + }, packycode: { name: "packycode", displayName: "PackyCode", From 5c62c478783218d7b481a160eb91f63389c42f69 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Feb 2026 23:02:33 +0800 Subject: [PATCH 20/51] chore(openclaw): remove dead code functions from openclaw_config Remove 6 unused functions marked with #[allow(dead_code)]: - get_openclaw_skills_dir: Skills not supported - get_env_vars/set_env_var: Environment vars unused - get_mcp_servers/set_mcp_server/remove_mcp_server: MCP not supported OpenClaw integration only requires provider management functionality. --- src-tauri/src/openclaw_config.rs | 70 -------------------------------- 1 file changed, 70 deletions(-) diff --git a/src-tauri/src/openclaw_config.rs b/src-tauri/src/openclaw_config.rs index 5a30d2565..9488216c8 100644 --- a/src-tauri/src/openclaw_config.rs +++ b/src-tauri/src/openclaw_config.rs @@ -70,14 +70,6 @@ pub fn get_openclaw_config_path() -> PathBuf { get_openclaw_dir().join("openclaw.json") } -/// 获取 OpenClaw Skills 目录路径 -/// -/// 返回 `~/.openclaw/skills/` -#[allow(dead_code)] -pub fn get_openclaw_skills_dir() -> PathBuf { - get_openclaw_dir().join("skills") -} - // ============================================================================ // Type Definitions // ============================================================================ @@ -262,65 +254,3 @@ pub fn set_typed_provider(id: &str, config: &OpenClawProviderConfig) -> Result<( let value = serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?; set_provider(id, value) } - -// ============================================================================ -// Environment Variables -// ============================================================================ - -/// 获取环境变量配置 -#[allow(dead_code)] -pub fn get_env_vars() -> Result, AppError> { - let config = read_openclaw_config()?; - Ok(config - .get("env") - .and_then(|v| v.as_object()) - .cloned() - .unwrap_or_default()) -} - -/// 设置环境变量 -#[allow(dead_code)] -pub fn set_env_var(key: &str, value: &str) -> Result<(), AppError> { - let mut config = read_openclaw_config()?; - - if config.get("env").is_none() { - config["env"] = json!({}); - } - - if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) { - env.insert(key.to_string(), Value::String(value.to_string())); - } - - write_openclaw_config(&config) -} - -// ============================================================================ -// MCP Functions (Reserved for future use) -// ============================================================================ - -/// 获取所有 MCP 服务器配置 -/// -/// OpenClaw MCP 配置在 `agents.list[].mcp.servers` -/// 由于 OpenClaw MCP 支持仍在开发中(Issue #4834),暂时返回空 -#[allow(dead_code)] -pub fn get_mcp_servers() -> Result, AppError> { - // OpenClaw MCP support is under development - // Return empty for now - Ok(Map::new()) -} - -/// 设置 MCP 服务器配置 -#[allow(dead_code)] -pub fn set_mcp_server(_id: &str, _config: Value) -> Result<(), AppError> { - // OpenClaw MCP support is under development - log::warn!("OpenClaw MCP support is not yet implemented"); - Ok(()) -} - -/// 删除 MCP 服务器配置 -#[allow(dead_code)] -pub fn remove_mcp_server(_id: &str) -> Result<(), AppError> { - // OpenClaw MCP support is under development - log::warn!("OpenClaw MCP support is not yet implemented"); - Ok(()) -} From 9bbb6c51ff0b7c963bd43f91346ebaf0f3d64e34 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Feb 2026 22:45:59 +0800 Subject: [PATCH 21/51] feat(ui): add OpenClaw brand icon and component --- src/components/BrandIcons.tsx | 14 ++++++++++++++ src/icons/extracted/claw.svg | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/icons/extracted/claw.svg diff --git a/src/components/BrandIcons.tsx b/src/components/BrandIcons.tsx index f6a68c67a..65bdb487a 100644 --- a/src/components/BrandIcons.tsx +++ b/src/components/BrandIcons.tsx @@ -7,6 +7,7 @@ interface IconProps { import ClaudeSvg from "@/icons/extracted/claude.svg?url"; import OpenAISvg from "@/icons/extracted/openai.svg?url"; import GeminiSvg from "@/icons/extracted/gemini.svg?url"; +import OpenClawSvg from "@/icons/extracted/claw.svg?url"; export function ClaudeIcon({ size = 16, className = "" }: IconProps) { return ( @@ -47,6 +48,19 @@ export function GeminiIcon({ size = 16, className = "" }: IconProps) { ); } +export function OpenClawIcon({ size = 16, className = "" }: IconProps) { + return ( + OpenClaw + ); +} + // MCP icon uses inline SVG to support currentColor for hover effects export function McpIcon({ size = 16, className = "" }: IconProps) { return ( diff --git a/src/icons/extracted/claw.svg b/src/icons/extracted/claw.svg new file mode 100644 index 000000000..bcbc1e10c --- /dev/null +++ b/src/icons/extracted/claw.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + From 27bcfcb2b86fa2e380619a42ab957fe0ba3a6bb9 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 3 Feb 2026 23:29:28 +0800 Subject: [PATCH 22/51] fix(ui): add OpenClaw to app switcher Add "openclaw" to ALL_APPS array so OpenClaw appears in the main interface app switcher when enabled in visibility settings. --- src/components/AppSwitcher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AppSwitcher.tsx b/src/components/AppSwitcher.tsx index 717bb690c..3be13a0d9 100644 --- a/src/components/AppSwitcher.tsx +++ b/src/components/AppSwitcher.tsx @@ -9,7 +9,7 @@ interface AppSwitcherProps { compact?: boolean; } -const ALL_APPS: AppId[] = ["claude", "codex", "gemini", "opencode"]; +const ALL_APPS: AppId[] = ["claude", "codex", "gemini", "opencode", "openclaw"]; const STORAGE_KEY = "cc-switch-last-app"; export function AppSwitcher({ From 52db7941eaf10e052079e2e6b8feef0c8b466818 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 4 Feb 2026 00:09:25 +0800 Subject: [PATCH 23/51] fix(ui): update OpenClaw icon in icon index Replace old pixel-art style icon with new gradient lobster icon to match the SVG added in 2dad4729. --- src/icons/extracted/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts index c93ccd6bb..a87ab5d88 100644 --- a/src/icons/extracted/index.ts +++ b/src/icons/extracted/index.ts @@ -37,7 +37,7 @@ export const icons: Record = { notion: `Notion`, ollama: `Ollama`, openai: `OpenAI`, - openclaw: `OpenClaw`, + openclaw: `OpenClaw`, packycode: `PackyCode`, palm: `PaLM`, perplexity: `Perplexity`, From 47f2c47a2ff2ede69de9b4853c12cacf9f7f7c3e Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 4 Feb 2026 21:19:13 +0800 Subject: [PATCH 24/51] feat(openclaw): add agents.defaults config support - Add types for default model config (primary + fallbacks) - Add types for model catalog/allowlist with aliases - Extend OpenClawModelEntry with cost and contextWindow fields - Add Tauri commands: get/set_openclaw_default_model, get/set_openclaw_model_catalog - Create frontend API (src/lib/api/openclaw.ts) - Add suggestedDefaults to provider presets with model metadata - Add i18n translations (zh/en/ja) for openclawConfig.* --- src-tauri/src/commands/provider.rs | 35 ++++ src-tauri/src/lib.rs | 4 + src-tauri/src/openclaw_config.rs | 168 ++++++++++++++++- src/config/openclawProviderPresets.ts | 249 +++++++++++++++++++++++--- src/i18n/locales/en.json | 29 +++ src/i18n/locales/ja.json | 29 +++ src/i18n/locales/zh.json | 29 +++ src/lib/api/openclaw.ts | 59 ++++++ src/types.ts | 14 ++ 9 files changed, 595 insertions(+), 21 deletions(-) create mode 100644 src/lib/api/openclaw.ts diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 70ce9048e..d6d51397d 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -342,3 +342,38 @@ pub fn get_openclaw_live_provider_ids() -> Result, String> { .map(|providers| providers.keys().cloned().collect()) .map_err(|e| e.to_string()) } + +// ============================================================================ +// OpenClaw Agents Configuration Commands +// ============================================================================ + +/// 获取 OpenClaw 默认模型配置(agents.defaults.model) +#[tauri::command] +pub fn get_openclaw_default_model( +) -> Result, String> { + crate::openclaw_config::get_default_model().map_err(|e| e.to_string()) +} + +/// 设置 OpenClaw 默认模型配置(agents.defaults.model) +#[tauri::command] +pub fn set_openclaw_default_model( + model: crate::openclaw_config::OpenClawDefaultModel, +) -> Result<(), String> { + crate::openclaw_config::set_default_model(&model).map_err(|e| e.to_string()) +} + +/// 获取 OpenClaw 模型目录/允许列表(agents.defaults.models) +#[tauri::command] +pub fn get_openclaw_model_catalog( +) -> Result>, String> +{ + crate::openclaw_config::get_model_catalog().map_err(|e| e.to_string()) +} + +/// 设置 OpenClaw 模型目录/允许列表(agents.defaults.models) +#[tauri::command] +pub fn set_openclaw_model_catalog( + catalog: std::collections::HashMap, +) -> Result<(), String> { + crate::openclaw_config::set_model_catalog(&catalog).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dc36d60d9..05cde9676 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1007,6 +1007,10 @@ pub fn run() { // OpenClaw specific commands::import_openclaw_providers_from_live, commands::get_openclaw_live_provider_ids, + commands::get_openclaw_default_model, + commands::set_openclaw_default_model, + commands::get_openclaw_model_catalog, + commands::set_openclaw_model_catalog, // Global upstream proxy commands::get_global_proxy_url, commands::set_global_proxy_url, diff --git a/src-tauri/src/openclaw_config.rs b/src-tauri/src/openclaw_config.rs index 9488216c8..ab6082c1d 100644 --- a/src-tauri/src/openclaw_config.rs +++ b/src-tauri/src/openclaw_config.rs @@ -42,6 +42,7 @@ use crate::settings::get_openclaw_override_dir; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; +use std::collections::HashMap; use std::path::PathBuf; // ============================================================================ @@ -101,6 +102,7 @@ pub struct OpenClawProviderConfig { /// OpenClaw 模型条目 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct OpenClawModelEntry { /// 模型 ID pub id: String, @@ -109,11 +111,92 @@ pub struct OpenClawModelEntry { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, - /// 其他自定义字段 + /// 模型别名(用于快捷引用) + #[serde(skip_serializing_if = "Option::is_none")] + pub alias: Option, + + /// 模型成本(输入/输出价格) + #[serde(skip_serializing_if = "Option::is_none")] + pub cost: Option, + + /// 上下文窗口大小 + #[serde(skip_serializing_if = "Option::is_none")] + pub context_window: Option, + + /// 其他自定义字段(保留未知字段) #[serde(flatten)] pub extra: Map, } +/// OpenClaw 模型成本配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawModelCost { + /// 输入价格(每百万 token) + pub input: f64, + + /// 输出价格(每百万 token) + pub output: f64, + + /// 其他自定义字段(保留未知字段) + #[serde(flatten)] + pub other: HashMap, +} + +/// OpenClaw 默认模型配置(agents.defaults.model) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawDefaultModel { + /// 主模型 ID(格式:provider/model) + pub primary: String, + + /// 回退模型列表 + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub fallbacks: Vec, + + /// 其他自定义字段(保留未知字段) + #[serde(flatten)] + pub other: HashMap, +} + +/// OpenClaw 模型目录条目(agents.defaults.models 中的值) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawModelCatalogEntry { + /// 模型别名(用于 UI 显示) + #[serde(skip_serializing_if = "Option::is_none")] + pub alias: Option, + + /// 其他自定义字段(保留未知字段) + #[serde(flatten)] + pub other: HashMap, +} + +/// OpenClaw agents.defaults 配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawAgentsDefaults { + /// 默认模型配置 + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + + /// 模型目录/允许列表(键为 provider/model 格式) + #[serde(skip_serializing_if = "Option::is_none")] + pub models: Option>, + + /// 其他自定义字段(保留未知字段) + #[serde(flatten)] + pub other: HashMap, +} + +/// OpenClaw agents 顶层配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawAgents { + /// 默认配置 + #[serde(skip_serializing_if = "Option::is_none")] + pub defaults: Option, + + /// 其他自定义字段(保留未知字段) + #[serde(flatten)] + pub other: HashMap, +} + // ============================================================================ // Core Read/Write Functions // ============================================================================ @@ -254,3 +337,86 @@ pub fn set_typed_provider(id: &str, config: &OpenClawProviderConfig) -> Result<( let value = serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?; set_provider(id, value) } + +// ============================================================================ +// Agents Configuration Functions +// ============================================================================ + +/// 读取默认模型配置(agents.defaults.model) +pub fn get_default_model() -> Result, AppError> { + let config = read_openclaw_config()?; + + let Some(model_value) = config + .get("agents") + .and_then(|a| a.get("defaults")) + .and_then(|d| d.get("model")) + else { + return Ok(None); + }; + + let model = serde_json::from_value(model_value.clone()) + .map_err(|e| AppError::Config(format!("Failed to parse agents.defaults.model: {e}")))?; + + Ok(Some(model)) +} + +/// 设置默认模型配置(agents.defaults.model) +pub fn set_default_model(model: &OpenClawDefaultModel) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + // Ensure agents.defaults path exists, preserving unknown fields + ensure_agents_defaults_path(&mut config); + + let model_value = + serde_json::to_value(model).map_err(|e| AppError::JsonSerialize { source: e })?; + + config["agents"]["defaults"]["model"] = model_value; + + write_openclaw_config(&config) +} + +/// 读取模型目录/允许列表(agents.defaults.models) +pub fn get_model_catalog() -> Result>, AppError> { + let config = read_openclaw_config()?; + + let Some(models_value) = config + .get("agents") + .and_then(|a| a.get("defaults")) + .and_then(|d| d.get("models")) + else { + return Ok(None); + }; + + let catalog = serde_json::from_value(models_value.clone()) + .map_err(|e| AppError::Config(format!("Failed to parse agents.defaults.models: {e}")))?; + + Ok(Some(catalog)) +} + +/// 设置模型目录/允许列表(agents.defaults.models) +pub fn set_model_catalog( + catalog: &HashMap, +) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + // Ensure agents.defaults path exists, preserving unknown fields + ensure_agents_defaults_path(&mut config); + + let catalog_value = + serde_json::to_value(catalog).map_err(|e| AppError::JsonSerialize { source: e })?; + + config["agents"]["defaults"]["models"] = catalog_value; + + write_openclaw_config(&config) +} + +/// Ensure the `agents.defaults` path exists in the config, +/// preserving any existing unknown fields. +fn ensure_agents_defaults_path(config: &mut Value) { + if config.get("agents").is_none() { + config["agents"] = json!({}); + } + if config["agents"].get("defaults").is_none() { + config["agents"]["defaults"] = json!({}); + } +} diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index 91b3fbad3..3ef6e8796 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -2,9 +2,21 @@ * OpenClaw provider presets configuration * OpenClaw uses models.providers structure with custom provider configs */ -import type { ProviderCategory, OpenClawProviderConfig } from "../types"; +import type { + ProviderCategory, + OpenClawProviderConfig, + OpenClawDefaultModel, +} from "../types"; import type { PresetTheme, TemplateValueConfig } from "./claudeProviderPresets"; +/** Suggested default model configuration for a preset */ +export interface OpenClawSuggestedDefaults { + /** Default model config to apply (agents.defaults.model) */ + model?: OpenClawDefaultModel; + /** Model catalog entries to add (agents.defaults.models) */ + modelCatalog?: Record; +} + export interface OpenClawProviderPreset { name: string; websiteUrl: string; @@ -25,6 +37,8 @@ export interface OpenClawProviderPreset { iconColor?: string; /** Mark as custom template (for UI distinction) */ isCustomTemplate?: boolean; + /** Suggested default model configuration */ + suggestedDefaults?: OpenClawSuggestedDefaults; } /** @@ -50,8 +64,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "openai-completions", models: [ - { id: "deepseek-chat", name: "DeepSeek V3.2" }, - { id: "deepseek-reasoner", name: "DeepSeek R1" }, + { + id: "deepseek-chat", + name: "DeepSeek V3.2", + contextWindow: 64000, + cost: { input: 0.0005, output: 0.002 }, + }, + { + id: "deepseek-reasoner", + name: "DeepSeek R1", + contextWindow: 64000, + cost: { input: 0.0005, output: 0.002 }, + }, ], }, category: "cn_official", @@ -64,6 +88,16 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { + primary: "deepseek/deepseek-chat", + fallbacks: ["deepseek/deepseek-reasoner"], + }, + modelCatalog: { + "deepseek/deepseek-chat": { alias: "DeepSeek" }, + "deepseek/deepseek-reasoner": { alias: "R1" }, + }, + }, }, { name: "Zhipu GLM", @@ -73,7 +107,14 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ baseUrl: "https://open.bigmodel.cn/api/paas/v4", apiKey: "", api: "openai-completions", - models: [{ id: "glm-4.7", name: "GLM-4.7" }], + models: [ + { + id: "glm-4.7", + name: "GLM-4.7", + contextWindow: 128000, + cost: { input: 0.001, output: 0.001 }, + }, + ], }, category: "cn_official", isPartner: true, @@ -93,6 +134,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { primary: "zhipu/glm-4.7" }, + modelCatalog: { "zhipu/glm-4.7": { alias: "GLM" } }, + }, }, { name: "Qwen Coder", @@ -102,7 +147,14 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", apiKey: "", api: "openai-completions", - models: [{ id: "qwen3-max", name: "Qwen3 Max" }], + models: [ + { + id: "qwen3-max", + name: "Qwen3 Max", + contextWindow: 32000, + cost: { input: 0.002, output: 0.006 }, + }, + ], }, category: "cn_official", icon: "qwen", @@ -120,6 +172,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { primary: "qwen/qwen3-max" }, + modelCatalog: { "qwen/qwen3-max": { alias: "Qwen" } }, + }, }, { name: "Kimi k2.5", @@ -129,7 +185,14 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ baseUrl: "https://api.moonshot.cn/v1", apiKey: "", api: "openai-completions", - models: [{ id: "kimi-k2.5", name: "Kimi K2.5" }], + models: [ + { + id: "kimi-k2.5", + name: "Kimi K2.5", + contextWindow: 131072, + cost: { input: 0.002, output: 0.006 }, + }, + ], }, category: "cn_official", icon: "kimi", @@ -147,6 +210,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { primary: "kimi/kimi-k2.5" }, + modelCatalog: { "kimi/kimi-k2.5": { alias: "Kimi" } }, + }, }, { name: "MiniMax", @@ -156,7 +223,14 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ baseUrl: "https://api.minimaxi.com/v1", apiKey: "", api: "openai-completions", - models: [{ id: "MiniMax-M2.1", name: "MiniMax M2.1" }], + models: [ + { + id: "MiniMax-M2.1", + name: "MiniMax M2.1", + contextWindow: 200000, + cost: { input: 0.001, output: 0.004 }, + }, + ], }, category: "cn_official", isPartner: true, @@ -174,6 +248,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { primary: "minimax/MiniMax-M2.1" }, + modelCatalog: { "minimax/MiniMax-M2.1": { alias: "MiniMax" } }, + }, }, // ========== Aggregators ========== @@ -186,8 +264,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic", models: [ - { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, - { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-5-20251101", + name: "Claude Opus 4.5", + contextWindow: 200000, + cost: { input: 15, output: 75 }, + }, ], }, category: "aggregator", @@ -200,6 +288,16 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { + primary: "aihubmix/claude-sonnet-4-5-20250929", + fallbacks: ["aihubmix/claude-opus-4-5-20251101"], + }, + modelCatalog: { + "aihubmix/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "aihubmix/claude-opus-4-5-20251101": { alias: "Opus" }, + }, + }, }, { name: "DMXAPI", @@ -210,8 +308,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic", models: [ - { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, - { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-5-20251101", + name: "Claude Opus 4.5", + contextWindow: 200000, + cost: { input: 15, output: 75 }, + }, ], }, category: "aggregator", @@ -224,6 +332,16 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { + primary: "dmxapi/claude-sonnet-4-5-20250929", + fallbacks: ["dmxapi/claude-opus-4-5-20251101"], + }, + modelCatalog: { + "dmxapi/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "dmxapi/claude-opus-4-5-20251101": { alias: "Opus" }, + }, + }, }, { name: "OpenRouter", @@ -234,8 +352,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "openai-completions", models: [ - { id: "anthropic/claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, - { id: "anthropic/claude-opus-4.5", name: "Claude Opus 4.5" }, + { + id: "anthropic/claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "anthropic/claude-opus-4.5", + name: "Claude Opus 4.5", + contextWindow: 200000, + cost: { input: 15, output: 75 }, + }, ], }, category: "aggregator", @@ -248,6 +376,16 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { + primary: "openrouter/anthropic/claude-sonnet-4.5", + fallbacks: ["openrouter/anthropic/claude-opus-4.5"], + }, + modelCatalog: { + "openrouter/anthropic/claude-sonnet-4.5": { alias: "Sonnet" }, + "openrouter/anthropic/claude-opus-4.5": { alias: "Opus" }, + }, + }, }, { name: "ModelScope", @@ -257,7 +395,14 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ baseUrl: "https://api-inference.modelscope.cn/v1", apiKey: "", api: "openai-completions", - models: [{ id: "ZhipuAI/GLM-4.7", name: "GLM-4.7" }], + models: [ + { + id: "ZhipuAI/GLM-4.7", + name: "GLM-4.7", + contextWindow: 128000, + cost: { input: 0.001, output: 0.001 }, + }, + ], }, category: "aggregator", icon: "modelscope", @@ -275,6 +420,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { primary: "modelscope/ZhipuAI/GLM-4.7" }, + modelCatalog: { "modelscope/ZhipuAI/GLM-4.7": { alias: "GLM" } }, + }, }, // ========== Third Party Partners ========== @@ -287,8 +436,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic", models: [ - { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, - { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-5-20251101", + name: "Claude Opus 4.5", + contextWindow: 200000, + cost: { input: 15, output: 75 }, + }, ], }, category: "third_party", @@ -302,6 +461,16 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { + primary: "packycode/claude-sonnet-4-5-20250929", + fallbacks: ["packycode/claude-opus-4-5-20251101"], + }, + modelCatalog: { + "packycode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "packycode/claude-opus-4-5-20251101": { alias: "Opus" }, + }, + }, }, { name: "Cubence", @@ -312,8 +481,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic", models: [ - { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, - { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-5-20251101", + name: "Claude Opus 4.5", + contextWindow: 200000, + cost: { input: 15, output: 75 }, + }, ], }, category: "third_party", @@ -328,6 +507,16 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { + primary: "cubence/claude-sonnet-4-5-20250929", + fallbacks: ["cubence/claude-opus-4-5-20251101"], + }, + modelCatalog: { + "cubence/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "cubence/claude-opus-4-5-20251101": { alias: "Opus" }, + }, + }, }, { name: "AIGoCode", @@ -338,8 +527,18 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ apiKey: "", api: "anthropic", models: [ - { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5" }, - { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }, + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-5-20251101", + name: "Claude Opus 4.5", + contextWindow: 200000, + cost: { input: 15, output: 75 }, + }, ], }, category: "third_party", @@ -354,6 +553,16 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ editorValue: "", }, }, + suggestedDefaults: { + model: { + primary: "aigocode/claude-sonnet-4-5-20250929", + fallbacks: ["aigocode/claude-opus-4-5-20251101"], + }, + modelCatalog: { + "aigocode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "aigocode/claude-opus-4-5-20251101": { alias: "Opus" }, + }, + }, }, // ========== Custom Template ========== diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 137f2cf62..b1ab8e460 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1745,5 +1745,34 @@ "unspecifiedHigh": "Uncategorized high-effort task category for tasks that don't fit other categories with large workload. Defaults to Claude Opus 4.6 max variant.", "writing": "Writing category for documentation, prose and technical writing. Defaults to Gemini 3 Flash fast generation model." } + }, + "openclawConfig": { + "defaultModel": { + "title": "Default Model", + "description": "Configure the default primary model and fallback models for OpenClaw", + "primary": "Primary Model", + "primaryPlaceholder": "e.g., deepseek/deepseek-chat", + "fallbacks": "Fallback Models", + "fallbacksPlaceholder": "e.g., openrouter/anthropic/claude-sonnet-4.5", + "addFallback": "Add Fallback Model", + "saved": "Default model configuration saved", + "saveFailed": "Failed to save default model" + }, + "modelCatalog": { + "title": "Model Catalog", + "description": "Configure model aliases and allowlist", + "modelId": "Model ID", + "modelIdPlaceholder": "e.g., deepseek/deepseek-chat", + "alias": "Alias", + "aliasPlaceholder": "e.g., DeepSeek", + "addEntry": "Add Model", + "removeEntry": "Remove", + "saved": "Model catalog saved", + "saveFailed": "Failed to save model catalog", + "empty": "No model catalog entries", + "emptyHint": "Add models to the catalog to configure aliases" + }, + "suggestedDefaults": "Apply Suggested Defaults", + "suggestedDefaultsHint": "Use this preset's recommended default model configuration" } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index e458b6f87..3412ee1d5 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1726,5 +1726,34 @@ "unspecifiedHigh": "未分類の高作業量タスクカテゴリ。他のカテゴリに該当せず作業量が大きいタスクに適用。デフォルトで Claude Opus 4.6 の max バリアントを使用。", "writing": "ライティングカテゴリ。ドキュメント、散文、技術文書に特化。デフォルトで Gemini 3 Flash 高速生成モデルを使用。" } + }, + "openclawConfig": { + "defaultModel": { + "title": "デフォルトモデル", + "description": "OpenClaw のデフォルトのプライマリモデルとフォールバックモデルを設定します", + "primary": "プライマリモデル", + "primaryPlaceholder": "例: deepseek/deepseek-chat", + "fallbacks": "フォールバックモデル", + "fallbacksPlaceholder": "例: openrouter/anthropic/claude-sonnet-4.5", + "addFallback": "フォールバックモデルを追加", + "saved": "デフォルトモデル設定を保存しました", + "saveFailed": "デフォルトモデルの保存に失敗しました" + }, + "modelCatalog": { + "title": "モデルカタログ", + "description": "モデルのエイリアスと許可リストを設定します", + "modelId": "モデル ID", + "modelIdPlaceholder": "例: deepseek/deepseek-chat", + "alias": "エイリアス", + "aliasPlaceholder": "例: DeepSeek", + "addEntry": "モデルを追加", + "removeEntry": "削除", + "saved": "モデルカタログを保存しました", + "saveFailed": "モデルカタログの保存に失敗しました", + "empty": "モデルカタログエントリがありません", + "emptyHint": "エイリアスを設定するにはカタログにモデルを追加してください" + }, + "suggestedDefaults": "推奨デフォルト設定を適用", + "suggestedDefaultsHint": "このプリセットの推奨デフォルトモデル設定を使用します" } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 82d36b804..1deeb99c0 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1745,5 +1745,34 @@ "unspecifiedHigh": "未归类高工作量任务类别,适用于不适合其他类别且工作量较大的任务,默认使用 Claude Opus 4.6 的最大变体。", "writing": "写作类别,专注于文档、散文和技术写作,默认使用 Gemini 3 Flash 快速生成模型。" } + }, + "openclawConfig": { + "defaultModel": { + "title": "默认模型", + "description": "配置 OpenClaw 的默认主模型和回退模型", + "primary": "主模型", + "primaryPlaceholder": "例如: deepseek/deepseek-chat", + "fallbacks": "回退模型", + "fallbacksPlaceholder": "例如: openrouter/anthropic/claude-sonnet-4.5", + "addFallback": "添加回退模型", + "saved": "默认模型配置已保存", + "saveFailed": "保存默认模型失败" + }, + "modelCatalog": { + "title": "模型目录", + "description": "配置可用模型的别名和允许列表", + "modelId": "模型 ID", + "modelIdPlaceholder": "例如: deepseek/deepseek-chat", + "alias": "别名", + "aliasPlaceholder": "例如: DeepSeek", + "addEntry": "添加模型", + "removeEntry": "移除", + "saved": "模型目录已保存", + "saveFailed": "保存模型目录失败", + "empty": "暂无模型目录配置", + "emptyHint": "添加模型到目录以配置别名" + }, + "suggestedDefaults": "应用建议默认配置", + "suggestedDefaultsHint": "使用此预设推荐的默认模型配置" } } diff --git a/src/lib/api/openclaw.ts b/src/lib/api/openclaw.ts new file mode 100644 index 000000000..cfad98ce0 --- /dev/null +++ b/src/lib/api/openclaw.ts @@ -0,0 +1,59 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { + OpenClawDefaultModel, + OpenClawModelCatalogEntry, +} from "@/types"; + +/** + * OpenClaw agents configuration API + * + * Manages agents.defaults configuration in ~/.openclaw/openclaw.json + */ +export const openclawApi = { + /** + * Get default model configuration (agents.defaults.model) + */ + async getDefaultModel(): Promise { + return await invoke("get_openclaw_default_model"); + }, + + /** + * Set default model configuration (agents.defaults.model) + */ + async setDefaultModel(model: OpenClawDefaultModel): Promise { + return await invoke("set_openclaw_default_model", { model }); + }, + + /** + * Get model catalog/allowlist (agents.defaults.models) + */ + async getModelCatalog(): Promise | null> { + return await invoke("get_openclaw_model_catalog"); + }, + + /** + * Set model catalog/allowlist (agents.defaults.models) + */ + async setModelCatalog( + catalog: Record, + ): Promise { + return await invoke("set_openclaw_model_catalog", { catalog }); + }, + + /** + * Import providers from live config (openclaw.json) to database + */ + async importProvidersFromLive(): Promise { + return await invoke("import_openclaw_providers_from_live"); + }, + + /** + * Get provider IDs that exist in live config (openclaw.json) + */ + async getLiveProviderIds(): Promise { + return await invoke("get_openclaw_live_provider_ids"); + }, +}; diff --git a/src/types.ts b/src/types.ts index dba9bace4..f53ae86b9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -434,6 +434,20 @@ export interface OpenCodeMcpServerSpec { export interface OpenClawModel { id: string; name: string; + alias?: string; + cost?: { input: number; output: number }; + contextWindow?: number; +} + +// OpenClaw 默认模型配置(agents.defaults.model) +export interface OpenClawDefaultModel { + primary: string; + fallbacks?: string[]; +} + +// OpenClaw 模型目录条目(agents.defaults.models 中的值) +export interface OpenClawModelCatalogEntry { + alias?: string; } // OpenClaw 供应商配置(settings_config 结构) From 715e9e89c488f4f1f40eb5595ef2441c8b9aa6de Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 5 Feb 2026 15:25:33 +0800 Subject: [PATCH 25/51] feat(openclaw): add additive mode frontend support - Add getOpenClawLiveProviderIds() API for querying live config - Update ProviderList to query OpenClaw live IDs - Rename isOpenCodeMode to isAdditiveMode (covers OpenCode + OpenClaw) - Update ProviderCard shouldAutoQuery and isActiveProvider logic - Update App.tsx onRemoveFromConfig and invalidateQueries for OpenClaw --- src/App.tsx | 17 +++++++++++++---- src/components/providers/ProviderActions.tsx | 15 ++++++++++----- src/components/providers/ProviderCard.tsx | 12 ++++++++++-- src/components/providers/ProviderList.tsx | 19 ++++++++++++++++--- src/lib/api/providers.ts | 8 ++++++++ 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 713fc8e1e..febd4fa34 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -425,10 +425,19 @@ function App() { const { provider, action } = confirmAction; if (action === "remove") { + // Remove from live config only (for additive mode apps like OpenCode/OpenClaw) + // Does NOT delete from database - provider remains in the list await providersApi.removeFromLiveConfig(provider.id, activeApp); - await queryClient.invalidateQueries({ - queryKey: ["opencodeLiveProviderIds"], - }); + // Invalidate queries to refresh the isInConfig state + if (activeApp === "opencode") { + await queryClient.invalidateQueries({ + queryKey: ["opencodeLiveProviderIds"], + }); + } else if (activeApp === "openclaw") { + await queryClient.invalidateQueries({ + queryKey: ["openclawLiveProviderIds"], + }); + } toast.success( t("notifications.removeFromConfigSuccess", { defaultValue: "已从配置移除", @@ -642,7 +651,7 @@ function App() { setConfirmAction({ provider, action: "delete" }) } onRemoveFromConfig={ - activeApp === "opencode" + activeApp === "opencode" || activeApp === "openclaw" ? (provider) => setConfirmAction({ provider, action: "remove" }) : undefined diff --git a/src/components/providers/ProviderActions.tsx b/src/components/providers/ProviderActions.tsx index e1184fefc..b54c04a4a 100644 --- a/src/components/providers/ProviderActions.tsx +++ b/src/components/providers/ProviderActions.tsx @@ -62,10 +62,13 @@ export function ProviderActions({ const { t } = useTranslation(); const iconButtonClass = "h-8 w-8 p-1"; - const isOpenCodeMode = appId === "opencode" && !isOmo; + // 累加模式应用(OpenCode 非 OMO 和 OpenClaw) + const isAdditiveMode = + (appId === "opencode" && !isOmo) || appId === "openclaw"; + // 故障转移模式下的按钮逻辑(累加模式和 OMO 应用不支持故障转移) const isFailoverMode = - !isOpenCodeMode && !isOmo && isAutoFailoverEnabled && onToggleFailover; + !isAdditiveMode && !isOmo && isAutoFailoverEnabled && onToggleFailover; const handleMainButtonClick = () => { if (isOmo) { @@ -74,7 +77,8 @@ export function ProviderActions({ } else { onSwitch(); } - } else if (isOpenCodeMode) { + } else if (isAdditiveMode) { + // 累加模式:切换配置状态(添加/移除) if (isInConfig) { if (onRemoveFromConfig) { onRemoveFromConfig(); @@ -112,7 +116,8 @@ export function ProviderActions({ }; } - if (isOpenCodeMode) { + // 累加模式(OpenCode 非 OMO / OpenClaw) + if (isAdditiveMode) { if (isInConfig) { return { disabled: false, @@ -180,7 +185,7 @@ export function ProviderActions({ const canDelete = isOmo ? !(isLastOmo && isCurrent) - : isOpenCodeMode + : isAdditiveMode ? true : !isCurrent; diff --git a/src/components/providers/ProviderCard.tsx b/src/components/providers/ProviderCard.tsx index 67310b45b..2a3a2946b 100644 --- a/src/components/providers/ProviderCard.tsx +++ b/src/components/providers/ProviderCard.tsx @@ -133,7 +133,10 @@ export function ProviderCard({ const usageEnabled = provider.meta?.usage_script?.enabled ?? false; - const shouldAutoQuery = appId === "opencode" ? isInConfig : isCurrent; + // 获取用量数据以判断是否有多套餐 + // 累加模式应用(OpenCode/OpenClaw):使用 isInConfig 代替 isCurrent + const shouldAutoQuery = + appId === "opencode" || appId === "openclaw" ? isInConfig : isCurrent; const autoQueryInterval = shouldAutoQuery ? provider.meta?.usage_script?.autoQueryInterval || 0 : 0; @@ -176,9 +179,14 @@ export function ProviderCard({ onOpenWebsite(displayUrl); }; + // 判断是否是"当前使用中"的供应商 + // - OMO 供应商:使用 isCurrent + // - 累加模式应用(OpenCode 非 OMO / OpenClaw):不存在"当前"概念,始终返回 false + // - 故障转移模式:代理实际使用的供应商(activeProviderId) + // - 普通模式:isCurrent const isActiveProvider = isOmo ? isCurrent - : appId === "opencode" + : appId === "opencode" || appId === "openclaw" ? false : isAutoFailoverEnabled ? activeProviderId === provider.id diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 7b1ab9eae..ebc4b7cf2 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -84,12 +84,25 @@ export function ProviderList({ enabled: appId === "opencode", }); + // OpenClaw: 查询 live 配置中的供应商 ID 列表,用于判断 isInConfig + const { data: openclawLiveIds } = useQuery({ + queryKey: ["openclawLiveProviderIds"], + queryFn: () => providersApi.getOpenClawLiveProviderIds(), + enabled: appId === "openclaw", + }); + + // 判断供应商是否已添加到配置(累加模式应用:OpenCode/OpenClaw) const isProviderInConfig = useCallback( (providerId: string): boolean => { - if (appId !== "opencode") return true; // 非 OpenCode 应用始终返回 true - return opencodeLiveIds?.includes(providerId) ?? false; + if (appId === "opencode") { + return opencodeLiveIds?.includes(providerId) ?? false; + } + if (appId === "openclaw") { + return openclawLiveIds?.includes(providerId) ?? false; + } + return true; // 其他应用始终返回 true }, - [appId, opencodeLiveIds], + [appId, opencodeLiveIds, openclawLiveIds], ); const { data: isAutoFailoverEnabled } = useAutoFailoverEnabled(appId); diff --git a/src/lib/api/providers.ts b/src/lib/api/providers.ts index dd154b4e5..d78ed0224 100644 --- a/src/lib/api/providers.ts +++ b/src/lib/api/providers.ts @@ -98,6 +98,14 @@ export const providersApi = { async getOpenCodeLiveProviderIds(): Promise { return await invoke("get_opencode_live_provider_ids"); }, + + /** + * 获取 OpenClaw live 配置中的供应商 ID 列表 + * 用于前端判断供应商是否已添加到 openclaw.json + */ + async getOpenClawLiveProviderIds(): Promise { + return await invoke("get_openclaw_live_provider_ids"); + }, }; // ============================================================================ From 512f22e83a224f736ae972be0c377d2b702e16fe Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 5 Feb 2026 15:25:57 +0800 Subject: [PATCH 26/51] feat(openclaw): add provider form fields and UI components - Add OpenClawFormFields component for provider configuration - Add Collapsible UI component (radix-ui dependency) - Update ProviderForm with OpenClaw-specific handlers and preset logic - Extend OpenClawModel type with reasoning, input, maxTokens fields - Exclude OpenClaw from universal provider tab in AddProviderDialog --- package.json | 1 + pnpm-lock.yaml | 3 + .../providers/AddProviderDialog.tsx | 8 +- .../providers/forms/OpenClawFormFields.tsx | 452 ++++++++++++++++++ .../providers/forms/ProviderForm.tsx | 243 +++++++++- src/components/ui/collapsible.tsx | 9 + src/types.ts | 10 +- 7 files changed, 722 insertions(+), 4 deletions(-) create mode 100644 src/components/providers/forms/OpenClawFormFields.tsx create mode 100644 src/components/ui/collapsible.tsx diff --git a/package.json b/package.json index 147441196..62c1c7177 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@lobehub/icons-static-svg": "^1.73.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d82cfdc38..6c791d5e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.3.3 version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/src/components/providers/AddProviderDialog.tsx b/src/components/providers/AddProviderDialog.tsx index 32c17f5dd..71974e7c0 100644 --- a/src/components/providers/AddProviderDialog.tsx +++ b/src/components/providers/AddProviderDialog.tsx @@ -35,7 +35,8 @@ export function AddProviderDialog({ onSubmit, }: AddProviderDialogProps) { const { t } = useTranslation(); - const showUniversalTab = appId !== "opencode"; + // OpenCode and OpenClaw don't support universal providers + const showUniversalTab = appId !== "opencode" && appId !== "openclaw"; const [activeTab, setActiveTab] = useState<"app-specific" | "universal">( "app-specific", ); @@ -185,6 +186,11 @@ export function AddProviderDialog({ if (options?.baseURL) { addUrl(options.baseURL); } + } else if (appId === "openclaw") { + // OpenClaw uses baseUrl directly + if (parsedConfig.baseUrl) { + addUrl(parsedConfig.baseUrl as string); + } } const urls = Array.from(urlSet); diff --git a/src/components/providers/forms/OpenClawFormFields.tsx b/src/components/providers/forms/OpenClawFormFields.tsx new file mode 100644 index 000000000..7db6ef7df --- /dev/null +++ b/src/components/providers/forms/OpenClawFormFields.tsx @@ -0,0 +1,452 @@ +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import { FormLabel } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react"; +import { ApiKeySection } from "./shared"; +import { openclawApiProtocols } from "@/config/openclawProviderPresets"; +import type { ProviderCategory, OpenClawModel } from "@/types"; + +interface OpenClawFormFieldsProps { + // Base URL + baseUrl: string; + onBaseUrlChange: (value: string) => void; + + // API Key + apiKey: string; + onApiKeyChange: (value: string) => void; + category?: ProviderCategory; + shouldShowApiKeyLink: boolean; + websiteUrl: string; + isPartner?: boolean; + partnerPromotionKey?: string; + + // API Protocol + api: string; + onApiChange: (value: string) => void; + + // Models + models: OpenClawModel[]; + onModelsChange: (models: OpenClawModel[]) => void; +} + +export function OpenClawFormFields({ + baseUrl, + onBaseUrlChange, + apiKey, + onApiKeyChange, + category, + shouldShowApiKeyLink, + websiteUrl, + isPartner, + partnerPromotionKey, + api, + onApiChange, + models, + onModelsChange, +}: OpenClawFormFieldsProps) { + const { t } = useTranslation(); + const [expandedModels, setExpandedModels] = useState>( + {} + ); + + // Toggle advanced section for a model + const toggleModelAdvanced = (index: number) => { + setExpandedModels((prev) => ({ ...prev, [index]: !prev[index] })); + }; + + // Add a new model entry + const handleAddModel = () => { + onModelsChange([ + ...models, + { + id: "", + name: "", + contextWindow: undefined, + maxTokens: undefined, + cost: undefined, + }, + ]); + }; + + // Remove a model entry + const handleRemoveModel = (index: number) => { + const newModels = [...models]; + newModels.splice(index, 1); + onModelsChange(newModels); + // Clean up expanded state + setExpandedModels((prev) => { + const updated = { ...prev }; + delete updated[index]; + return updated; + }); + }; + + // Update model field + const handleModelChange = ( + index: number, + field: keyof OpenClawModel, + value: unknown + ) => { + const newModels = [...models]; + newModels[index] = { ...newModels[index], [field]: value }; + onModelsChange(newModels); + }; + + // Update model cost + const handleCostChange = ( + index: number, + costField: "input" | "output" | "cacheRead" | "cacheWrite", + value: string + ) => { + const newModels = [...models]; + const numValue = parseFloat(value); + const currentCost = newModels[index].cost || { input: 0, output: 0 }; + newModels[index] = { + ...newModels[index], + cost: { + ...currentCost, + [costField]: isNaN(numValue) ? undefined : numValue, + }, + }; + onModelsChange(newModels); + }; + + return ( + <> + {/* API Protocol Selector */} +
+ + {t("openclaw.apiProtocol", { + defaultValue: "API 协议", + })} + + +

+ {t("openclaw.apiProtocolHint", { + defaultValue: + "选择与供应商 API 兼容的协议类型。大多数供应商使用 OpenAI Completions 格式。", + })} +

+
+ + {/* Base URL */} +
+ + {t("openclaw.baseUrl", { defaultValue: "API 端点" })} + + onBaseUrlChange(e.target.value)} + placeholder="https://api.example.com/v1" + /> +

+ {t("openclaw.baseUrlHint", { + defaultValue: "供应商的 API 端点地址。", + })} +

+
+ + {/* API Key */} + + + {/* Models Editor */} +
+
+ + {t("openclaw.models", { defaultValue: "模型列表" })} + + +
+ + {models.length === 0 ? ( +

+ {t("openclaw.noModels", { + defaultValue: "暂无模型配置。点击添加模型来配置可用模型。", + })} +

+ ) : ( +
+ {models.map((model, index) => ( +
+ {/* Model ID and Name row */} +
+
+ + + handleModelChange(index, "id", e.target.value) + } + placeholder={t("openclaw.modelIdPlaceholder", { + defaultValue: "claude-3-sonnet", + })} + /> +
+
+ + + handleModelChange(index, "name", e.target.value) + } + placeholder={t("openclaw.modelNamePlaceholder", { + defaultValue: "Claude 3 Sonnet", + })} + /> +
+ +
+ + {/* Context Window, Max Tokens and Reasoning row */} +
+
+ + + handleModelChange( + index, + "contextWindow", + e.target.value ? parseInt(e.target.value) : undefined + ) + } + placeholder="200000" + /> +
+
+ + + handleModelChange( + index, + "maxTokens", + e.target.value ? parseInt(e.target.value) : undefined + ) + } + placeholder="32000" + /> +
+
+ +
+ + handleModelChange(index, "reasoning", checked) + } + /> + + {model.reasoning + ? t("openclaw.reasoningOn", { defaultValue: "启用" }) + : t("openclaw.reasoningOff", { defaultValue: "关闭" })} + +
+
+ {/* Spacer for alignment with delete button */} +
+
+ + {/* Basic Cost row */} +
+
+ + + handleCostChange(index, "input", e.target.value) + } + placeholder="3" + /> +
+
+ + + handleCostChange(index, "output", e.target.value) + } + placeholder="15" + /> +
+ {/* Spacer for alignment */} +
+
+
+ + {/* Advanced Options (Collapsible) */} + toggleModelAdvanced(index)} + > + + + + +
+
+ + + handleCostChange(index, "cacheRead", e.target.value) + } + placeholder="0.3" + /> +
+
+ + + handleCostChange( + index, + "cacheWrite", + e.target.value + ) + } + placeholder="3.75" + /> +
+ {/* Spacer for alignment */} +
+
+
+

+ {t("openclaw.cacheCostHint", { + defaultValue: + "缓存价格用于计算 Prompt Caching 的成本。如不使用缓存可留空。", + })} +

+ + +
+ ))} +
+ )} + +

+ {t("openclaw.modelsHint", { + defaultValue: + "配置该供应商支持的模型。模型 ID 用于 API 调用,显示名称用于界面展示。", + })} +

+
+ + ); +} diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 381a7d68c..347492506 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -34,7 +34,13 @@ import { OPENCODE_PRESET_MODEL_VARIANTS, type OpenCodeProviderPreset, } from "@/config/opencodeProviderPresets"; +import { + openclawProviderPresets, + type OpenClawProviderPreset, +} from "@/config/openclawProviderPresets"; import { OpenCodeFormFields } from "./OpenCodeFormFields"; +import { OpenClawFormFields } from "./OpenClawFormFields"; +import type { OpenCodeModel, OpenClawModel } from "@/types"; import type { UniversalProviderPreset } from "@/config/universalProviderPresets"; import { applyTemplateValues } from "@/utils/providerConfigUtils"; import { mergeProviderMeta } from "@/utils/providerMetaUtils"; @@ -172,13 +178,25 @@ function toOpencodeExtraOptions( return extra; } +const OPENCLAW_DEFAULT_CONFIG = JSON.stringify( + { + baseUrl: "", + apiKey: "", + api: "openai-completions", + models: [], + }, + null, + 2, +); + type PresetEntry = { id: string; preset: | ProviderPreset | CodexProviderPreset | GeminiProviderPreset - | OpenCodeProviderPreset; + | OpenCodeProviderPreset + | OpenClawProviderPreset; }; interface ProviderFormProps { @@ -337,7 +355,9 @@ export function ProviderForm({ ? GEMINI_DEFAULT_CONFIG : appId === "opencode" ? OPENCODE_DEFAULT_CONFIG - : CLAUDE_DEFAULT_CONFIG, + : appId === "openclaw" + ? OPENCLAW_DEFAULT_CONFIG + : CLAUDE_DEFAULT_CONFIG, icon: initialData?.icon ?? "", iconColor: initialData?.iconColor ?? "", }), @@ -464,6 +484,11 @@ export function ProviderForm({ id: `opencode-${index}`, preset, })); + } else if (appId === "openclaw") { + return openclawProviderPresets.map((preset, index) => ({ + id: `openclaw-${index}`, + preset, + })); } return providerPresets.map((preset, index) => ({ id: `claude-${index}`, @@ -987,6 +1012,128 @@ export function ProviderForm({ setUseOmoCommonConfig(useCommonConfig); }, []); + // OpenClaw 配置状态 + const [openclawBaseUrl, setOpenclawBaseUrl] = useState(() => { + if (appId !== "openclaw") return ""; + try { + const config = JSON.parse( + initialData?.settingsConfig + ? JSON.stringify(initialData.settingsConfig) + : OPENCLAW_DEFAULT_CONFIG, + ); + return config.baseUrl || ""; + } catch { + return ""; + } + }); + + const [openclawApiKey, setOpenclawApiKey] = useState(() => { + if (appId !== "openclaw") return ""; + try { + const config = JSON.parse( + initialData?.settingsConfig + ? JSON.stringify(initialData.settingsConfig) + : OPENCLAW_DEFAULT_CONFIG, + ); + return config.apiKey || ""; + } catch { + return ""; + } + }); + + const [openclawApi, setOpenclawApi] = useState(() => { + if (appId !== "openclaw") return "openai-completions"; + try { + const config = JSON.parse( + initialData?.settingsConfig + ? JSON.stringify(initialData.settingsConfig) + : OPENCLAW_DEFAULT_CONFIG, + ); + return config.api || "openai-completions"; + } catch { + return "openai-completions"; + } + }); + + const [openclawModels, setOpenclawModels] = useState(() => { + if (appId !== "openclaw") return []; + try { + const config = JSON.parse( + initialData?.settingsConfig + ? JSON.stringify(initialData.settingsConfig) + : OPENCLAW_DEFAULT_CONFIG, + ); + return config.models || []; + } catch { + return []; + } + }); + + // OpenClaw handlers - sync state to form + const handleOpenclawBaseUrlChange = useCallback( + (baseUrl: string) => { + setOpenclawBaseUrl(baseUrl); + try { + const config = JSON.parse( + form.getValues("settingsConfig") || OPENCLAW_DEFAULT_CONFIG, + ); + config.baseUrl = baseUrl.trim().replace(/\/+$/, ""); + form.setValue("settingsConfig", JSON.stringify(config, null, 2)); + } catch { + // ignore + } + }, + [form], + ); + + const handleOpenclawApiKeyChange = useCallback( + (apiKey: string) => { + setOpenclawApiKey(apiKey); + try { + const config = JSON.parse( + form.getValues("settingsConfig") || OPENCLAW_DEFAULT_CONFIG, + ); + config.apiKey = apiKey; + form.setValue("settingsConfig", JSON.stringify(config, null, 2)); + } catch { + // ignore + } + }, + [form], + ); + + const handleOpenclawApiChange = useCallback( + (api: string) => { + setOpenclawApi(api); + try { + const config = JSON.parse( + form.getValues("settingsConfig") || OPENCLAW_DEFAULT_CONFIG, + ); + config.api = api; + form.setValue("settingsConfig", JSON.stringify(config, null, 2)); + } catch { + // ignore + } + }, + [form], + ); + + const handleOpenclawModelsChange = useCallback( + (models: OpenClawModel[]) => { + setOpenclawModels(models); + try { + const config = JSON.parse( + form.getValues("settingsConfig") || OPENCLAW_DEFAULT_CONFIG, + ); + config.models = models; + form.setValue("settingsConfig", JSON.stringify(config, null, 2)); + } catch { + // ignore + } + }, + [form], + ); + const updateOpencodeSettings = useCallback( (updater: (config: Record) => void) => { try { @@ -1399,6 +1546,21 @@ export function ProviderForm({ formWebsiteUrl: form.watch("websiteUrl") || "", }); + // 使用 API Key 链接 hook (OpenClaw) + const { + shouldShowApiKeyLink: shouldShowOpenclawApiKeyLink, + websiteUrl: openclawWebsiteUrl, + isPartner: isOpenclawPartner, + partnerPromotionKey: openclawPartnerPromotionKey, + } = useApiKeyLink({ + appId: "openclaw", + category, + selectedPresetId, + presetEntries, + formWebsiteUrl: form.watch("websiteUrl") || "", + }); + + // 使用端点测速候选 hook const speedTestEndpoints = useSpeedTestEndpoints({ appId, selectedPresetId, @@ -1430,6 +1592,13 @@ export function ProviderForm({ setOpencodeExtraOptions({}); resetOmoDraftState(); } + // OpenClaw 自定义模式:重置为空配置 + if (appId === "openclaw") { + setOpenclawBaseUrl(""); + setOpenclawApiKey(""); + setOpenclawApi("openai-completions"); + setOpenclawModels([]); + } return; } @@ -1513,6 +1682,28 @@ export function ProviderForm({ return; } + // OpenClaw preset handling + if (appId === "openclaw") { + const preset = entry.preset as OpenClawProviderPreset; + const config = preset.settingsConfig; + + // Update OpenClaw-specific states + setOpenclawBaseUrl(config.baseUrl || ""); + setOpenclawApiKey(config.apiKey || ""); + setOpenclawApi(config.api || "openai-completions"); + setOpenclawModels(config.models || []); + + // Update form fields + form.reset({ + name: preset.name, + websiteUrl: preset.websiteUrl ?? "", + settingsConfig: JSON.stringify(config, null, 2), + icon: preset.icon ?? "", + iconColor: preset.iconColor ?? "", + }); + return; + } + const preset = entry.preset as ProviderPreset; const config = applyTemplateValues( preset.settingsConfig, @@ -1752,6 +1943,26 @@ export function ProviderForm({ /> )} + {/* OpenClaw 专属字段 */} + {appId === "openclaw" && ( + + )} + + {/* 配置编辑器:Codex、Claude、Gemini 分别使用不同的编辑器 */} {appId === "codex" ? ( <> {settingsConfigErrorField} + ) : appId === "openclaw" ? ( + <> +
+ + form.setValue("settingsConfig", config)} + placeholder={`{ + "baseUrl": "https://api.example.com/v1", + "apiKey": "your-api-key-here", + "api": "openai-completions", + "models": [] +}`} + rows={14} + showValidation={true} + language="json" + /> +
+ ( + + + + )} + /> + ) : ( <> Date: Thu, 5 Feb 2026 15:26:12 +0800 Subject: [PATCH 27/51] fix(openclaw): update API protocol values in provider presets Update anthropic -> anthropic-messages to match OpenClaw gateway config --- src/config/openclawProviderPresets.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index 3ef6e8796..85687b005 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -43,11 +43,14 @@ export interface OpenClawProviderPreset { /** * OpenClaw API protocol options + * @see https://github.com/openclaw/openclaw/blob/main/docs/gateway/configuration.md */ export const openclawApiProtocols = [ { value: "openai-completions", label: "OpenAI Completions" }, - { value: "anthropic", label: "Anthropic" }, + { value: "openai-responses", label: "OpenAI Responses" }, + { value: "anthropic-messages", label: "Anthropic Messages" }, { value: "google-generative-ai", label: "Google Generative AI" }, + { value: "bedrock-converse-stream", label: "AWS Bedrock" }, ] as const; /** @@ -262,7 +265,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ settingsConfig: { baseUrl: "https://aihubmix.com/v1", apiKey: "", - api: "anthropic", + api: "anthropic-messages", models: [ { id: "claude-sonnet-4-5-20250929", @@ -306,7 +309,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ settingsConfig: { baseUrl: "https://www.dmxapi.cn/v1", apiKey: "", - api: "anthropic", + api: "anthropic-messages", models: [ { id: "claude-sonnet-4-5-20250929", @@ -434,7 +437,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ settingsConfig: { baseUrl: "https://www.packyapi.com/v1", apiKey: "", - api: "anthropic", + api: "anthropic-messages", models: [ { id: "claude-sonnet-4-5-20250929", @@ -479,7 +482,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ settingsConfig: { baseUrl: "https://api.cubence.com/v1", apiKey: "", - api: "anthropic", + api: "anthropic-messages", models: [ { id: "claude-sonnet-4-5-20250929", @@ -525,7 +528,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ settingsConfig: { baseUrl: "https://api.aigocode.com/v1", apiKey: "", - api: "anthropic", + api: "anthropic-messages", models: [ { id: "claude-sonnet-4-5-20250929", From 392344e1e9ab788a6e6b6337fc23ef95efd4842c Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 5 Feb 2026 16:25:57 +0800 Subject: [PATCH 28/51] fix(openclaw): prevent creating default provider on first launch Add additive mode guard in import_default_config() to skip OpenCode and OpenClaw apps, which should use their dedicated import functions. --- src-tauri/src/services/provider/live.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index b9997bb33..e7f7500d3 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -402,6 +402,12 @@ pub fn read_live_settings(app_type: AppType) -> Result { /// Returns `Ok(true)` if a provider was actually imported, /// `Ok(false)` if skipped (providers already exist for this app). pub fn import_default_config(state: &AppState, app_type: AppType) -> Result { + // Additive mode apps (OpenCode, OpenClaw) should use their dedicated + // import_xxx_providers_from_live functions, not this generic default config import + if app_type.is_additive_mode() { + return Ok(false); + } + { let providers = state.db.get_all_providers(app_type.as_str())?; if !providers.is_empty() { From 227f08e910d3bd8c045d2156335ee416e189fcb0 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 5 Feb 2026 21:29:24 +0800 Subject: [PATCH 29/51] fix(openclaw): pass providerKey in AddProviderDialog submit handler The providerKey was only being passed for opencode, causing openclaw provider creation to fail with "Provider key is required" error. --- src/components/providers/AddProviderDialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/providers/AddProviderDialog.tsx b/src/components/providers/AddProviderDialog.tsx index 71974e7c0..5bd4d386d 100644 --- a/src/components/providers/AddProviderDialog.tsx +++ b/src/components/providers/AddProviderDialog.tsx @@ -94,7 +94,8 @@ export function AddProviderDialog({ ...(values.meta ? { meta: values.meta } : {}), }; - if (appId === "opencode" && values.providerKey) { + // OpenCode/OpenClaw: pass providerKey for ID generation + if ((appId === "opencode" || appId === "openclaw") && values.providerKey) { providerData.providerKey = values.providerKey; } From 7b2cf6681281ff7e0e1cb705ba530294160819ab Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 5 Feb 2026 21:42:54 +0800 Subject: [PATCH 30/51] feat(openclaw): add providerKey input field with validation - Add providerKey input field in ProviderForm for OpenClaw - Implement duplicate key detection by querying existing providers - Add format validation (lowercase letters, numbers, hyphens only) - Disable field in edit mode (key cannot be changed after creation) - Update mutations.ts to support OpenClaw providerKey - Add i18n translations for zh/en/ja --- .../providers/forms/ProviderForm.tsx | 90 +++++++++++++++++++ src/i18n/locales/en.json | 8 ++ src/i18n/locales/ja.json | 8 ++ src/i18n/locales/zh.json | 8 ++ src/lib/query/mutations.ts | 4 +- 5 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 347492506..98a667144 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -871,6 +871,24 @@ export function ProviderForm({ return providerId || ""; }); + // OpenClaw: query existing providers for duplicate key checking + const { data: openclawProvidersData } = useProvidersQuery("openclaw"); + const existingOpenclawKeys = useMemo(() => { + if (!openclawProvidersData?.providers) return []; + // Exclude current provider ID when in edit mode + return Object.keys(openclawProvidersData.providers).filter( + (k) => k !== providerId, + ); + }, [openclawProvidersData?.providers, providerId]); + + // OpenClaw Provider Key state + const [openclawProviderKey, setOpenclawProviderKey] = useState(() => { + if (appId !== "openclaw") return ""; + // In edit mode, use the existing provider ID as the key + return providerId || ""; + }); + + // OpenCode 配置状态 const [opencodeNpm, setOpencodeNpm] = useState(() => { if (appId !== "opencode") return OPENCODE_DEFAULT_NPM; return initialOpencodeConfig?.npm || OPENCODE_DEFAULT_NPM; @@ -1261,6 +1279,24 @@ export function ProviderForm({ } } + // OpenClaw: validate provider key + if (appId === "openclaw") { + const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; + if (!openclawProviderKey.trim()) { + toast.error(t("openclaw.providerKeyRequired")); + return; + } + if (!keyPattern.test(openclawProviderKey)) { + toast.error(t("openclaw.providerKeyInvalid")); + return; + } + if (!isEditMode && existingOpenclawKeys.includes(openclawProviderKey)) { + toast.error(t("openclaw.providerKeyDuplicate")); + return; + } + } + + // 非官方供应商必填校验:端点和 API Key if (category !== "official") { if (appId === "claude") { if (!baseUrl.trim()) { @@ -1394,6 +1430,8 @@ export function ProviderForm({ } else { payload.providerKey = opencodeProviderKey; } + } else if (appId === "openclaw") { + payload.providerKey = openclawProviderKey; } if (category === "omo" && !payload.presetCategory) { @@ -1594,6 +1632,7 @@ export function ProviderForm({ } // OpenClaw 自定义模式:重置为空配置 if (appId === "openclaw") { + setOpenclawProviderKey(""); setOpenclawBaseUrl(""); setOpenclawApiKey(""); setOpenclawApi("openai-completions"); @@ -1687,6 +1726,9 @@ export function ProviderForm({ const preset = entry.preset as OpenClawProviderPreset; const config = preset.settingsConfig; + // Clear provider key (user must enter their own unique key) + setOpenclawProviderKey(""); + // Update OpenClaw-specific states setOpenclawBaseUrl(config.baseUrl || ""); setOpenclawApiKey(config.apiKey || ""); @@ -1808,6 +1850,54 @@ export function ProviderForm({

)}
+ ) : appId === "openclaw" ? ( +
+ + + setOpenclawProviderKey( + e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""), + ) + } + placeholder={t("openclaw.providerKeyPlaceholder")} + disabled={isEditMode} + className={ + (existingOpenclawKeys.includes(openclawProviderKey) && + !isEditMode) || + (openclawProviderKey.trim() !== "" && + !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey)) + ? "border-destructive" + : "" + } + /> + {existingOpenclawKeys.includes(openclawProviderKey) && + !isEditMode && ( +

+ {t("openclaw.providerKeyDuplicate")} +

+ )} + {openclawProviderKey.trim() !== "" && + !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey) && ( +

+ {t("openclaw.providerKeyInvalid")} +

+ )} + {!( + existingOpenclawKeys.includes(openclawProviderKey) && + !isEditMode + ) && + (openclawProviderKey.trim() === "" || + /^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey)) && ( +

+ {t("openclaw.providerKeyHint")} +

+ )} +
) : undefined } /> diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b1ab8e460..2f52a6b49 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -732,6 +732,14 @@ "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" }, + "openclaw": { + "providerKey": "Provider Key", + "providerKeyPlaceholder": "my-provider", + "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." + }, "providerPreset": { "label": "Provider Preset", "custom": "Custom Configuration", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 3412ee1d5..4fe768253 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -732,6 +732,14 @@ "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" }, + "openclaw": { + "providerKey": "プロバイダーキー", + "providerKeyPlaceholder": "my-provider", + "providerKeyHint": "設定ファイル内のユニーク識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用可能。", + "providerKeyRequired": "プロバイダーキーを入力してください", + "providerKeyDuplicate": "このキーは既に使用されています", + "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用可能。" + }, "providerPreset": { "label": "プロバイダータイプ", "custom": "カスタム設定", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 1deeb99c0..7308d55c8 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -732,6 +732,14 @@ "modelOptionKeyPlaceholder": "provider", "modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}" }, + "openclaw": { + "providerKey": "供应商标识", + "providerKeyPlaceholder": "my-provider", + "providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符", + "providerKeyRequired": "请填写供应商标识", + "providerKeyDuplicate": "此标识已被使用,请更换", + "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符" + }, "providerPreset": { "label": "预设供应商", "custom": "自定义配置", diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index 89e3e486b..a05ca8d49 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -16,12 +16,12 @@ export const useAddProviderMutation = (appId: AppId) => { ) => { let id: string; - if (appId === "opencode") { + if (appId === "opencode" || appId === "openclaw") { if (providerInput.category === "omo") { id = `omo-${generateUUID()}`; } else { if (!providerInput.providerKey) { - throw new Error("Provider key is required for OpenCode"); + throw new Error(`Provider key is required for ${appId}`); } id = providerInput.providerKey; } From 46ab15f9f6b85722299cdb7532bc031d2292457a Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 5 Feb 2026 21:43:01 +0800 Subject: [PATCH 31/51] style: apply code formatting --- src/App.tsx | 6 +++++- .../providers/forms/OpenClawFormFields.tsx | 16 +++++++++------- src/components/ui/collapsible.tsx | 10 +++++----- src/lib/api/openclaw.ts | 5 +---- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index febd4fa34..5c237e4c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -597,7 +597,11 @@ function App() { return ( ); case "mcp": diff --git a/src/components/providers/forms/OpenClawFormFields.tsx b/src/components/providers/forms/OpenClawFormFields.tsx index 7db6ef7df..ccf1e17d3 100644 --- a/src/components/providers/forms/OpenClawFormFields.tsx +++ b/src/components/providers/forms/OpenClawFormFields.tsx @@ -61,7 +61,7 @@ export function OpenClawFormFields({ }: OpenClawFormFieldsProps) { const { t } = useTranslation(); const [expandedModels, setExpandedModels] = useState>( - {} + {}, ); // Toggle advanced section for a model @@ -100,7 +100,7 @@ export function OpenClawFormFields({ const handleModelChange = ( index: number, field: keyof OpenClawModel, - value: unknown + value: unknown, ) => { const newModels = [...models]; newModels[index] = { ...newModels[index], [field]: value }; @@ -111,7 +111,7 @@ export function OpenClawFormFields({ const handleCostChange = ( index: number, costField: "input" | "output" | "cacheRead" | "cacheWrite", - value: string + value: string, ) => { const newModels = [...models]; const numValue = parseFloat(value); @@ -275,7 +275,7 @@ export function OpenClawFormFields({ handleModelChange( index, "contextWindow", - e.target.value ? parseInt(e.target.value) : undefined + e.target.value ? parseInt(e.target.value) : undefined, ) } placeholder="200000" @@ -294,7 +294,7 @@ export function OpenClawFormFields({ handleModelChange( index, "maxTokens", - e.target.value ? parseInt(e.target.value) : undefined + e.target.value ? parseInt(e.target.value) : undefined, ) } placeholder="32000" @@ -316,7 +316,9 @@ export function OpenClawFormFields({ {model.reasoning ? t("openclaw.reasoningOn", { defaultValue: "启用" }) - : t("openclaw.reasoningOff", { defaultValue: "关闭" })} + : t("openclaw.reasoningOff", { + defaultValue: "关闭", + })}
@@ -417,7 +419,7 @@ export function OpenClawFormFields({ handleCostChange( index, "cacheWrite", - e.target.value + e.target.value, ) } placeholder="3.75" diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx index a23e7a281..5c28cbcc3 100644 --- a/src/components/ui/collapsible.tsx +++ b/src/components/ui/collapsible.tsx @@ -1,9 +1,9 @@ -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; -const Collapsible = CollapsiblePrimitive.Root +const Collapsible = CollapsiblePrimitive.Root; -const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; -const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; -export { Collapsible, CollapsibleTrigger, CollapsibleContent } +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/src/lib/api/openclaw.ts b/src/lib/api/openclaw.ts index cfad98ce0..251650a22 100644 --- a/src/lib/api/openclaw.ts +++ b/src/lib/api/openclaw.ts @@ -1,8 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; -import type { - OpenClawDefaultModel, - OpenClawModelCatalogEntry, -} from "@/types"; +import type { OpenClawDefaultModel, OpenClawModelCatalogEntry } from "@/types"; /** * OpenClaw agents configuration API From c431a860640e5f268cc3a1204b4f4920c645d358 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 5 Feb 2026 22:58:05 +0800 Subject: [PATCH 32/51] fix(openclaw): update button state and toast message after switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add cache invalidation for openclawLiveProviderIds in useSwitchProviderMutation to ensure button state updates correctly after adding/removing providers. Also fix toast message to show "已添加到配置" for OpenClaw (same as OpenCode). --- src/hooks/useProviderActions.ts | 16 +++++++++------- src/lib/query/mutations.ts | 6 ++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts index ede55f8a2..daf4229b4 100644 --- a/src/hooks/useProviderActions.ts +++ b/src/hooks/useProviderActions.ts @@ -104,13 +104,15 @@ export function useProviderActions(activeApp: AppId) { ); } else { // 普通供应商:显示切换成功 - // OpenCode: show "added to config" message instead of "switched" - const messageKey = - activeApp === "opencode" - ? "notifications.addToConfigSuccess" - : "notifications.switchSuccess"; - const defaultMessage = - activeApp === "opencode" ? "已添加到配置" : "切换成功!"; + // OpenCode/OpenClaw: show "added to config" message instead of "switched" + const isMultiProviderApp = + activeApp === "opencode" || activeApp === "openclaw"; + const messageKey = isMultiProviderApp + ? "notifications.addToConfigSuccess" + : "notifications.switchSuccess"; + const defaultMessage = isMultiProviderApp + ? "已添加到配置" + : "切换成功!"; toast.success(t(messageKey, { defaultValue: defaultMessage }), { closeButton: true, diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index a05ca8d49..c5a263deb 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -174,6 +174,7 @@ export const useSwitchProviderMutation = (appId: AppId) => { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["providers", appId] }); + // OpenCode/OpenClaw: also invalidate live provider IDs cache to update button state if (appId === "opencode") { await queryClient.invalidateQueries({ queryKey: ["opencodeLiveProviderIds"], @@ -182,6 +183,11 @@ export const useSwitchProviderMutation = (appId: AppId) => { queryKey: ["omo", "current-provider-id"], }); } + if (appId === "openclaw") { + await queryClient.invalidateQueries({ + queryKey: ["openclawLiveProviderIds"], + }); + } try { await providersApi.updateTrayMenu(); From 02f9f2e3b8ee40a7bf789660f2827f5bf694f1f3 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 6 Feb 2026 11:28:22 +0800 Subject: [PATCH 33/51] feat(openclaw): register suggested models to allowlist on provider add When adding an OpenClaw provider with suggestedDefaults, automatically merge its model catalog into the allowlist and set the default model if not already configured. --- .../providers/AddProviderDialog.tsx | 22 ++++++-- .../providers/forms/ProviderForm.tsx | 18 ++++++- src/hooks/useProviderActions.ts | 52 +++++++++++++++++-- src/i18n/locales/en.json | 3 +- src/i18n/locales/ja.json | 3 +- src/i18n/locales/zh.json | 3 +- src/lib/api/index.ts | 2 +- 7 files changed, 92 insertions(+), 11 deletions(-) diff --git a/src/components/providers/AddProviderDialog.tsx b/src/components/providers/AddProviderDialog.tsx index 5bd4d386d..0040f4a14 100644 --- a/src/components/providers/AddProviderDialog.tsx +++ b/src/components/providers/AddProviderDialog.tsx @@ -17,6 +17,7 @@ import { UniversalProviderPanel } from "@/components/universal"; import { providerPresets } from "@/config/claudeProviderPresets"; import { codexProviderPresets } from "@/config/codexProviderPresets"; import { geminiProviderPresets } from "@/config/geminiProviderPresets"; +import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets"; import type { UniversalProviderPreset } from "@/config/universalProviderPresets"; interface AddProviderDialogProps { @@ -24,7 +25,10 @@ interface AddProviderDialogProps { onOpenChange: (open: boolean) => void; appId: AppId; onSubmit: ( - provider: Omit & { providerKey?: string }, + provider: Omit & { + providerKey?: string; + suggestedDefaults?: OpenClawSuggestedDefaults; + }, ) => Promise | void; } @@ -83,7 +87,11 @@ export function AddProviderDialog({ unknown >; - const providerData: Omit & { providerKey?: string } = { + // 构造基础提交数据 + const providerData: Omit & { + providerKey?: string; + suggestedDefaults?: OpenClawSuggestedDefaults; + } = { name: values.name.trim(), notes: values.notes?.trim() || undefined, websiteUrl: values.websiteUrl?.trim() || undefined, @@ -95,7 +103,10 @@ export function AddProviderDialog({ }; // OpenCode/OpenClaw: pass providerKey for ID generation - if ((appId === "opencode" || appId === "openclaw") && values.providerKey) { + if ( + (appId === "opencode" || appId === "openclaw") && + values.providerKey + ) { providerData.providerKey = values.providerKey; } @@ -213,6 +224,11 @@ export function AddProviderDialog({ } } + // OpenClaw: pass suggestedDefaults for model registration + if (appId === "openclaw" && values.suggestedDefaults) { + providerData.suggestedDefaults = values.suggestedDefaults; + } + await onSubmit(providerData); onOpenChange(false); }, diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 98a667144..2cfb91af8 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -37,6 +37,7 @@ import { import { openclawProviderPresets, type OpenClawProviderPreset, + type OpenClawSuggestedDefaults, } from "@/config/openclawProviderPresets"; import { OpenCodeFormFields } from "./OpenCodeFormFields"; import { OpenClawFormFields } from "./OpenClawFormFields"; @@ -277,6 +278,7 @@ export function ProviderForm({ category?: ProviderCategory; isPartner?: boolean; partnerPromotionKey?: string; + suggestedDefaults?: OpenClawSuggestedDefaults; } | null>(null); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = @@ -1446,6 +1448,10 @@ export function ProviderForm({ if (activePreset.isPartner) { payload.isPartner = activePreset.isPartner; } + // OpenClaw: 传递预设的 suggestedDefaults 到提交数据 + if (activePreset.suggestedDefaults) { + payload.suggestedDefaults = activePreset.suggestedDefaults; + } } if (!isEditMode && draftCustomEndpoints.length > 0) { @@ -1726,6 +1732,15 @@ export function ProviderForm({ const preset = entry.preset as OpenClawProviderPreset; const config = preset.settingsConfig; + // Update activePreset with suggestedDefaults for OpenClaw + setActivePreset({ + id: value, + category: preset.category, + isPartner: preset.isPartner, + partnerPromotionKey: preset.partnerPromotionKey, + suggestedDefaults: preset.suggestedDefaults, + }); + // Clear provider key (user must enter their own unique key) setOpenclawProviderKey(""); @@ -2206,5 +2221,6 @@ export type ProviderFormValues = ProviderFormData & { presetCategory?: ProviderCategory; isPartner?: boolean; meta?: ProviderMeta; - providerKey?: string; + providerKey?: string; // OpenCode/OpenClaw: user-defined provider key + suggestedDefaults?: OpenClawSuggestedDefaults; // OpenClaw: suggested default model configuration }; diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts index daf4229b4..0f1d9bb91 100644 --- a/src/hooks/useProviderActions.ts +++ b/src/hooks/useProviderActions.ts @@ -2,8 +2,9 @@ import { useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; -import { providersApi, settingsApi, type AppId } from "@/lib/api"; +import { providersApi, settingsApi, openclawApi, type AppId } from "@/lib/api"; import type { Provider, UsageScript } from "@/types"; +import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets"; import { useAddProviderMutation, useUpdateProviderMutation, @@ -54,10 +55,55 @@ export function useProviderActions(activeApp: AppId) { // 添加供应商 const addProvider = useCallback( - async (provider: Omit & { providerKey?: string }) => { + async ( + provider: Omit & { + providerKey?: string; + suggestedDefaults?: OpenClawSuggestedDefaults; + }, + ) => { await addProviderMutation.mutateAsync(provider); + + // OpenClaw: register models to allowlist after adding provider + if (activeApp === "openclaw" && provider.suggestedDefaults) { + const { model, modelCatalog } = provider.suggestedDefaults; + let modelsRegistered = false; + + try { + // 1. Merge model catalog (allowlist) + if (modelCatalog && Object.keys(modelCatalog).length > 0) { + const existingCatalog = (await openclawApi.getModelCatalog()) || {}; + const mergedCatalog = { ...existingCatalog, ...modelCatalog }; + await openclawApi.setModelCatalog(mergedCatalog); + modelsRegistered = true; + } + + // 2. Set default model (only if not already set) + if (model) { + const existingDefault = await openclawApi.getDefaultModel(); + if (!existingDefault?.primary) { + await openclawApi.setDefaultModel(model); + } + } + + // Show success toast if models were registered + if (modelsRegistered) { + toast.success( + t("notifications.openclawModelsRegistered", { + defaultValue: "模型已注册到 /model 列表", + }), + { closeButton: true }, + ); + } + } catch (error) { + // Log warning but don't block main flow - provider config is already saved + console.warn( + "[OpenClaw] Failed to register models to allowlist:", + error, + ); + } + } }, - [addProviderMutation], + [addProviderMutation, activeApp, t], ); // 更新供应商 diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2f52a6b49..8d9a687ed 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -162,7 +162,8 @@ "settingsSaved": "Settings saved", "settingsSaveFailed": "Failed to save settings: {{error}}", "openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled", - "openLinkFailed": "Failed to open link" + "openLinkFailed": "Failed to open link", + "openclawModelsRegistered": "Models have been registered to /model list" }, "confirm": { "deleteProvider": "Delete Provider", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 4fe768253..3f7458300 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -162,7 +162,8 @@ "settingsSaved": "設定を保存しました", "settingsSaveFailed": "設定の保存に失敗しました: {{error}}", "openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です", - "openLinkFailed": "リンクを開けませんでした" + "openLinkFailed": "リンクを開けませんでした", + "openclawModelsRegistered": "モデルが /model リストに登録されました" }, "confirm": { "deleteProvider": "プロバイダーを削除", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 7308d55c8..3d9d9ec54 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -162,7 +162,8 @@ "settingsSaved": "设置已保存", "settingsSaveFailed": "保存设置失败:{{error}}", "openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用", - "openLinkFailed": "链接打开失败" + "openLinkFailed": "链接打开失败", + "openclawModelsRegistered": "模型已注册到 /model 列表" }, "confirm": { "deleteProvider": "删除供应商", diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 8e48d0151..38d6b09e2 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -7,7 +7,7 @@ export { skillsApi } from "./skills"; export { usageApi } from "./usage"; export { vscodeApi } from "./vscode"; export { proxyApi } from "./proxy"; -export { sessionsApi } from "./sessions"; +export { openclawApi } from "./openclaw"; export * as configApi from "./config"; export type { ProviderSwitchEvent } from "./providers"; export type { Prompt } from "./prompts"; From 26a2fc4fe047946dc860e0373e3f8b82eb4b5bb3 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 6 Feb 2026 15:49:40 +0800 Subject: [PATCH 34/51] fix(openclaw,opencode): correct Opus pricing and upgrade to Opus 4.6 - Fix Opus pricing from $15/$75 (Opus 4 price) to $5/$25 in openclaw presets - Upgrade model ID from claude-opus-4-5-20251101 to claude-opus-4-6 - Update all suggestedDefaults references (fallbacks + modelCatalog) - Apply same model upgrade in opencode presets (7 providers) --- src/config/openclawProviderPresets.ts | 60 +++++++++++++-------------- src/config/opencodeProviderPresets.ts | 14 +++---- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index 85687b005..5c0761135 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -274,10 +274,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ cost: { input: 3, output: 15 }, }, { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", + id: "claude-opus-4-6", + name: "Claude Opus 4.6", contextWindow: 200000, - cost: { input: 15, output: 75 }, + cost: { input: 5, output: 25 }, }, ], }, @@ -294,11 +294,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ suggestedDefaults: { model: { primary: "aihubmix/claude-sonnet-4-5-20250929", - fallbacks: ["aihubmix/claude-opus-4-5-20251101"], + fallbacks: ["aihubmix/claude-opus-4-6"], }, modelCatalog: { "aihubmix/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, - "aihubmix/claude-opus-4-5-20251101": { alias: "Opus" }, + "aihubmix/claude-opus-4-6": { alias: "Opus" }, }, }, }, @@ -318,10 +318,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ cost: { input: 3, output: 15 }, }, { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", + id: "claude-opus-4-6", + name: "Claude Opus 4.6", contextWindow: 200000, - cost: { input: 15, output: 75 }, + cost: { input: 5, output: 25 }, }, ], }, @@ -338,11 +338,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ suggestedDefaults: { model: { primary: "dmxapi/claude-sonnet-4-5-20250929", - fallbacks: ["dmxapi/claude-opus-4-5-20251101"], + fallbacks: ["dmxapi/claude-opus-4-6"], }, modelCatalog: { "dmxapi/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, - "dmxapi/claude-opus-4-5-20251101": { alias: "Opus" }, + "dmxapi/claude-opus-4-6": { alias: "Opus" }, }, }, }, @@ -362,10 +362,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ cost: { input: 3, output: 15 }, }, { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", + id: "anthropic/claude-opus-4.6", + name: "Claude Opus 4.6", contextWindow: 200000, - cost: { input: 15, output: 75 }, + cost: { input: 5, output: 25 }, }, ], }, @@ -382,11 +382,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ suggestedDefaults: { model: { primary: "openrouter/anthropic/claude-sonnet-4.5", - fallbacks: ["openrouter/anthropic/claude-opus-4.5"], + fallbacks: ["openrouter/anthropic/claude-opus-4.6"], }, modelCatalog: { "openrouter/anthropic/claude-sonnet-4.5": { alias: "Sonnet" }, - "openrouter/anthropic/claude-opus-4.5": { alias: "Opus" }, + "openrouter/anthropic/claude-opus-4.6": { alias: "Opus" }, }, }, }, @@ -446,10 +446,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ cost: { input: 3, output: 15 }, }, { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", + id: "claude-opus-4-6", + name: "Claude Opus 4.6", contextWindow: 200000, - cost: { input: 15, output: 75 }, + cost: { input: 5, output: 25 }, }, ], }, @@ -467,11 +467,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ suggestedDefaults: { model: { primary: "packycode/claude-sonnet-4-5-20250929", - fallbacks: ["packycode/claude-opus-4-5-20251101"], + fallbacks: ["packycode/claude-opus-4-6"], }, modelCatalog: { "packycode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, - "packycode/claude-opus-4-5-20251101": { alias: "Opus" }, + "packycode/claude-opus-4-6": { alias: "Opus" }, }, }, }, @@ -491,10 +491,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ cost: { input: 3, output: 15 }, }, { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", + id: "claude-opus-4-6", + name: "Claude Opus 4.6", contextWindow: 200000, - cost: { input: 15, output: 75 }, + cost: { input: 5, output: 25 }, }, ], }, @@ -513,11 +513,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ suggestedDefaults: { model: { primary: "cubence/claude-sonnet-4-5-20250929", - fallbacks: ["cubence/claude-opus-4-5-20251101"], + fallbacks: ["cubence/claude-opus-4-6"], }, modelCatalog: { "cubence/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, - "cubence/claude-opus-4-5-20251101": { alias: "Opus" }, + "cubence/claude-opus-4-6": { alias: "Opus" }, }, }, }, @@ -537,10 +537,10 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ cost: { input: 3, output: 15 }, }, { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", + id: "claude-opus-4-6", + name: "Claude Opus 4.6", contextWindow: 200000, - cost: { input: 15, output: 75 }, + cost: { input: 5, output: 25 }, }, ], }, @@ -559,11 +559,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ suggestedDefaults: { model: { primary: "aigocode/claude-sonnet-4-5-20250929", - fallbacks: ["aigocode/claude-opus-4-5-20251101"], + fallbacks: ["aigocode/claude-opus-4-6"], }, modelCatalog: { "aigocode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, - "aigocode/claude-opus-4-5-20251101": { alias: "Opus" }, + "aigocode/claude-opus-4-6": { alias: "Opus" }, }, }, }, diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index c01c1448d..51bfa5503 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -844,7 +844,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, models: { "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, - "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" }, + "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, category: "aggregator", @@ -871,7 +871,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, models: { "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, - "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" }, + "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, category: "aggregator", @@ -898,7 +898,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, models: { "anthropic/claude-sonnet-4.5": { name: "Claude Sonnet 4.5" }, - "anthropic/claude-opus-4.5": { name: "Claude Opus 4.5" }, + "anthropic/claude-opus-4.6": { name: "Claude Opus 4.6" }, }, }, category: "aggregator", @@ -952,7 +952,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, models: { "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, - "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" }, + "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, category: "third_party", @@ -980,7 +980,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, models: { "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, - "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" }, + "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, category: "third_party", @@ -1009,7 +1009,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, models: { "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" }, - "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" }, + "claude-opus-4-6": { name: "Claude Opus 4.6" }, }, }, category: "third_party", @@ -1070,7 +1070,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, models: { "claude-sonnet-4.5": { name: "Claude Sonnet 4.5" }, - "claude-opus-4.5": { name: "Claude Opus 4.5" }, + "claude-opus-4.6": { name: "Claude Opus 4.6" }, }, }, category: "third_party", From f1fbe324d1b2dbe75109b1b6676874017691c5dd Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Feb 2026 09:37:59 +0800 Subject: [PATCH 35/51] feat(openclaw): add default model button and fix sessionsApi export - Add "Enable as Default" button to OpenClaw provider cards - Place enable button before add/remove button for better UX - Disable remove button when provider is set as default - Add sessionsApi re-export from api barrel file to fix white screen - Add i18n keys for default model feature (zh/en/ja) --- src/App.tsx | 4 ++ src/components/providers/ProviderActions.tsx | 33 ++++++++++++- src/components/providers/ProviderCard.tsx | 9 ++++ src/components/providers/ProviderList.tsx | 32 +++++++++++++ src/hooks/useProviderActions.ts | 49 +++++++++++++++++++- src/i18n/locales/en.json | 7 ++- src/i18n/locales/ja.json | 7 ++- src/i18n/locales/zh.json | 7 ++- src/lib/api/index.ts | 1 + src/lib/query/mutations.ts | 3 ++ 10 files changed, 146 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5c237e4c5..a4af76b4a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -198,6 +198,7 @@ function App() { switchProvider, deleteProvider, saveUsageScript, + setAsDefaultModel, } = useProviderActions(activeApp); const disableOmoMutation = useDisableCurrentOmo(); @@ -670,6 +671,9 @@ function App() { activeApp === "claude" ? handleOpenTerminal : undefined } onCreate={() => setIsAddOpen(true)} + onSetAsDefault={ + activeApp === "openclaw" ? setAsDefaultModel : undefined + } /> diff --git a/src/components/providers/ProviderActions.tsx b/src/components/providers/ProviderActions.tsx index b54c04a4a..d53c0a486 100644 --- a/src/components/providers/ProviderActions.tsx +++ b/src/components/providers/ProviderActions.tsx @@ -10,6 +10,7 @@ import { Terminal, TestTube2, Trash2, + Zap, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; @@ -36,6 +37,9 @@ interface ProviderActionsProps { isAutoFailoverEnabled?: boolean; isInFailoverQueue?: boolean; onToggleFailover?: (enabled: boolean) => void; + // OpenClaw: default model + isDefaultModel?: boolean; + onSetAsDefault?: () => void; } export function ProviderActions({ @@ -58,6 +62,9 @@ export function ProviderActions({ isAutoFailoverEnabled = false, isInFailoverQueue = false, onToggleFailover, + // OpenClaw: default model + isDefaultModel = false, + onSetAsDefault, }: ProviderActionsProps) { const { t } = useTranslation(); const iconButtonClass = "h-8 w-8 p-1"; @@ -120,10 +127,12 @@ export function ProviderActions({ if (isAdditiveMode) { if (isInConfig) { return { - disabled: false, + disabled: isDefaultModel === true, variant: "secondary" as const, - className: + className: cn( "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", + isDefaultModel && "opacity-40 cursor-not-allowed", + ), icon: , text: t("provider.removeFromConfig", { defaultValue: "移除" }), }; @@ -191,6 +200,26 @@ export function ProviderActions({ return (
+ {appId === "openclaw" && isInConfig && onSetAsDefault && ( + + )} +
diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index ebc4b7cf2..8c4f635cc 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -19,6 +19,7 @@ import { useQuery } from "@tanstack/react-query"; import type { Provider } from "@/types"; import type { AppId } from "@/lib/api"; import { providersApi } from "@/lib/api/providers"; +import { openclawApi } from "@/lib/api/openclaw"; import { useDragSort } from "@/hooks/useDragSort"; import { ProviderCard } from "@/components/providers/ProviderCard"; import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState"; @@ -51,6 +52,7 @@ interface ProviderListProps { isProxyRunning?: boolean; // 代理服务运行状态 isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管) activeProviderId?: string; // 代理当前实际使用的供应商 ID(用于故障转移模式下标注绿色边框) + onSetAsDefault?: (provider: Provider) => void; // OpenClaw: set as default model } export function ProviderList({ @@ -71,6 +73,7 @@ export function ProviderList({ isProxyRunning = false, isProxyTakeover = false, activeProviderId, + onSetAsDefault, }: ProviderListProps) { const { t } = useTranslation(); const { sortedProviders, sensors, handleDragEnd } = useDragSort( @@ -105,6 +108,22 @@ export function ProviderList({ [appId, opencodeLiveIds, openclawLiveIds], ); + // OpenClaw: query default model to determine which provider is default + const { data: openclawDefaultModel } = useQuery({ + queryKey: ["openclawDefaultModel"], + queryFn: () => openclawApi.getDefaultModel(), + enabled: appId === "openclaw", + }); + + const isProviderDefaultModel = useCallback( + (providerId: string): boolean => { + if (appId !== "openclaw" || !openclawDefaultModel?.primary) return false; + return openclawDefaultModel.primary.startsWith(providerId + "/"); + }, + [appId, openclawDefaultModel], + ); + + // 故障转移相关 const { data: isAutoFailoverEnabled } = useAutoFailoverEnabled(appId); const { data: failoverQueue } = useFailoverQueue(appId); const addToQueue = useAddToFailoverQueue(); @@ -253,6 +272,11 @@ export function ProviderList({ handleToggleFailover(provider.id, enabled) } activeProviderId={activeProviderId} + // OpenClaw: default model + isDefaultModel={isProviderDefaultModel(provider.id)} + onSetAsDefault={ + onSetAsDefault ? () => onSetAsDefault(provider) : undefined + } /> ); })} @@ -365,6 +389,9 @@ interface SortableProviderCardProps { isInFailoverQueue: boolean; onToggleFailover: (enabled: boolean) => void; activeProviderId?: string; + // OpenClaw: default model + isDefaultModel?: boolean; + onSetAsDefault?: () => void; } function SortableProviderCard({ @@ -392,6 +419,8 @@ function SortableProviderCard({ isInFailoverQueue, onToggleFailover, activeProviderId, + isDefaultModel, + onSetAsDefault, }: SortableProviderCardProps) { const { setNodeRef, @@ -441,6 +470,9 @@ function SortableProviderCard({ isInFailoverQueue={isInFailoverQueue} onToggleFailover={onToggleFailover} activeProviderId={activeProviderId} + // OpenClaw: default model + isDefaultModel={isDefaultModel} + onSetAsDefault={onSetAsDefault} />
); diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts index 0f1d9bb91..2f2083df8 100644 --- a/src/hooks/useProviderActions.ts +++ b/src/hooks/useProviderActions.ts @@ -3,7 +3,12 @@ import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { providersApi, settingsApi, openclawApi, type AppId } from "@/lib/api"; -import type { Provider, UsageScript } from "@/types"; +import type { + Provider, + UsageScript, + OpenClawProviderConfig, + OpenClawDefaultModel, +} from "@/types"; import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets"; import { useAddProviderMutation, @@ -218,12 +223,54 @@ export function useProviderActions(activeApp: AppId) { [activeApp, queryClient, t], ); + // Set provider as default model (OpenClaw only) + const setAsDefaultModel = useCallback( + async (provider: Provider) => { + const config = provider.settingsConfig as OpenClawProviderConfig; + if (!config.models || config.models.length === 0) { + toast.error( + t("notifications.openclawNoModels", { + defaultValue: "该供应商没有配置模型", + }), + ); + return; + } + + const model: OpenClawDefaultModel = { + primary: `${provider.id}/${config.models[0].id}`, + fallbacks: config.models.slice(1).map((m) => `${provider.id}/${m.id}`), + }; + + try { + await openclawApi.setDefaultModel(model); + await queryClient.invalidateQueries({ + queryKey: ["openclawDefaultModel"], + }); + toast.success( + t("notifications.openclawDefaultModelSet", { + defaultValue: "已设为默认模型", + }), + { closeButton: true }, + ); + } catch (error) { + const detail = + extractErrorMessage(error) || + t("notifications.openclawDefaultModelSetFailed", { + defaultValue: "设置默认模型失败", + }); + toast.error(detail); + } + }, + [queryClient, t], + ); + return { addProvider, updateProvider, switchProvider, deleteProvider, saveUsageScript, + setAsDefaultModel, isLoading: addProviderMutation.isPending || updateProviderMutation.isPending || diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8d9a687ed..d9df8cbe6 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -88,6 +88,8 @@ "addOpenCodeProvider": "Add OpenCode Provider", "addToConfig": "Add", "removeFromConfig": "Remove", + "setAsDefault": "Enable", + "isDefault": "Default", "inConfig": "Added", "addProviderHint": "Fill in the information to quickly switch providers in the list.", "editClaudeProvider": "Edit Claude Code Provider", @@ -163,7 +165,10 @@ "settingsSaveFailed": "Failed to save settings: {{error}}", "openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled", "openLinkFailed": "Failed to open link", - "openclawModelsRegistered": "Models have been registered to /model list" + "openclawModelsRegistered": "Models have been registered to /model list", + "openclawDefaultModelSet": "Set as default model", + "openclawDefaultModelSetFailed": "Failed to set default model", + "openclawNoModels": "No models configured" }, "confirm": { "deleteProvider": "Delete Provider", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 3f7458300..02effd72b 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -88,6 +88,8 @@ "addOpenCodeProvider": "OpenCode プロバイダーを追加", "addToConfig": "追加", "removeFromConfig": "削除", + "setAsDefault": "有効化", + "isDefault": "デフォルト", "inConfig": "追加済み", "addProviderHint": "一覧にすばやく切り替えられるよう、ここに情報を入力してください。", "editClaudeProvider": "Claude Code プロバイダーを編集", @@ -163,7 +165,10 @@ "settingsSaveFailed": "設定の保存に失敗しました: {{error}}", "openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です", "openLinkFailed": "リンクを開けませんでした", - "openclawModelsRegistered": "モデルが /model リストに登録されました" + "openclawModelsRegistered": "モデルが /model リストに登録されました", + "openclawDefaultModelSet": "デフォルトモデルに設定しました", + "openclawDefaultModelSetFailed": "デフォルトモデルの設定に失敗しました", + "openclawNoModels": "モデルが設定されていません" }, "confirm": { "deleteProvider": "プロバイダーを削除", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 3d9d9ec54..c4012ac51 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -88,6 +88,8 @@ "addOpenCodeProvider": "添加 OpenCode 供应商", "addToConfig": "添加", "removeFromConfig": "移除", + "setAsDefault": "启用", + "isDefault": "默认", "inConfig": "已添加", "addProviderHint": "填写信息后即可在列表中快速切换供应商。", "editClaudeProvider": "编辑 Claude Code 供应商", @@ -163,7 +165,10 @@ "settingsSaveFailed": "保存设置失败:{{error}}", "openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用", "openLinkFailed": "链接打开失败", - "openclawModelsRegistered": "模型已注册到 /model 列表" + "openclawModelsRegistered": "模型已注册到 /model 列表", + "openclawDefaultModelSet": "已设为默认模型", + "openclawDefaultModelSetFailed": "设置默认模型失败", + "openclawNoModels": "该供应商没有配置模型" }, "confirm": { "deleteProvider": "删除供应商", diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 38d6b09e2..6a797f1ea 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -8,6 +8,7 @@ export { usageApi } from "./usage"; export { vscodeApi } from "./vscode"; export { proxyApi } from "./proxy"; export { openclawApi } from "./openclaw"; +export { sessionsApi } from "./sessions"; export * as configApi from "./config"; export type { ProviderSwitchEvent } from "./providers"; export type { Prompt } from "./prompts"; diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index c5a263deb..da83c204f 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -187,6 +187,9 @@ export const useSwitchProviderMutation = (appId: AppId) => { await queryClient.invalidateQueries({ queryKey: ["openclawLiveProviderIds"], }); + await queryClient.invalidateQueries({ + queryKey: ["openclawDefaultModel"], + }); } try { From a33f8fe9735d3c045b59a36736919393d4d1d451 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Feb 2026 11:05:57 +0800 Subject: [PATCH 36/51] perf: remove unnecessary query cache for Tauri local IPC Tauri IPC calls are in-process (microsecond latency), so the 5-minute staleTime and disabled refetchOnWindowFocus from web app templates are counterproductive. Set staleTime to 0 and enable refetchOnWindowFocus so external config changes are reflected immediately. --- src/lib/query/queryClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/query/queryClient.ts b/src/lib/query/queryClient.ts index 2d3a9e2b8..ed5001759 100644 --- a/src/lib/query/queryClient.ts +++ b/src/lib/query/queryClient.ts @@ -4,8 +4,8 @@ export const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, - refetchOnWindowFocus: false, - staleTime: 1000 * 60 * 5, + refetchOnWindowFocus: true, + staleTime: 0, }, mutations: { retry: false, From 182015264cacdc60230e31233572f2e21c52b874 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Feb 2026 14:49:15 +0800 Subject: [PATCH 37/51] fix(openclaw): add openclaw to all Record usages Expand McpApps interface, APP_IDS, APP_ICON_MAP, enabledCounts initializers, and test mock data to include the openclaw key, resolving TypeScript errors after AppId union was extended. --- src/components/mcp/McpFormModal.tsx | 2 ++ src/components/mcp/UnifiedMcpPanel.tsx | 2 +- src/components/skills/UnifiedSkillsPanel.tsx | 2 +- src/config/appConfig.tsx | 17 +++++++++++++++-- src/types.ts | 1 + tests/msw/state.ts | 8 ++++---- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index 93d12745d..70ab3b03a 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -66,6 +66,7 @@ const McpFormModal: React.FC = ({ codex: boolean; gemini: boolean; opencode: boolean; + openclaw: boolean; }>(() => { if (initialData?.apps) { return { ...initialData.apps }; @@ -75,6 +76,7 @@ const McpFormModal: React.FC = ({ codex: defaultEnabledApps.includes("codex"), gemini: defaultEnabledApps.includes("gemini"), opencode: defaultEnabledApps.includes("opencode"), + openclaw: defaultEnabledApps.includes("openclaw"), }; }); diff --git a/src/components/mcp/UnifiedMcpPanel.tsx b/src/components/mcp/UnifiedMcpPanel.tsx index bab09acd5..57e0cd85f 100644 --- a/src/components/mcp/UnifiedMcpPanel.tsx +++ b/src/components/mcp/UnifiedMcpPanel.tsx @@ -56,7 +56,7 @@ const UnifiedMcpPanel = React.forwardRef< }, [serversMap]); const enabledCounts = useMemo(() => { - const counts = { claude: 0, codex: 0, gemini: 0, opencode: 0 }; + const counts = { claude: 0, codex: 0, gemini: 0, opencode: 0, openclaw: 0 }; serverEntries.forEach(([_, server]) => { for (const app of APP_IDS) { if (server.apps[app]) counts[app]++; diff --git a/src/components/skills/UnifiedSkillsPanel.tsx b/src/components/skills/UnifiedSkillsPanel.tsx index 6d6f62ce2..aa647c70d 100644 --- a/src/components/skills/UnifiedSkillsPanel.tsx +++ b/src/components/skills/UnifiedSkillsPanel.tsx @@ -53,7 +53,7 @@ const UnifiedSkillsPanel = React.forwardRef< const installFromZipMutation = useInstallSkillsFromZip(); const enabledCounts = useMemo(() => { - const counts = { claude: 0, codex: 0, gemini: 0, opencode: 0 }; + const counts = { claude: 0, codex: 0, gemini: 0, opencode: 0, openclaw: 0 }; if (!skills) return counts; skills.forEach((skill) => { for (const app of APP_IDS) { diff --git a/src/config/appConfig.tsx b/src/config/appConfig.tsx index 6ab2b5046..9bb977462 100644 --- a/src/config/appConfig.tsx +++ b/src/config/appConfig.tsx @@ -1,6 +1,11 @@ import React from "react"; import type { AppId } from "@/lib/api/types"; -import { ClaudeIcon, CodexIcon, GeminiIcon } from "@/components/BrandIcons"; +import { + ClaudeIcon, + CodexIcon, + GeminiIcon, + OpenClawIcon, +} from "@/components/BrandIcons"; import { ProviderIcon } from "@/components/ProviderIcon"; export interface AppConfig { @@ -10,7 +15,7 @@ export interface AppConfig { badgeClass: string; } -export const APP_IDS: AppId[] = ["claude", "codex", "gemini", "opencode"]; +export const APP_IDS: AppId[] = ["claude", "codex", "gemini", "opencode", "openclaw"]; export const APP_ICON_MAP: Record = { claude: { @@ -52,4 +57,12 @@ export const APP_ICON_MAP: Record = { badgeClass: "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-500/20 border-0 gap-1.5", }, + openclaw: { + label: "OpenClaw", + icon: , + activeClass: + "bg-rose-500/10 ring-1 ring-rose-500/20 hover:bg-rose-500/20 text-rose-600 dark:text-rose-400", + badgeClass: + "bg-rose-500/10 text-rose-700 dark:text-rose-300 hover:bg-rose-500/20 border-0 gap-1.5", + }, }; diff --git a/src/types.ts b/src/types.ts index 4138cd766..45b22d620 100644 --- a/src/types.ts +++ b/src/types.ts @@ -288,6 +288,7 @@ export interface McpApps { codex: boolean; gemini: boolean; opencode: boolean; + openclaw: boolean; } // MCP 服务器条目(v3.7.0 统一结构) diff --git a/tests/msw/state.ts b/tests/msw/state.ts index bb08e101f..2db234840 100644 --- a/tests/msw/state.ts +++ b/tests/msw/state.ts @@ -86,7 +86,7 @@ let mcpConfigs: McpConfigState = { id: "sample", name: "Sample Claude Server", enabled: true, - apps: { claude: true, codex: false, gemini: false, opencode: false }, + apps: { claude: true, codex: false, gemini: false, opencode: false, openclaw: false }, server: { type: "stdio", command: "claude-server", @@ -98,7 +98,7 @@ let mcpConfigs: McpConfigState = { id: "httpServer", name: "HTTP Codex Server", enabled: false, - apps: { claude: false, codex: true, gemini: false, opencode: false }, + apps: { claude: false, codex: true, gemini: false, opencode: false, openclaw: false }, server: { type: "http", url: "http://localhost:3000", @@ -131,7 +131,7 @@ export const resetProviderState = () => { id: "sample", name: "Sample Claude Server", enabled: true, - apps: { claude: true, codex: false, gemini: false, opencode: false }, + apps: { claude: true, codex: false, gemini: false, opencode: false, openclaw: false }, server: { type: "stdio", command: "claude-server", @@ -143,7 +143,7 @@ export const resetProviderState = () => { id: "httpServer", name: "HTTP Codex Server", enabled: false, - apps: { claude: false, codex: true, gemini: false, opencode: false }, + apps: { claude: false, codex: true, gemini: false, opencode: false, openclaw: false }, server: { type: "http", url: "http://localhost:3000", From 705cc8a5af9afc7ce5b45100fca92e934dc561a5 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Feb 2026 21:42:36 +0800 Subject: [PATCH 38/51] feat(openclaw): add Workspace Files panel for managing bootstrap md files Add a dedicated panel to read/write OpenClaw's 6 workspace bootstrap files (AGENTS.md, SOUL.md, USER.md, IDENTITY.md, TOOLS.md, MEMORY.md) directly from ~/.openclaw/workspace/, with a whitelist-secured backend and Markdown editor UI. Also fix prompt auto-import missing OpenCode/OpenClaw and the PromptFormPanel filenameMap type exclusion. --- src-tauri/src/commands/mod.rs | 2 + src-tauri/src/commands/workspace.rs | 59 +++++++++ src-tauri/src/lib.rs | 5 + src/App.tsx | 22 +++- src/components/prompts/PromptFormPanel.tsx | 5 +- .../workspace/WorkspaceFileEditor.tsx | 95 +++++++++++++++ .../workspace/WorkspaceFilesPanel.tsx | 115 ++++++++++++++++++ src/i18n/locales/en.json | 16 +++ src/i18n/locales/ja.json | 16 +++ src/i18n/locales/zh.json | 16 +++ src/lib/api/index.ts | 1 + src/lib/api/workspace.ts | 11 ++ tests/components/McpFormModal.test.tsx | 1 + 13 files changed, 360 insertions(+), 4 deletions(-) create mode 100644 src-tauri/src/commands/workspace.rs create mode 100644 src/components/workspace/WorkspaceFileEditor.tsx create mode 100644 src/components/workspace/WorkspaceFilesPanel.tsx create mode 100644 src/lib/api/workspace.ts diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index f7a1b81c0..6a102b864 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -20,6 +20,7 @@ mod stream_check; mod sync_support; mod usage; mod webdav_sync; +mod workspace; pub use config::*; pub use deeplink::*; @@ -40,3 +41,4 @@ pub use skill::*; pub use stream_check::*; pub use usage::*; pub use webdav_sync::*; +pub use workspace::*; diff --git a/src-tauri/src/commands/workspace.rs b/src-tauri/src/commands/workspace.rs new file mode 100644 index 000000000..8e092de4f --- /dev/null +++ b/src-tauri/src/commands/workspace.rs @@ -0,0 +1,59 @@ +use crate::config::write_text_file; +use crate::openclaw_config::get_openclaw_dir; + +/// Allowed workspace filenames (whitelist for security) +const ALLOWED_FILES: &[&str] = &[ + "AGENTS.md", + "SOUL.md", + "USER.md", + "IDENTITY.md", + "TOOLS.md", + "MEMORY.md", +]; + +fn validate_filename(filename: &str) -> Result<(), String> { + if !ALLOWED_FILES.contains(&filename) { + return Err(format!( + "Invalid workspace filename: {filename}. Allowed: {}", + ALLOWED_FILES.join(", ") + )); + } + Ok(()) +} + +/// Read an OpenClaw workspace file content. +/// Returns None if the file does not exist. +#[tauri::command] +pub async fn read_workspace_file(filename: String) -> Result, String> { + validate_filename(&filename)?; + + let path = get_openclaw_dir().join("workspace").join(&filename); + + if !path.exists() { + return Ok(None); + } + + std::fs::read_to_string(&path).map(Some).map_err(|e| { + format!("Failed to read workspace file {filename}: {e}") + }) +} + +/// Write content to an OpenClaw workspace file (atomic write). +/// Creates the workspace directory if it does not exist. +#[tauri::command] +pub async fn write_workspace_file(filename: String, content: String) -> Result<(), String> { + validate_filename(&filename)?; + + let workspace_dir = get_openclaw_dir().join("workspace"); + + // Ensure workspace directory exists + std::fs::create_dir_all(&workspace_dir).map_err(|e| { + format!("Failed to create workspace directory: {e}") + })?; + + let path = workspace_dir.join(&filename); + + write_text_file(&path, &content).map_err(|e| { + format!("Failed to write workspace file {filename}: {e}") + }) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 05cde9676..26c2f4cbd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -582,6 +582,8 @@ pub fn run() { crate::app_config::AppType::Claude, crate::app_config::AppType::Codex, crate::app_config::AppType::Gemini, + crate::app_config::AppType::OpenCode, + crate::app_config::AppType::OpenClaw, ] { match crate::services::prompt::PromptService::import_from_file_on_first_launch( &app_state, @@ -1023,6 +1025,9 @@ pub fn run() { commands::get_current_omo_provider_id, commands::get_omo_provider_count, commands::disable_current_omo, + // Workspace files (OpenClaw) + commands::read_workspace_file, + commands::write_workspace_file, ]); let app = builder diff --git a/src/App.tsx b/src/App.tsx index a4af76b4a..95c946c28 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import { Download, FolderArchive, Search, + FolderOpen, } from "lucide-react"; import type { Provider, VisibleApps } from "@/types"; import type { EnvConflict } from "@/types/env"; @@ -56,6 +57,7 @@ import { McpIcon } from "@/components/BrandIcons"; import { Button } from "@/components/ui/button"; import { SessionManagerPage } from "@/components/sessions/SessionManagerPage"; import { useDisableCurrentOmo } from "@/lib/query/omo"; +import WorkspaceFilesPanel from "@/components/workspace/WorkspaceFilesPanel"; type View = | "providers" @@ -66,14 +68,15 @@ type View = | "mcp" | "agents" | "universal" - | "sessions"; + | "sessions" + | "workspace"; const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px const HEADER_HEIGHT = 64; // px const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT; const STORAGE_KEY = "cc-switch-last-app"; -const VALID_APPS: AppId[] = ["claude", "codex", "gemini", "opencode"]; +const VALID_APPS: AppId[] = ["claude", "codex", "gemini", "opencode", "openclaw"]; const getInitialApp = (): AppId => { const saved = localStorage.getItem(STORAGE_KEY) as AppId | null; @@ -94,6 +97,7 @@ const VALID_VIEWS: View[] = [ "agents", "universal", "sessions", + "workspace", ]; const getInitialView = (): View => { @@ -625,6 +629,8 @@ function App() { case "sessions": return ; + case "workspace": + return ; default: return (
@@ -783,6 +789,7 @@ function App() { defaultValue: "统一供应商", })} {currentView === "sessions" && t("sessionManager.title")} + {currentView === "workspace" && t("workspace.title")}
) : ( @@ -985,6 +992,17 @@ function App() { > + {activeApp === "openclaw" && ( + + )} + } + > + {loading ? ( +
+ {t("prompts.loading")} +
+ ) : ( + + )} + + ); +}; + +export default WorkspaceFileEditor; diff --git a/src/components/workspace/WorkspaceFilesPanel.tsx b/src/components/workspace/WorkspaceFilesPanel.tsx new file mode 100644 index 000000000..04d151ca4 --- /dev/null +++ b/src/components/workspace/WorkspaceFilesPanel.tsx @@ -0,0 +1,115 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + FileCode, + Heart, + User, + IdCard, + Wrench, + Brain, + CheckCircle2, + Circle, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { workspaceApi } from "@/lib/api/workspace"; +import WorkspaceFileEditor from "./WorkspaceFileEditor"; + +interface WorkspaceFile { + filename: string; + icon: LucideIcon; + descKey: string; +} + +const WORKSPACE_FILES: WorkspaceFile[] = [ + { filename: "AGENTS.md", icon: FileCode, descKey: "workspace.files.agents" }, + { filename: "SOUL.md", icon: Heart, descKey: "workspace.files.soul" }, + { filename: "USER.md", icon: User, descKey: "workspace.files.user" }, + { + filename: "IDENTITY.md", + icon: IdCard, + descKey: "workspace.files.identity", + }, + { filename: "TOOLS.md", icon: Wrench, descKey: "workspace.files.tools" }, + { filename: "MEMORY.md", icon: Brain, descKey: "workspace.files.memory" }, +]; + +const WorkspaceFilesPanel: React.FC = () => { + const { t } = useTranslation(); + const [editingFile, setEditingFile] = useState(null); + const [fileExists, setFileExists] = useState>({}); + + const checkFileExistence = async () => { + const results: Record = {}; + await Promise.all( + WORKSPACE_FILES.map(async (f) => { + try { + const content = await workspaceApi.readFile(f.filename); + results[f.filename] = content !== null; + } catch { + results[f.filename] = false; + } + }), + ); + setFileExists(results); + }; + + useEffect(() => { + void checkFileExistence(); + }, []); + + const handleEditorClose = () => { + setEditingFile(null); + // Re-check file existence after closing editor (file may have been created) + void checkFileExistence(); + }; + + return ( +
+

+ ~/.openclaw/workspace/ +

+ +
+ {WORKSPACE_FILES.map((file) => { + const Icon = file.icon; + const exists = fileExists[file.filename]; + + return ( + + ); + })} +
+ + +
+ ); +}; + +export default WorkspaceFilesPanel; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index d9df8cbe6..1dee7f489 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1115,6 +1115,22 @@ "deleteMessage": "Are you sure you want to delete prompt \"{{name}}\"?" } }, + "workspace": { + "title": "Workspace Files", + "manage": "Workspace", + "files": { + "agents": "Agent instructions and rules", + "soul": "Agent personality and communication style", + "user": "User profile and preferences", + "identity": "Agent name and avatar", + "tools": "Local tool documentation", + "memory": "Long-term memory and decisions" + }, + "editing": "Edit {{filename}}", + "saveSuccess": "Saved successfully", + "saveFailed": "Failed to save", + "loadFailed": "Failed to load" + }, "env": { "warning": { "title": "Environment Variable Conflicts Detected", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 02effd72b..f2d753d37 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1115,6 +1115,22 @@ "deleteMessage": "プロンプト「{{name}}」を削除してもよろしいですか?" } }, + "workspace": { + "title": "ワークスペースファイル", + "manage": "ワークスペース", + "files": { + "agents": "エージェントの操作指示とルール", + "soul": "エージェントの人格とコミュニケーションスタイル", + "user": "ユーザープロファイルと設定", + "identity": "エージェントの名前とアバター", + "tools": "ローカルツールドキュメント", + "memory": "長期記憶と意思決定記録" + }, + "editing": "{{filename}} を編集", + "saveSuccess": "保存しました", + "saveFailed": "保存に失敗しました", + "loadFailed": "読み込みに失敗しました" + }, "env": { "warning": { "title": "競合する環境変数を検出しました", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index c4012ac51..f0c2b08dc 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1115,6 +1115,22 @@ "deleteMessage": "确定要删除提示词 \"{{name}}\" 吗?" } }, + "workspace": { + "title": "Workspace 文件管理", + "manage": "Workspace", + "files": { + "agents": "Agent 操作指令和规则", + "soul": "Agent 人格和沟通风格", + "user": "用户档案和偏好", + "identity": "Agent 名称和头像", + "tools": "本地工具文档", + "memory": "长期记忆和决策记录" + }, + "editing": "编辑 {{filename}}", + "saveSuccess": "保存成功", + "saveFailed": "保存失败", + "loadFailed": "读取失败" + }, "env": { "warning": { "title": "检测到系统环境变量冲突", diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 6a797f1ea..7a0bfd1c2 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -9,6 +9,7 @@ export { vscodeApi } from "./vscode"; export { proxyApi } from "./proxy"; export { openclawApi } from "./openclaw"; export { sessionsApi } from "./sessions"; +export { workspaceApi } from "./workspace"; export * as configApi from "./config"; export type { ProviderSwitchEvent } from "./providers"; export type { Prompt } from "./prompts"; diff --git a/src/lib/api/workspace.ts b/src/lib/api/workspace.ts new file mode 100644 index 000000000..095913483 --- /dev/null +++ b/src/lib/api/workspace.ts @@ -0,0 +1,11 @@ +import { invoke } from "@tauri-apps/api/core"; + +export const workspaceApi = { + async readFile(filename: string): Promise { + return invoke("read_workspace_file", { filename }); + }, + + async writeFile(filename: string, content: string): Promise { + return invoke("write_workspace_file", { filename, content }); + }, +}; diff --git a/tests/components/McpFormModal.test.tsx b/tests/components/McpFormModal.test.tsx index c70951186..8816ceaf9 100644 --- a/tests/components/McpFormModal.test.tsx +++ b/tests/components/McpFormModal.test.tsx @@ -433,6 +433,7 @@ type = "stdio" codex: false, gemini: false, opencode: false, + openclaw: false, }); expect(onSave).toHaveBeenCalledTimes(1); expect(toastErrorMock).not.toHaveBeenCalled(); From 4f2812f4720ea88129f1f4073bc48d9d35b91aab Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Feb 2026 22:05:15 +0800 Subject: [PATCH 39/51] feat(openclaw): show only Workspace button in menu bar Hide Skills, Prompts, Sessions, and MCP buttons when OpenClaw is active, as it only needs Workspace file management functionality. --- src/App.tsx | 109 ++++++++++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 95c946c28..2096b3fc6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -76,7 +76,13 @@ const HEADER_HEIGHT = 64; // px const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT; const STORAGE_KEY = "cc-switch-last-app"; -const VALID_APPS: AppId[] = ["claude", "codex", "gemini", "opencode", "openclaw"]; +const VALID_APPS: AppId[] = [ + "claude", + "codex", + "gemini", + "opencode", + "openclaw", +]; const getInitialApp = (): AppId => { const saved = localStorage.getItem(STORAGE_KEY) as AppId | null; @@ -968,31 +974,7 @@ function App() { />
- - - {activeApp === "openclaw" && ( + {activeApp === "openclaw" ? ( + ) : ( + <> + + + + + )} - -
+ <> + + + + + ) : ( <> + + + ); +}; + +export default AgentsDefaultsPanel; diff --git a/src/components/openclaw/EnvPanel.tsx b/src/components/openclaw/EnvPanel.tsx new file mode 100644 index 000000000..9a36844a0 --- /dev/null +++ b/src/components/openclaw/EnvPanel.tsx @@ -0,0 +1,179 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Plus, Trash2, Save, Eye, EyeOff } from "lucide-react"; +import { toast } from "sonner"; +import { openclawApi } from "@/lib/api/openclaw"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import type { OpenClawEnvConfig } from "@/types"; + +interface EnvEntry { + key: string; + value: string; + isNew?: boolean; +} + +const EnvPanel: React.FC = () => { + const { t } = useTranslation(); + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [visibleKeys, setVisibleKeys] = useState>(new Set()); + + const loadEnv = useCallback(async () => { + try { + setLoading(true); + const env = await openclawApi.getEnv(); + const items: EnvEntry[] = Object.entries(env).map(([key, value]) => ({ + key, + value: String(value ?? ""), + })); + setEntries(items.length > 0 ? items : []); + } catch (err) { + toast.error(t("openclaw.env.loadFailed")); + console.error("Failed to load env config:", err); + } finally { + setLoading(false); + } + }, [t]); + + useEffect(() => { + void loadEnv(); + }, [loadEnv]); + + const handleSave = async () => { + try { + setSaving(true); + const env: OpenClawEnvConfig = {}; + for (const entry of entries) { + const trimmedKey = entry.key.trim(); + if (trimmedKey) { + env[trimmedKey] = entry.value; + } + } + await openclawApi.setEnv(env); + toast.success(t("openclaw.env.saveSuccess")); + // Reload to normalize + await loadEnv(); + } catch (err) { + toast.error(t("openclaw.env.saveFailed")); + console.error("Failed to save env config:", err); + } finally { + setSaving(false); + } + }; + + const addEntry = () => { + setEntries((prev) => [...prev, { key: "", value: "", isNew: true }]); + }; + + const removeEntry = (index: number) => { + setEntries((prev) => prev.filter((_, i) => i !== index)); + }; + + const updateEntry = (index: number, field: "key" | "value", val: string) => { + setEntries((prev) => + prev.map((entry, i) => + i === index ? { ...entry, [field]: val } : entry, + ), + ); + }; + + const toggleVisibility = (key: string) => { + setVisibleKeys((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + }; + + const isApiKey = (key: string) => /key|token|secret|password/i.test(key); + + if (loading) { + return ( +
+
+ {t("common.loading")} +
+
+ ); + } + + return ( +
+

+ {t("openclaw.env.description")} +

+ +
+ {entries.map((entry, index) => { + const sensitive = isApiKey(entry.key); + const visible = visibleKeys.has(`${index}`); + + return ( +
+
+ updateEntry(index, "key", e.target.value)} + placeholder={t("openclaw.env.keyPlaceholder")} + className="font-mono text-xs" + autoFocus={entry.isNew} + /> +
+
+ updateEntry(index, "value", e.target.value)} + placeholder={t("openclaw.env.valuePlaceholder")} + className="font-mono text-xs" + /> + {sensitive && ( + + )} +
+ +
+ ); + })} +
+ +
+ +
+ +
+
+ ); +}; + +export default EnvPanel; diff --git a/src/components/openclaw/ToolsPanel.tsx b/src/components/openclaw/ToolsPanel.tsx new file mode 100644 index 000000000..58830296f --- /dev/null +++ b/src/components/openclaw/ToolsPanel.tsx @@ -0,0 +1,204 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Plus, Trash2, Save } from "lucide-react"; +import { toast } from "sonner"; +import { openclawApi } from "@/lib/api/openclaw"; +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 { OpenClawToolsConfig } from "@/types"; + +const PROFILE_OPTIONS = ["default", "strict", "permissive", "custom"]; + +const ToolsPanel: React.FC = () => { + const { t } = useTranslation(); + const [config, setConfig] = useState({}); + const [allowList, setAllowList] = useState([]); + const [denyList, setDenyList] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const loadTools = useCallback(async () => { + try { + setLoading(true); + const tools = await openclawApi.getTools(); + setConfig(tools); + setAllowList(tools.allow ?? []); + setDenyList(tools.deny ?? []); + } catch (err) { + toast.error(t("openclaw.tools.loadFailed")); + console.error("Failed to load tools config:", err); + } finally { + setLoading(false); + } + }, [t]); + + useEffect(() => { + void loadTools(); + }, [loadTools]); + + const handleSave = async () => { + try { + setSaving(true); + const { profile, allow, deny, ...other } = config; + const newConfig: OpenClawToolsConfig = { + ...other, + profile: config.profile, + allow: allowList.filter((s) => s.trim()), + deny: denyList.filter((s) => s.trim()), + }; + await openclawApi.setTools(newConfig); + toast.success(t("openclaw.tools.saveSuccess")); + await loadTools(); + } catch (err) { + toast.error(t("openclaw.tools.saveFailed")); + console.error("Failed to save tools config:", err); + } finally { + setSaving(false); + } + }; + + const updateListItem = ( + list: string[], + setList: React.Dispatch>, + index: number, + value: string, + ) => { + setList(list.map((item, i) => (i === index ? value : item))); + }; + + const removeListItem = ( + list: string[], + setList: React.Dispatch>, + index: number, + ) => { + setList(list.filter((_, i) => i !== index)); + }; + + if (loading) { + return ( +
+
+ {t("common.loading")} +
+
+ ); + } + + return ( +
+

+ {t("openclaw.tools.description")} +

+ + {/* Profile selector */} +
+ + +
+ + {/* Allow list */} +
+ +
+ {allowList.map((item, index) => ( +
+ + updateListItem(allowList, setAllowList, index, e.target.value) + } + placeholder={t("openclaw.tools.patternPlaceholder")} + className="font-mono text-xs" + /> + +
+ ))} + +
+
+ + {/* Deny list */} +
+ +
+ {denyList.map((item, index) => ( +
+ + updateListItem(denyList, setDenyList, index, e.target.value) + } + placeholder={t("openclaw.tools.patternPlaceholder")} + className="font-mono text-xs" + /> + +
+ ))} + +
+
+ + {/* Save button */} +
+ +
+
+ ); +}; + +export default ToolsPanel; diff --git a/src/lib/api/openclaw.ts b/src/lib/api/openclaw.ts index 251650a22..ae36d4037 100644 --- a/src/lib/api/openclaw.ts +++ b/src/lib/api/openclaw.ts @@ -1,12 +1,25 @@ import { invoke } from "@tauri-apps/api/core"; -import type { OpenClawDefaultModel, OpenClawModelCatalogEntry } from "@/types"; +import type { + OpenClawDefaultModel, + OpenClawModelCatalogEntry, + OpenClawAgentsDefaults, + OpenClawEnvConfig, + OpenClawToolsConfig, +} from "@/types"; /** - * OpenClaw agents configuration API + * OpenClaw configuration API * - * Manages agents.defaults configuration in ~/.openclaw/openclaw.json + * Manages ~/.openclaw/openclaw.json sections: + * - agents.defaults (model, catalog) + * - env (environment variables) + * - tools (permissions) */ export const openclawApi = { + // ============================================================ + // Agents Configuration + // ============================================================ + /** * Get default model configuration (agents.defaults.model) */ @@ -40,6 +53,24 @@ export const openclawApi = { return await invoke("set_openclaw_model_catalog", { catalog }); }, + /** + * Get full agents.defaults config (all fields) + */ + async getAgentsDefaults(): Promise { + return await invoke("get_openclaw_agents_defaults"); + }, + + /** + * Set full agents.defaults config (all fields) + */ + async setAgentsDefaults(defaults: OpenClawAgentsDefaults): Promise { + return await invoke("set_openclaw_agents_defaults", { defaults }); + }, + + // ============================================================ + // Provider Import + // ============================================================ + /** * Import providers from live config (openclaw.json) to database */ @@ -53,4 +84,40 @@ export const openclawApi = { async getLiveProviderIds(): Promise { return await invoke("get_openclaw_live_provider_ids"); }, + + // ============================================================ + // Env Configuration + // ============================================================ + + /** + * Get env config (env section of openclaw.json) + */ + async getEnv(): Promise { + return await invoke("get_openclaw_env"); + }, + + /** + * Set env config (env section of openclaw.json) + */ + async setEnv(env: OpenClawEnvConfig): Promise { + return await invoke("set_openclaw_env", { env }); + }, + + // ============================================================ + // Tools Configuration + // ============================================================ + + /** + * Get tools config (tools section of openclaw.json) + */ + async getTools(): Promise { + return await invoke("get_openclaw_tools"); + }, + + /** + * Set tools config (tools section of openclaw.json) + */ + async setTools(tools: OpenClawToolsConfig): Promise { + return await invoke("set_openclaw_tools", { tools }); + }, }; diff --git a/src/types.ts b/src/types.ts index 45b22d620..f76790c19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -467,3 +467,23 @@ export interface OpenClawProviderConfig { api?: string; // API 协议类型(如 "openai-completions"、"anthropic") models?: OpenClawModel[]; // 可用模型列表 } + +// OpenClaw agents.defaults 完整配置 +export interface OpenClawAgentsDefaults { + model?: OpenClawDefaultModel; + models?: Record; + [key: string]: unknown; // preserve unknown fields +} + +// OpenClaw env 配置(openclaw.json 的 env 节点) +export interface OpenClawEnvConfig { + [key: string]: unknown; +} + +// OpenClaw tools 配置(openclaw.json 的 tools 节点) +export interface OpenClawToolsConfig { + profile?: string; + allow?: string[]; + deny?: string[]; + [key: string]: unknown; // preserve unknown fields +} From eccb95b83ddeb052d3c29ee34f6dbedfb978fb9e Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 8 Feb 2026 00:00:22 +0800 Subject: [PATCH 43/51] fix(openclaw): enable scrolling on config panels Add overflow-y-auto to main content area so OpenClaw panels (Env, Tools, Agents) can scroll when content exceeds viewport. --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 626b020d3..9985431ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1102,7 +1102,7 @@ function App() {
-
+
{renderContent()}
From bc87f9d9eb66f49e823acd65454af1cb98c26905 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 8 Feb 2026 11:14:46 +0800 Subject: [PATCH 44/51] fix(openclaw): address code review findings for robustness - Fix EnvPanel visibleKeys using entry key name instead of array index to prevent visibility state corruption after deletion - Add NaN guard in AgentsDefaultsPanel numeric field parsing - Validate provider id and models before importing from live config - Upgrade import failure log level from debug to warn for OpenCode/OpenClaw --- src-tauri/src/lib.rs | 4 ++-- src-tauri/src/services/provider/live.rs | 10 ++++++++++ .../openclaw/AgentsDefaultsPanel.tsx | 19 +++++++++++++++---- src/components/openclaw/EnvPanel.tsx | 5 +++-- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 70b05b617..92edd4aaf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -501,7 +501,7 @@ pub fn run() { log::info!("✓ Imported {count} OpenCode provider(s) from live config"); } Ok(_) => log::debug!("○ No OpenCode providers found to import"), - Err(e) => log::debug!("○ Failed to import OpenCode providers: {e}"), + Err(e) => log::warn!("○ Failed to import OpenCode providers: {e}"), } // 2.2 OMO 配置导入(当数据库中无 OMO provider 时,从本地文件导入) @@ -534,7 +534,7 @@ pub fn run() { log::info!("✓ Imported {count} OpenClaw provider(s) from live config"); } Ok(_) => log::debug!("○ No OpenClaw providers found to import"), - Err(e) => log::debug!("○ Failed to import OpenClaw providers: {e}"), + Err(e) => log::warn!("○ Failed to import OpenClaw providers: {e}"), } // 3. 导入 MCP 服务器配置(表空时触发) diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 01242fd5d..2bd279a60 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -707,6 +707,16 @@ pub fn import_openclaw_providers_from_live(state: &AppState) -> Result { }; } - // Optional numeric fields + // Optional fields if (workspace.trim()) updated.workspace = workspace.trim(); else delete updated.workspace; - if (timeout.trim()) updated.timeout = Number(timeout); + // Numeric fields: validate before saving to avoid NaN + const parseNum = (v: string) => { + const n = Number(v); + return !isNaN(n) && isFinite(n) ? n : undefined; + }; + + const timeoutNum = timeout.trim() ? parseNum(timeout) : undefined; + if (timeoutNum !== undefined) updated.timeout = timeoutNum; else delete updated.timeout; - if (contextTokens.trim()) updated.contextTokens = Number(contextTokens); + const ctxNum = contextTokens.trim() ? parseNum(contextTokens) : undefined; + if (ctxNum !== undefined) updated.contextTokens = ctxNum; else delete updated.contextTokens; - if (maxConcurrent.trim()) updated.maxConcurrent = Number(maxConcurrent); + const concNum = maxConcurrent.trim() + ? parseNum(maxConcurrent) + : undefined; + if (concNum !== undefined) updated.maxConcurrent = concNum; else delete updated.maxConcurrent; await openclawApi.setAgentsDefaults(updated); diff --git a/src/components/openclaw/EnvPanel.tsx b/src/components/openclaw/EnvPanel.tsx index 9a36844a0..537845064 100644 --- a/src/components/openclaw/EnvPanel.tsx +++ b/src/components/openclaw/EnvPanel.tsx @@ -112,7 +112,8 @@ const EnvPanel: React.FC = () => {
{entries.map((entry, index) => { const sensitive = isApiKey(entry.key); - const visible = visibleKeys.has(`${index}`); + const visibilityId = entry.key || `__new_${index}`; + const visible = visibleKeys.has(visibilityId); return (
@@ -138,7 +139,7 @@ const EnvPanel: React.FC = () => { variant="ghost" size="icon" className="flex-shrink-0 h-9 w-9 text-muted-foreground" - onClick={() => toggleVisibility(`${index}`)} + onClick={() => toggleVisibility(visibilityId)} > {visible ? ( From 00b424628fdaf791b99b771f1b141dca630a0764 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 8 Feb 2026 15:35:09 +0800 Subject: [PATCH 45/51] refactor(openclaw): migrate config panels to TanStack Query hooks Centralize query keys and extract reusable hooks (useOpenClaw.ts), replacing manual useState/useEffect load/save patterns in Env, Tools, and AgentsDefaults panels for consistency with MCP/Skills modules. --- src/App.tsx | 3 +- .../openclaw/AgentsDefaultsPanel.tsx | 78 +++++------ src/components/openclaw/EnvPanel.tsx | 53 +++----- src/components/openclaw/ToolsPanel.tsx | 56 ++++---- src/components/providers/ProviderList.tsx | 22 +-- src/hooks/useOpenClaw.ts | 128 ++++++++++++++++++ src/hooks/useProviderActions.ts | 3 +- src/lib/query/mutations.ts | 5 +- 8 files changed, 228 insertions(+), 120 deletions(-) create mode 100644 src/hooks/useOpenClaw.ts diff --git a/src/App.tsx b/src/App.tsx index 9985431ec..384eaa661 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import { } from "@/lib/api"; import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env"; import { useProviderActions } from "@/hooks/useProviderActions"; +import { openclawKeys } from "@/hooks/useOpenClaw"; import { useProxyStatus } from "@/hooks/useProxyStatus"; import { useLastValidValue } from "@/hooks/useLastValidValue"; import { extractErrorMessage } from "@/utils/errorUtils"; @@ -458,7 +459,7 @@ function App() { }); } else if (activeApp === "openclaw") { await queryClient.invalidateQueries({ - queryKey: ["openclawLiveProviderIds"], + queryKey: openclawKeys.liveProviderIds, }); } toast.success( diff --git a/src/components/openclaw/AgentsDefaultsPanel.tsx b/src/components/openclaw/AgentsDefaultsPanel.tsx index 481b91b0d..03d5af3f9 100644 --- a/src/components/openclaw/AgentsDefaultsPanel.tsx +++ b/src/components/openclaw/AgentsDefaultsPanel.tsx @@ -1,8 +1,12 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Save } from "lucide-react"; import { toast } from "sonner"; -import { openclawApi } from "@/lib/api/openclaw"; +import { + useOpenClawAgentsDefaults, + useSaveOpenClawAgentsDefaults, +} from "@/hooks/useOpenClaw"; +import { extractErrorMessage } from "@/utils/errorUtils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -10,11 +14,11 @@ import type { OpenClawAgentsDefaults } from "@/types"; const AgentsDefaultsPanel: React.FC = () => { const { t } = useTranslation(); + const { data: agentsData, isLoading } = useOpenClawAgentsDefaults(); + const saveAgentsMutation = useSaveOpenClawAgentsDefaults(); const [defaults, setDefaults] = useState(null); const [primaryModel, setPrimaryModel] = useState(""); const [fallbacks, setFallbacks] = useState(""); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); // Extra known fields from agents.defaults const [workspace, setWorkspace] = useState(""); @@ -22,38 +26,25 @@ const AgentsDefaultsPanel: React.FC = () => { const [contextTokens, setContextTokens] = useState(""); const [maxConcurrent, setMaxConcurrent] = useState(""); - const loadDefaults = useCallback(async () => { - try { - setLoading(true); - const data = await openclawApi.getAgentsDefaults(); - setDefaults(data); - - if (data) { - setPrimaryModel(data.model?.primary ?? ""); - setFallbacks((data.model?.fallbacks ?? []).join(", ")); - - // Extract known extra fields - setWorkspace(String(data.workspace ?? "")); - setTimeout_(String(data.timeout ?? "")); - setContextTokens(String(data.contextTokens ?? "")); - setMaxConcurrent(String(data.maxConcurrent ?? "")); - } - } catch (err) { - toast.error(t("openclaw.agents.loadFailed")); - console.error("Failed to load agents defaults:", err); - } finally { - setLoading(false); - } - }, [t]); - useEffect(() => { - void loadDefaults(); - }, [loadDefaults]); + // agentsData is undefined while loading, null when config section is absent + if (agentsData === undefined) return; + setDefaults(agentsData); + + if (agentsData) { + setPrimaryModel(agentsData.model?.primary ?? ""); + setFallbacks((agentsData.model?.fallbacks ?? []).join(", ")); + + // Extract known extra fields + setWorkspace(String(agentsData.workspace ?? "")); + setTimeout_(String(agentsData.timeout ?? "")); + setContextTokens(String(agentsData.contextTokens ?? "")); + setMaxConcurrent(String(agentsData.maxConcurrent ?? "")); + } + }, [agentsData]); const handleSave = async () => { try { - setSaving(true); - // Preserve all unknown fields from original data const updated: OpenClawAgentsDefaults = { ...defaults }; @@ -94,18 +85,17 @@ const AgentsDefaultsPanel: React.FC = () => { if (concNum !== undefined) updated.maxConcurrent = concNum; else delete updated.maxConcurrent; - await openclawApi.setAgentsDefaults(updated); + await saveAgentsMutation.mutateAsync(updated); toast.success(t("openclaw.agents.saveSuccess")); - await loadDefaults(); - } catch (err) { - toast.error(t("openclaw.agents.saveFailed")); - console.error("Failed to save agents defaults:", err); - } finally { - setSaving(false); + } catch (error) { + const detail = extractErrorMessage(error); + toast.error(t("openclaw.agents.saveFailed"), { + description: detail || undefined, + }); } }; - if (loading) { + if (isLoading) { return (
@@ -222,9 +212,13 @@ const AgentsDefaultsPanel: React.FC = () => { {/* Save button */}
-
diff --git a/src/components/openclaw/EnvPanel.tsx b/src/components/openclaw/EnvPanel.tsx index 537845064..baba04fae 100644 --- a/src/components/openclaw/EnvPanel.tsx +++ b/src/components/openclaw/EnvPanel.tsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Plus, Trash2, Save, Eye, EyeOff } from "lucide-react"; import { toast } from "sonner"; -import { openclawApi } from "@/lib/api/openclaw"; +import { useOpenClawEnv, useSaveOpenClawEnv } from "@/hooks/useOpenClaw"; +import { extractErrorMessage } from "@/utils/errorUtils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import type { OpenClawEnvConfig } from "@/types"; @@ -15,35 +16,23 @@ interface EnvEntry { const EnvPanel: React.FC = () => { const { t } = useTranslation(); + const { data: envData, isLoading } = useOpenClawEnv(); + const saveEnvMutation = useSaveOpenClawEnv(); const [entries, setEntries] = useState([]); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); const [visibleKeys, setVisibleKeys] = useState>(new Set()); - const loadEnv = useCallback(async () => { - try { - setLoading(true); - const env = await openclawApi.getEnv(); - const items: EnvEntry[] = Object.entries(env).map(([key, value]) => ({ + useEffect(() => { + if (envData) { + const items: EnvEntry[] = Object.entries(envData).map(([key, value]) => ({ key, value: String(value ?? ""), })); setEntries(items.length > 0 ? items : []); - } catch (err) { - toast.error(t("openclaw.env.loadFailed")); - console.error("Failed to load env config:", err); - } finally { - setLoading(false); } - }, [t]); - - useEffect(() => { - void loadEnv(); - }, [loadEnv]); + }, [envData]); const handleSave = async () => { try { - setSaving(true); const env: OpenClawEnvConfig = {}; for (const entry of entries) { const trimmedKey = entry.key.trim(); @@ -51,15 +40,13 @@ const EnvPanel: React.FC = () => { env[trimmedKey] = entry.value; } } - await openclawApi.setEnv(env); + await saveEnvMutation.mutateAsync(env); toast.success(t("openclaw.env.saveSuccess")); - // Reload to normalize - await loadEnv(); - } catch (err) { - toast.error(t("openclaw.env.saveFailed")); - console.error("Failed to save env config:", err); - } finally { - setSaving(false); + } catch (error) { + const detail = extractErrorMessage(error); + toast.error(t("openclaw.env.saveFailed"), { + description: detail || undefined, + }); } }; @@ -93,7 +80,7 @@ const EnvPanel: React.FC = () => { const isApiKey = (key: string) => /key|token|secret|password/i.test(key); - if (loading) { + if (isLoading) { return (
@@ -168,9 +155,13 @@ const EnvPanel: React.FC = () => { {t("openclaw.env.add")}
-
diff --git a/src/components/openclaw/ToolsPanel.tsx b/src/components/openclaw/ToolsPanel.tsx index 58830296f..b2ee74cd2 100644 --- a/src/components/openclaw/ToolsPanel.tsx +++ b/src/components/openclaw/ToolsPanel.tsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Plus, Trash2, Save } from "lucide-react"; import { toast } from "sonner"; -import { openclawApi } from "@/lib/api/openclaw"; +import { useOpenClawTools, useSaveOpenClawTools } from "@/hooks/useOpenClaw"; +import { extractErrorMessage } from "@/utils/errorUtils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -19,34 +20,22 @@ const PROFILE_OPTIONS = ["default", "strict", "permissive", "custom"]; const ToolsPanel: React.FC = () => { const { t } = useTranslation(); + const { data: toolsData, isLoading } = useOpenClawTools(); + const saveToolsMutation = useSaveOpenClawTools(); const [config, setConfig] = useState({}); const [allowList, setAllowList] = useState([]); const [denyList, setDenyList] = useState([]); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - - const loadTools = useCallback(async () => { - try { - setLoading(true); - const tools = await openclawApi.getTools(); - setConfig(tools); - setAllowList(tools.allow ?? []); - setDenyList(tools.deny ?? []); - } catch (err) { - toast.error(t("openclaw.tools.loadFailed")); - console.error("Failed to load tools config:", err); - } finally { - setLoading(false); - } - }, [t]); useEffect(() => { - void loadTools(); - }, [loadTools]); + if (toolsData) { + setConfig(toolsData); + setAllowList(toolsData.allow ?? []); + setDenyList(toolsData.deny ?? []); + } + }, [toolsData]); const handleSave = async () => { try { - setSaving(true); const { profile, allow, deny, ...other } = config; const newConfig: OpenClawToolsConfig = { ...other, @@ -54,14 +43,13 @@ const ToolsPanel: React.FC = () => { allow: allowList.filter((s) => s.trim()), deny: denyList.filter((s) => s.trim()), }; - await openclawApi.setTools(newConfig); + await saveToolsMutation.mutateAsync(newConfig); toast.success(t("openclaw.tools.saveSuccess")); - await loadTools(); - } catch (err) { - toast.error(t("openclaw.tools.saveFailed")); - console.error("Failed to save tools config:", err); - } finally { - setSaving(false); + } catch (error) { + const detail = extractErrorMessage(error); + toast.error(t("openclaw.tools.saveFailed"), { + description: detail || undefined, + }); } }; @@ -82,7 +70,7 @@ const ToolsPanel: React.FC = () => { setList(list.filter((_, i) => i !== index)); }; - if (loading) { + if (isLoading) { return (
@@ -192,9 +180,13 @@ const ToolsPanel: React.FC = () => { {/* Save button */}
-
diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 8c4f635cc..58111cba1 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -19,8 +19,12 @@ import { useQuery } from "@tanstack/react-query"; import type { Provider } from "@/types"; import type { AppId } from "@/lib/api"; import { providersApi } from "@/lib/api/providers"; -import { openclawApi } from "@/lib/api/openclaw"; import { useDragSort } from "@/hooks/useDragSort"; +import { + useOpenClawLiveProviderIds, + useOpenClawDefaultModel, +} from "@/hooks/useOpenClaw"; +// import { useStreamCheck } from "@/hooks/useStreamCheck"; // 测试功能已隐藏 import { ProviderCard } from "@/components/providers/ProviderCard"; import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState"; import { @@ -88,11 +92,9 @@ export function ProviderList({ }); // OpenClaw: 查询 live 配置中的供应商 ID 列表,用于判断 isInConfig - const { data: openclawLiveIds } = useQuery({ - queryKey: ["openclawLiveProviderIds"], - queryFn: () => providersApi.getOpenClawLiveProviderIds(), - enabled: appId === "openclaw", - }); + const { data: openclawLiveIds } = useOpenClawLiveProviderIds( + appId === "openclaw", + ); // 判断供应商是否已添加到配置(累加模式应用:OpenCode/OpenClaw) const isProviderInConfig = useCallback( @@ -109,11 +111,9 @@ export function ProviderList({ ); // OpenClaw: query default model to determine which provider is default - const { data: openclawDefaultModel } = useQuery({ - queryKey: ["openclawDefaultModel"], - queryFn: () => openclawApi.getDefaultModel(), - enabled: appId === "openclaw", - }); + const { data: openclawDefaultModel } = useOpenClawDefaultModel( + appId === "openclaw", + ); const isProviderDefaultModel = useCallback( (providerId: string): boolean => { diff --git a/src/hooks/useOpenClaw.ts b/src/hooks/useOpenClaw.ts new file mode 100644 index 000000000..384bb0399 --- /dev/null +++ b/src/hooks/useOpenClaw.ts @@ -0,0 +1,128 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { openclawApi } from "@/lib/api/openclaw"; +import { providersApi } from "@/lib/api/providers"; +import type { + OpenClawEnvConfig, + OpenClawToolsConfig, + OpenClawAgentsDefaults, +} from "@/types"; + +/** + * Centralized query keys for all OpenClaw-related queries. + * Import this from any file that needs to invalidate OpenClaw caches. + */ +export const openclawKeys = { + all: ["openclaw"] as const, + liveProviderIds: ["openclaw", "liveProviderIds"] as const, + defaultModel: ["openclaw", "defaultModel"] as const, + env: ["openclaw", "env"] as const, + tools: ["openclaw", "tools"] as const, + agentsDefaults: ["openclaw", "agentsDefaults"] as const, +}; + +// ============================================================ +// Query hooks +// ============================================================ + +/** + * Query live provider IDs from openclaw.json config. + * Used by ProviderList to show "In Config" badge. + */ +export function useOpenClawLiveProviderIds(enabled: boolean) { + return useQuery({ + queryKey: openclawKeys.liveProviderIds, + queryFn: () => providersApi.getOpenClawLiveProviderIds(), + enabled, + }); +} + +/** + * Query the default model from agents.defaults.model. + * Used by ProviderList to show which provider is the default. + */ +export function useOpenClawDefaultModel(enabled: boolean) { + return useQuery({ + queryKey: openclawKeys.defaultModel, + queryFn: () => openclawApi.getDefaultModel(), + enabled, + }); +} + +/** + * Query env section of openclaw.json. + */ +export function useOpenClawEnv() { + return useQuery({ + queryKey: openclawKeys.env, + queryFn: () => openclawApi.getEnv(), + }); +} + +/** + * Query tools section of openclaw.json. + */ +export function useOpenClawTools() { + return useQuery({ + queryKey: openclawKeys.tools, + queryFn: () => openclawApi.getTools(), + }); +} + +/** + * Query agents.defaults section of openclaw.json. + */ +export function useOpenClawAgentsDefaults() { + return useQuery({ + queryKey: openclawKeys.agentsDefaults, + queryFn: () => openclawApi.getAgentsDefaults(), + }); +} + +// ============================================================ +// Mutation hooks +// ============================================================ + +/** + * Save env config. Invalidates env query on success. + * Toast notifications are handled by the component. + */ +export function useSaveOpenClawEnv() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (env: OpenClawEnvConfig) => openclawApi.setEnv(env), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: openclawKeys.env }); + }, + }); +} + +/** + * Save tools config. Invalidates tools query on success. + * Toast notifications are handled by the component. + */ +export function useSaveOpenClawTools() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (tools: OpenClawToolsConfig) => openclawApi.setTools(tools), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: openclawKeys.tools }); + }, + }); +} + +/** + * Save agents.defaults config. Invalidates both agentsDefaults and defaultModel + * queries on success (since changing agents.defaults may affect the default model). + * Toast notifications are handled by the component. + */ +export function useSaveOpenClawAgentsDefaults() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (defaults: OpenClawAgentsDefaults) => + openclawApi.setAgentsDefaults(defaults), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: openclawKeys.agentsDefaults }); + queryClient.invalidateQueries({ queryKey: openclawKeys.defaultModel }); + }, + }); +} diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts index 2f2083df8..42a5c4120 100644 --- a/src/hooks/useProviderActions.ts +++ b/src/hooks/useProviderActions.ts @@ -17,6 +17,7 @@ import { useSwitchProviderMutation, } from "@/lib/query"; import { extractErrorMessage } from "@/utils/errorUtils"; +import { openclawKeys } from "@/hooks/useOpenClaw"; /** * Hook for managing provider actions (add, update, delete, switch) @@ -244,7 +245,7 @@ export function useProviderActions(activeApp: AppId) { try { await openclawApi.setDefaultModel(model); await queryClient.invalidateQueries({ - queryKey: ["openclawDefaultModel"], + queryKey: openclawKeys.defaultModel, }); toast.success( t("notifications.openclawDefaultModelSet", { diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index da83c204f..28efdd570 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -5,6 +5,7 @@ import { providersApi, settingsApi, type AppId } from "@/lib/api"; import type { Provider, Settings } from "@/types"; import { extractErrorMessage } from "@/utils/errorUtils"; import { generateUUID } from "@/utils/uuid"; +import { openclawKeys } from "@/hooks/useOpenClaw"; export const useAddProviderMutation = (appId: AppId) => { const queryClient = useQueryClient(); @@ -185,10 +186,10 @@ export const useSwitchProviderMutation = (appId: AppId) => { } if (appId === "openclaw") { await queryClient.invalidateQueries({ - queryKey: ["openclawLiveProviderIds"], + queryKey: openclawKeys.liveProviderIds, }); await queryClient.invalidateQueries({ - queryKey: ["openclawDefaultModel"], + queryKey: openclawKeys.defaultModel, }); } From 508120617612f324f4134430ae362afdfbabd794 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 8 Feb 2026 17:35:29 +0800 Subject: [PATCH 46/51] fix(openclaw): address code review findings across P0-P3 issues - Add 25 missing i18n keys for OpenClawFormFields in all 3 locales (P0) - Replace key={index} with stable crypto.randomUUID() keys in EnvPanel, ToolsPanel, and OpenClawFormFields to prevent list state bugs (P1) - Exclude openclaw from ProxyToggle/FailoverToggle in App.tsx (P1) - Add merge_additive_config() for openclaw/opencode deep link imports (P1) - Normalize serde(flatten) field naming to `extra` + HashMap (P2) - Add directory existence check in remove_openclaw_provider_from_live (P2) - Remove dead code in import_default_config and openclaw API methods (P2) - Add duplicate key validation in EnvPanel before save (P2) - Add openclawConfigDir to Settings type (P2) - Add staleTime to OpenClaw query hooks (P3) - Fix type-unsafe delete via destructuring in mutations.ts (P3) --- src-tauri/src/deeplink/provider.rs | 45 ++++++++++++ src-tauri/src/openclaw_config.rs | 33 ++++----- src-tauri/src/services/provider/live.rs | 43 +++--------- src/App.tsx | 2 +- src/components/openclaw/EnvPanel.tsx | 15 +++- src/components/openclaw/ToolsPanel.tsx | 69 +++++++++++++------ .../providers/AddProviderDialog.tsx | 1 + .../providers/forms/OpenClawFormFields.tsx | 21 +++++- src/hooks/useOpenClaw.ts | 3 + src/i18n/locales/en.json | 29 +++++++- src/i18n/locales/ja.json | 29 +++++++- src/i18n/locales/zh.json | 29 +++++++- src/lib/api/openclaw.ts | 18 ----- src/lib/query/mutations.ts | 4 +- src/types.ts | 2 + 15 files changed, 241 insertions(+), 102 deletions(-) diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index 0528a2e49..cdb0f3e39 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -482,6 +482,10 @@ pub fn parse_and_merge_config( "claude" => merge_claude_config(&mut merged, &config_value)?, "codex" => merge_codex_config(&mut merged, &config_value)?, "gemini" => merge_gemini_config(&mut merged, &config_value)?, + // Additive mode apps use JSON config directly; pass through as-is + "openclaw" | "opencode" => { + merge_additive_config(&mut merged, &config_value)?; + } "" => { // No app specified, skip merging return Ok(merged); @@ -653,6 +657,47 @@ fn merge_gemini_config( Ok(()) } +/// Merge configuration for additive mode apps (OpenClaw, OpenCode) +/// +/// These apps use JSON config directly, so we only extract common fields +/// (api_key, endpoint, model) from the config if not already set in URL params. +fn merge_additive_config( + request: &mut DeepLinkImportRequest, + config: &serde_json::Value, +) -> Result<(), AppError> { + // Extract api_key from config if not provided in URL + if request.api_key.as_ref().is_none_or(|s| s.is_empty()) { + if let Some(api_key) = config + .get("apiKey") + .or_else(|| config.get("api_key")) + .and_then(|v| v.as_str()) + { + request.api_key = Some(api_key.to_string()); + } + } + + // Extract endpoint from config if not provided in URL + if request.endpoint.as_ref().is_none_or(|s| s.is_empty()) { + if let Some(base_url) = config + .get("baseUrl") + .or_else(|| config.get("base_url")) + .or_else(|| config.get("options").and_then(|o| o.get("baseURL"))) + .and_then(|v| v.as_str()) + { + request.endpoint = Some(base_url.to_string()); + } + } + + // Auto-fill homepage from endpoint + if request.homepage.as_ref().is_none_or(|s| s.is_empty()) { + if let Some(endpoint) = request.endpoint.as_ref().filter(|s| !s.is_empty()) { + request.homepage = infer_homepage_from_endpoint(endpoint); + } + } + + Ok(()) +} + /// Extract base_url from Codex TOML config fn extract_codex_base_url(toml_value: &toml::Value) -> Option { // Try to find base_url in model_providers section diff --git a/src-tauri/src/openclaw_config.rs b/src-tauri/src/openclaw_config.rs index 30a51ef3f..20b18ccf9 100644 --- a/src-tauri/src/openclaw_config.rs +++ b/src-tauri/src/openclaw_config.rs @@ -95,9 +95,9 @@ pub struct OpenClawProviderConfig { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub models: Vec, - /// 其他自定义字段(保留原始配置) + /// Other custom fields (preserve unknown fields) #[serde(flatten)] - pub extra: Map, + pub extra: HashMap, } /// OpenClaw 模型条目 @@ -123,9 +123,9 @@ pub struct OpenClawModelEntry { #[serde(skip_serializing_if = "Option::is_none")] pub context_window: Option, - /// 其他自定义字段(保留未知字段) + /// Other custom fields (preserve unknown fields) #[serde(flatten)] - pub extra: Map, + pub extra: HashMap, } /// OpenClaw 模型成本配置 @@ -137,9 +137,9 @@ pub struct OpenClawModelCost { /// 输出价格(每百万 token) pub output: f64, - /// 其他自定义字段(保留未知字段) + /// Other custom fields (preserve unknown fields) #[serde(flatten)] - pub other: HashMap, + pub extra: HashMap, } /// OpenClaw 默认模型配置(agents.defaults.model) @@ -152,9 +152,9 @@ pub struct OpenClawDefaultModel { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub fallbacks: Vec, - /// 其他自定义字段(保留未知字段) + /// Other custom fields (preserve unknown fields) #[serde(flatten)] - pub other: HashMap, + pub extra: HashMap, } /// OpenClaw 模型目录条目(agents.defaults.models 中的值) @@ -164,9 +164,9 @@ pub struct OpenClawModelCatalogEntry { #[serde(skip_serializing_if = "Option::is_none")] pub alias: Option, - /// 其他自定义字段(保留未知字段) + /// Other custom fields (preserve unknown fields) #[serde(flatten)] - pub other: HashMap, + pub extra: HashMap, } /// OpenClaw agents.defaults 配置 @@ -180,21 +180,22 @@ pub struct OpenClawAgentsDefaults { #[serde(skip_serializing_if = "Option::is_none")] pub models: Option>, - /// 其他自定义字段(保留未知字段) + /// Other custom fields (preserve unknown fields) #[serde(flatten)] - pub other: HashMap, + pub extra: HashMap, } /// OpenClaw agents 顶层配置 #[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] pub struct OpenClawAgents { /// 默认配置 #[serde(skip_serializing_if = "Option::is_none")] pub defaults: Option, - /// 其他自定义字段(保留未知字段) + /// Other custom fields (preserve unknown fields) #[serde(flatten)] - pub other: HashMap, + pub extra: HashMap, } // ============================================================================ @@ -513,7 +514,7 @@ pub struct OpenClawToolsConfig { /// Other custom fields (preserve unknown fields) #[serde(flatten)] - pub other: HashMap, + pub extra: HashMap, } /// Read the tools config section @@ -525,7 +526,7 @@ pub fn get_tools_config() -> Result { profile: None, allow: Vec::new(), deny: Vec::new(), - other: HashMap::new(), + extra: HashMap::new(), }); }; diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 2bd279a60..da4e15fa6 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -478,40 +478,9 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result { - // OpenCode uses additive mode - import from live is not the same pattern - // For now, return an empty config structure - use crate::opencode_config::{get_opencode_config_path, read_opencode_config}; - - let config_path = get_opencode_config_path(); - if !config_path.exists() { - return Err(AppError::localized( - "opencode.live.missing", - "OpenCode 配置文件不存在", - "OpenCode configuration file is missing", - )); - } - - // For OpenCode, we return the full config - but note that OpenCode - // uses additive mode, so importing defaults works differently - read_opencode_config()? - } - AppType::OpenClaw => { - // OpenClaw uses additive mode - import from live is not the same pattern - use crate::openclaw_config::{get_openclaw_config_path, read_openclaw_config}; - - let config_path = get_openclaw_config_path(); - if !config_path.exists() { - return Err(AppError::localized( - "openclaw.live.missing", - "OpenClaw 配置文件不存在", - "OpenClaw configuration file is missing", - )); - } - - // For OpenClaw, we return the full config - but note that OpenClaw - // uses additive mode, so importing defaults works differently - read_openclaw_config()? + // OpenCode and OpenClaw use additive mode and are handled by early return above + AppType::OpenCode | AppType::OpenClaw => { + unreachable!("additive mode apps are handled by early return") } }; @@ -762,6 +731,12 @@ pub fn import_openclaw_providers_from_live(state: &AppState) -> Result Result<(), AppError> { use crate::openclaw_config; + // Check if OpenClaw config directory exists + if !openclaw_config::get_openclaw_dir().exists() { + log::debug!("OpenClaw config directory doesn't exist, skipping removal of '{provider_id}'"); + return Ok(()); + } + openclaw_config::remove_provider(provider_id)?; log::info!("OpenClaw provider '{provider_id}' removed from live config"); diff --git a/src/App.tsx b/src/App.tsx index 384eaa661..1eedef4da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -970,7 +970,7 @@ function App() { )} {currentView === "providers" && ( <> - {activeApp !== "opencode" && ( + {activeApp !== "opencode" && activeApp !== "openclaw" && ( <>
{ useEffect(() => { if (envData) { const items: EnvEntry[] = Object.entries(envData).map(([key, value]) => ({ + id: crypto.randomUUID(), key, value: String(value ?? ""), })); @@ -34,9 +36,15 @@ const EnvPanel: React.FC = () => { const handleSave = async () => { try { const env: OpenClawEnvConfig = {}; + const seen = new Set(); for (const entry of entries) { const trimmedKey = entry.key.trim(); if (trimmedKey) { + if (seen.has(trimmedKey)) { + toast.error(t("openclaw.env.duplicateKey", { key: trimmedKey })); + return; + } + seen.add(trimmedKey); env[trimmedKey] = entry.value; } } @@ -51,7 +59,10 @@ const EnvPanel: React.FC = () => { }; const addEntry = () => { - setEntries((prev) => [...prev, { key: "", value: "", isNew: true }]); + setEntries((prev) => [ + ...prev, + { id: crypto.randomUUID(), key: "", value: "", isNew: true }, + ]); }; const removeEntry = (index: number) => { @@ -103,7 +114,7 @@ const EnvPanel: React.FC = () => { const visible = visibleKeys.has(visibilityId); return ( -
+
{ @@ -23,14 +28,24 @@ const ToolsPanel: React.FC = () => { const { data: toolsData, isLoading } = useOpenClawTools(); const saveToolsMutation = useSaveOpenClawTools(); const [config, setConfig] = useState({}); - const [allowList, setAllowList] = useState([]); - const [denyList, setDenyList] = useState([]); + const [allowList, setAllowList] = useState([]); + const [denyList, setDenyList] = useState([]); useEffect(() => { if (toolsData) { setConfig(toolsData); - setAllowList(toolsData.allow ?? []); - setDenyList(toolsData.deny ?? []); + setAllowList( + (toolsData.allow ?? []).map((v) => ({ + id: crypto.randomUUID(), + value: v, + })), + ); + setDenyList( + (toolsData.deny ?? []).map((v) => ({ + id: crypto.randomUUID(), + value: v, + })), + ); } }, [toolsData]); @@ -40,8 +55,8 @@ const ToolsPanel: React.FC = () => { const newConfig: OpenClawToolsConfig = { ...other, profile: config.profile, - allow: allowList.filter((s) => s.trim()), - deny: denyList.filter((s) => s.trim()), + allow: allowList.map((item) => item.value).filter((s) => s.trim()), + deny: denyList.map((item) => item.value).filter((s) => s.trim()), }; await saveToolsMutation.mutateAsync(newConfig); toast.success(t("openclaw.tools.saveSuccess")); @@ -54,20 +69,20 @@ const ToolsPanel: React.FC = () => { }; const updateListItem = ( - list: string[], - setList: React.Dispatch>, + setList: React.Dispatch>, index: number, value: string, ) => { - setList(list.map((item, i) => (i === index ? value : item))); + setList((prev) => + prev.map((item, i) => (i === index ? { ...item, value } : item)), + ); }; const removeListItem = ( - list: string[], - setList: React.Dispatch>, + setList: React.Dispatch>, index: number, ) => { - setList(list.filter((_, i) => i !== index)); + setList((prev) => prev.filter((_, i) => i !== index)); }; if (isLoading) { @@ -113,11 +128,11 @@ const ToolsPanel: React.FC = () => {
{allowList.map((item, index) => ( -
+
- updateListItem(allowList, setAllowList, index, e.target.value) + updateListItem(setAllowList, index, e.target.value) } placeholder={t("openclaw.tools.patternPlaceholder")} className="font-mono text-xs" @@ -126,7 +141,7 @@ const ToolsPanel: React.FC = () => { variant="ghost" size="icon" className="flex-shrink-0 h-9 w-9 text-muted-foreground hover:text-destructive" - onClick={() => removeListItem(allowList, setAllowList, index)} + onClick={() => removeListItem(setAllowList, index)} > @@ -135,7 +150,12 @@ const ToolsPanel: React.FC = () => { @@ -170,7 +190,12 @@ const ToolsPanel: React.FC = () => {
- {/* Context Window, Max Tokens and Reasoning row */} -
-
- - - handleModelChange( - index, - "contextWindow", - e.target.value ? parseInt(e.target.value) : undefined, - ) - } - placeholder="200000" - /> -
-
- - - handleModelChange( - index, - "maxTokens", - e.target.value ? parseInt(e.target.value) : undefined, - ) - } - placeholder="32000" - /> -
-
- -
- - handleModelChange(index, "reasoning", checked) - } - /> - - {model.reasoning - ? t("openclaw.reasoningOn", { defaultValue: "启用" }) - : t("openclaw.reasoningOff", { - defaultValue: "关闭", - })} - -
-
- {/* Spacer for alignment with delete button */} -
-
- - {/* Basic Cost row */} -
-
- - - handleCostChange(index, "input", e.target.value) - } - placeholder="3" - /> -
-
- - - handleCostChange(index, "output", e.target.value) - } - placeholder="15" - /> -
- {/* Spacer for alignment */} -
-
-
- {/* Advanced Options (Collapsible) */} - + + {/* Context Window, Max Tokens and Reasoning row */} +
+
+ + + handleModelChange( + index, + "contextWindow", + e.target.value + ? parseInt(e.target.value) + : undefined, + ) + } + placeholder="200000" + /> +
+
+ + + handleModelChange( + index, + "maxTokens", + e.target.value + ? parseInt(e.target.value) + : undefined, + ) + } + placeholder="32000" + /> +
+
+ +
+ + handleModelChange(index, "reasoning", checked) + } + /> + + {model.reasoning + ? t("openclaw.reasoningOn", { + defaultValue: "启用", + }) + : t("openclaw.reasoningOff", { + defaultValue: "关闭", + })} + +
+
+
+ + {/* Cost row */} +
+
+ + + handleCostChange(index, "input", e.target.value) + } + placeholder="3" + /> +
+
+ + + handleCostChange(index, "output", e.target.value) + } + placeholder="15" + /> +
+
+
+ + {/* Cache Cost row */}
- {/* Spacer for alignment */}
-
-

+

{t("openclaw.cacheCostHint", { defaultValue: "缓存价格用于计算 Prompt Caching 的成本。如不使用缓存可留空。", From f974d203a8218b53a1bee65d184d04a924245de8 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 8 Feb 2026 22:26:55 +0800 Subject: [PATCH 48/51] fix(openclaw): remove /v1 from anthropic-messages provider baseUrls Anthropic SDK auto-appends /v1 to baseUrl, so including it in the preset causes double-path (/v1/v1/messages). Affects AiHubMix, DMXAPI, PackyCode, Cubence, and AIGoCode. --- src/config/openclawProviderPresets.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index 5c0761135..c9fad48dc 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -263,7 +263,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ websiteUrl: "https://aihubmix.com", apiKeyUrl: "https://aihubmix.com", settingsConfig: { - baseUrl: "https://aihubmix.com/v1", + baseUrl: "https://aihubmix.com", apiKey: "", api: "anthropic-messages", models: [ @@ -307,7 +307,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ websiteUrl: "https://www.dmxapi.cn", apiKeyUrl: "https://www.dmxapi.cn", settingsConfig: { - baseUrl: "https://www.dmxapi.cn/v1", + baseUrl: "https://www.dmxapi.cn", apiKey: "", api: "anthropic-messages", models: [ @@ -435,7 +435,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ websiteUrl: "https://www.packyapi.com", apiKeyUrl: "https://www.packyapi.com/register?aff=cc-switch", settingsConfig: { - baseUrl: "https://www.packyapi.com/v1", + baseUrl: "https://www.packyapi.com", apiKey: "", api: "anthropic-messages", models: [ @@ -480,7 +480,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ websiteUrl: "https://cubence.com", apiKeyUrl: "https://cubence.com/signup?code=CCSWITCH&source=ccs", settingsConfig: { - baseUrl: "https://api.cubence.com/v1", + baseUrl: "https://api.cubence.com", apiKey: "", api: "anthropic-messages", models: [ @@ -526,7 +526,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ websiteUrl: "https://aigocode.com", apiKeyUrl: "https://aigocode.com/invite/CC-SWITCH", settingsConfig: { - baseUrl: "https://api.aigocode.com/v1", + baseUrl: "https://api.aigocode.com", apiKey: "", api: "anthropic-messages", models: [ From dca12b9f7dcaa3b43dbf63001e62716c35dcb00a Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 8 Feb 2026 22:53:57 +0800 Subject: [PATCH 49/51] feat(openclaw): add 13 new provider presets Migrate missing providers from Claude/Codex/Gemini/OpenCode presets: Tier 1 Partners: - RightCode (anthropic-messages) - AICodeMirror (anthropic-messages) - Zhipu GLM en (openai-completions, international) - MiniMax en (openai-completions, international) Tier 2 Chinese Officials: - Kimi For Coding, KAT-Coder, Longcat, DouBaoSeed, BaiLing, Xiaomi MiMo Tier 3 Aggregators: - SiliconFlow, SiliconFlow en, Nvidia --- src/config/openclawProviderPresets.ts | 486 ++++++++++++++++++++++++++ 1 file changed, 486 insertions(+) diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index c9fad48dc..6eb615797 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -142,6 +142,46 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ modelCatalog: { "zhipu/glm-4.7": { alias: "GLM" } }, }, }, + { + name: "Zhipu GLM en", + websiteUrl: "https://z.ai", + apiKeyUrl: "https://z.ai/subscribe?ic=8JVLJQFSKB", + settingsConfig: { + baseUrl: "https://api.z.ai/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "glm-4.7", + name: "GLM-4.7", + contextWindow: 128000, + cost: { input: 0.001, output: 0.001 }, + }, + ], + }, + category: "cn_official", + isPartner: true, + partnerPromotionKey: "zhipu", + icon: "zhipu", + iconColor: "#0F62FE", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: "https://api.z.ai/v1", + defaultValue: "https://api.z.ai/v1", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "zhipu-en/glm-4.7" }, + modelCatalog: { "zhipu-en/glm-4.7": { alias: "GLM" } }, + }, + }, { name: "Qwen Coder", websiteUrl: "https://bailian.console.aliyun.com", @@ -218,6 +258,44 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ modelCatalog: { "kimi/kimi-k2.5": { alias: "Kimi" } }, }, }, + { + name: "Kimi For Coding", + websiteUrl: "https://www.kimi.com/coding/docs/", + apiKeyUrl: "https://platform.moonshot.cn/console/api-keys", + settingsConfig: { + baseUrl: "https://api.kimi.com/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "kimi-for-coding", + name: "Kimi For Coding", + contextWindow: 131072, + cost: { input: 0.002, output: 0.006 }, + }, + ], + }, + category: "cn_official", + icon: "kimi", + iconColor: "#6366F1", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: "https://api.kimi.com/v1", + defaultValue: "https://api.kimi.com/v1", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "kimi-coding/kimi-for-coding" }, + modelCatalog: { "kimi-coding/kimi-for-coding": { alias: "Kimi" } }, + }, + }, { name: "MiniMax", websiteUrl: "https://platform.minimaxi.com", @@ -256,6 +334,222 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ modelCatalog: { "minimax/MiniMax-M2.1": { alias: "MiniMax" } }, }, }, + { + name: "MiniMax en", + websiteUrl: "https://platform.minimax.io", + apiKeyUrl: "https://platform.minimax.io/subscribe/coding-plan", + settingsConfig: { + baseUrl: "https://api.minimax.io/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "MiniMax-M2.1", + name: "MiniMax M2.1", + contextWindow: 200000, + cost: { input: 0.001, output: 0.004 }, + }, + ], + }, + category: "cn_official", + isPartner: true, + partnerPromotionKey: "minimax_en", + theme: { + backgroundColor: "#f64551", + textColor: "#FFFFFF", + }, + icon: "minimax", + iconColor: "#FF6B6B", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "minimax-en/MiniMax-M2.1" }, + modelCatalog: { "minimax-en/MiniMax-M2.1": { alias: "MiniMax" } }, + }, + }, + { + name: "KAT-Coder", + websiteUrl: "https://console.streamlake.ai", + apiKeyUrl: "https://console.streamlake.ai/console/api-key", + settingsConfig: { + baseUrl: + "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/openai", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "KAT-Coder-Pro", + name: "KAT-Coder Pro", + contextWindow: 128000, + cost: { input: 0.002, output: 0.006 }, + }, + ], + }, + category: "cn_official", + icon: "catcoder", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: + "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/openai", + defaultValue: + "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/openai", + editorValue: "", + }, + ENDPOINT_ID: { + label: "Endpoint ID", + placeholder: "", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "katcoder/KAT-Coder-Pro" }, + modelCatalog: { "katcoder/KAT-Coder-Pro": { alias: "KAT-Coder" } }, + }, + }, + { + name: "Longcat", + websiteUrl: "https://longcat.chat/platform", + apiKeyUrl: "https://longcat.chat/platform/api_keys", + settingsConfig: { + baseUrl: "https://api.longcat.chat/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "LongCat-Flash-Chat", + name: "LongCat Flash Chat", + contextWindow: 128000, + cost: { input: 0.001, output: 0.004 }, + }, + ], + }, + category: "cn_official", + icon: "longcat", + iconColor: "#29E154", + templateValues: { + baseUrl: { + label: "Base URL", + placeholder: "https://api.longcat.chat/v1", + defaultValue: "https://api.longcat.chat/v1", + editorValue: "", + }, + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "longcat/LongCat-Flash-Chat" }, + modelCatalog: { "longcat/LongCat-Flash-Chat": { alias: "LongCat" } }, + }, + }, + { + name: "DouBaoSeed", + websiteUrl: "https://www.volcengine.com/product/doubao", + apiKeyUrl: "https://www.volcengine.com/product/doubao", + settingsConfig: { + baseUrl: "https://ark.cn-beijing.volces.com/api/v3", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "doubao-seed-code-preview-latest", + name: "DouBao Seed Code Preview", + contextWindow: 128000, + cost: { input: 0.002, output: 0.006 }, + }, + ], + }, + category: "cn_official", + icon: "doubao", + iconColor: "#3370FF", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "doubaoseed/doubao-seed-code-preview-latest" }, + modelCatalog: { + "doubaoseed/doubao-seed-code-preview-latest": { alias: "DouBao" }, + }, + }, + }, + { + name: "BaiLing", + websiteUrl: "https://alipaytbox.yuque.com/sxs0ba/ling/get_started", + settingsConfig: { + baseUrl: "https://api.tbox.cn/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "Ling-1T", + name: "Ling 1T", + contextWindow: 128000, + cost: { input: 0.001, output: 0.004 }, + }, + ], + }, + category: "cn_official", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "bailing/Ling-1T" }, + modelCatalog: { "bailing/Ling-1T": { alias: "BaiLing" } }, + }, + }, + { + name: "Xiaomi MiMo", + websiteUrl: "https://platform.xiaomimimo.com", + apiKeyUrl: "https://platform.xiaomimimo.com/#/console/api-keys", + settingsConfig: { + baseUrl: "https://api.xiaomimimo.com/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "mimo-v2-flash", + name: "MiMo V2 Flash", + contextWindow: 128000, + cost: { input: 0.001, output: 0.004 }, + }, + ], + }, + category: "cn_official", + icon: "xiaomimimo", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "xiaomimimo/mimo-v2-flash" }, + modelCatalog: { "xiaomimimo/mimo-v2-flash": { alias: "MiMo" } }, + }, + }, // ========== Aggregators ========== { @@ -428,6 +722,106 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ modelCatalog: { "modelscope/ZhipuAI/GLM-4.7": { alias: "GLM" } }, }, }, + { + name: "SiliconFlow", + websiteUrl: "https://siliconflow.cn", + apiKeyUrl: "https://cloud.siliconflow.cn/me/account/ak", + settingsConfig: { + baseUrl: "https://api.siliconflow.cn/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "Pro/MiniMaxAI/MiniMax-M2.1", + name: "MiniMax M2.1", + contextWindow: 200000, + cost: { input: 0.001, output: 0.004 }, + }, + ], + }, + category: "aggregator", + icon: "siliconflow", + iconColor: "#6E29F6", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "siliconflow/Pro/MiniMaxAI/MiniMax-M2.1" }, + modelCatalog: { + "siliconflow/Pro/MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax" }, + }, + }, + }, + { + name: "SiliconFlow en", + websiteUrl: "https://siliconflow.com", + apiKeyUrl: "https://cloud.siliconflow.com/account/ak", + settingsConfig: { + baseUrl: "https://api.siliconflow.com/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "MiniMaxAI/MiniMax-M2.1", + name: "MiniMax M2.1", + contextWindow: 200000, + cost: { input: 0.001, output: 0.004 }, + }, + ], + }, + category: "aggregator", + icon: "siliconflow", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "siliconflow-en/MiniMaxAI/MiniMax-M2.1" }, + modelCatalog: { + "siliconflow-en/MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax" }, + }, + }, + }, + { + name: "Nvidia", + websiteUrl: "https://build.nvidia.com", + apiKeyUrl: "https://build.nvidia.com/settings/api-keys", + settingsConfig: { + baseUrl: "https://integrate.api.nvidia.com/v1", + apiKey: "", + api: "openai-completions", + models: [ + { + id: "moonshotai/kimi-k2.5", + name: "Kimi K2.5", + contextWindow: 131072, + cost: { input: 0.002, output: 0.006 }, + }, + ], + }, + category: "aggregator", + icon: "nvidia", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "nvapi-...", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { primary: "nvidia/moonshotai/kimi-k2.5" }, + modelCatalog: { "nvidia/moonshotai/kimi-k2.5": { alias: "Kimi" } }, + }, + }, // ========== Third Party Partners ========== { @@ -567,6 +961,98 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ }, }, }, + { + name: "RightCode", + websiteUrl: "https://www.right.codes", + apiKeyUrl: "https://www.right.codes/register?aff=CCSWITCH", + settingsConfig: { + baseUrl: "https://www.right.codes/claude", + apiKey: "", + api: "anthropic-messages", + models: [ + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + contextWindow: 200000, + cost: { input: 5, output: 25 }, + }, + ], + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "rightcode", + icon: "rc", + iconColor: "#E96B2C", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { + primary: "rightcode/claude-sonnet-4-5-20250929", + fallbacks: ["rightcode/claude-opus-4-6"], + }, + modelCatalog: { + "rightcode/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "rightcode/claude-opus-4-6": { alias: "Opus" }, + }, + }, + }, + { + name: "AICodeMirror", + websiteUrl: "https://www.aicodemirror.com", + apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3", + settingsConfig: { + baseUrl: "https://api.aicodemirror.com/api/claudecode", + apiKey: "", + api: "anthropic-messages", + models: [ + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + contextWindow: 200000, + cost: { input: 3, output: 15 }, + }, + { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + contextWindow: 200000, + cost: { input: 5, output: 25 }, + }, + ], + }, + category: "third_party", + isPartner: true, + partnerPromotionKey: "aicodemirror", + icon: "aicodemirror", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + suggestedDefaults: { + model: { + primary: "aicodemirror/claude-sonnet-4-5-20250929", + fallbacks: ["aicodemirror/claude-opus-4-6"], + }, + modelCatalog: { + "aicodemirror/claude-sonnet-4-5-20250929": { alias: "Sonnet" }, + "aicodemirror/claude-opus-4-6": { alias: "Opus" }, + }, + }, + }, // ========== Custom Template ========== { From 5a17a67b8b7bfc44f7f3298310e8cfce95282743 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 13 Feb 2026 14:40:34 +0800 Subject: [PATCH 50/51] fix: deduplicate OpenCodeModel import after rebase --- src/components/providers/forms/ProviderForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 2cfb91af8..36451b04f 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -16,6 +16,7 @@ import type { ClaudeApiFormat, OpenCodeModel, OpenCodeProviderConfig, + OpenClawModel, } from "@/types"; import { providerPresets, @@ -41,7 +42,6 @@ import { } from "@/config/openclawProviderPresets"; import { OpenCodeFormFields } from "./OpenCodeFormFields"; import { OpenClawFormFields } from "./OpenClawFormFields"; -import type { OpenCodeModel, OpenClawModel } from "@/types"; import type { UniversalProviderPreset } from "@/config/universalProviderPresets"; import { applyTemplateValues } from "@/utils/providerConfigUtils"; import { mergeProviderMeta } from "@/utils/providerMetaUtils"; From 7b20c17ea42753de481f35b2bf1fa74d12dcdb9d Mon Sep 17 00:00:00 2001 From: makoMako <48956204+zhu-jl18@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:35:20 +0800 Subject: [PATCH 51/51] =?UTF-8?q?fix(opencode):=20=E8=A1=A5=E9=BD=90=20ins?= =?UTF-8?q?tall.sh=20=E5=AE=89=E8=A3=85=E8=B7=AF=E5=BE=84=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=20(#988)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 OpenCode 路径扫描来源(按官方 install.sh 优先级): OPENCODE_INSTALL_DIR > XDG_BIN_DIR > ~/bin > ~/.opencode/bin 保留并增强 Go 安装路径扫描(~/go/bin、GOPATH/*/bin)。 区分单值环境变量(push_env_single_dir)与 path-list 环境变量 (extend_from_path_list),避免对 OPENCODE_INSTALL_DIR 等单值 变量误用 split_paths。 增加路径去重逻辑(push_unique_path),避免重复扫描。 增加跨平台可执行候选逻辑(tool_executable_candidates): Windows: .cmd / .exe / 裸命令 Unix: 裸命令 将 PATH 拼接提至外层循环,减少重复 syscall。 增加单元测试覆盖路径拼装、去重及 Windows 候选顺序。 Closes #958 Co-authored-by: Warp --- src-tauri/src/commands/misc.rs | 218 +++++++++++++++++++++++++++------ 1 file changed, 183 insertions(+), 35 deletions(-) diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index e7c21471d..f5bf331fc 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -313,6 +313,79 @@ fn try_get_version_wsl(_tool: &str, _distro: &str) -> (Option, Option, path: std::path::PathBuf) { + if path.as_os_str().is_empty() { + return; + } + + if !paths.iter().any(|existing| existing == &path) { + paths.push(path); + } +} + +fn push_env_single_dir(paths: &mut Vec, value: Option) { + if let Some(raw) = value { + push_unique_path(paths, std::path::PathBuf::from(raw)); + } +} + +fn extend_from_path_list( + paths: &mut Vec, + value: Option, + suffix: Option<&str>, +) { + if let Some(raw) = value { + for p in std::env::split_paths(&raw) { + let dir = match suffix { + Some(s) => p.join(s), + None => p, + }; + push_unique_path(paths, dir); + } + } +} + +/// OpenCode install.sh 路径优先级(见 https://github.com/anomalyco/opencode README): +/// $OPENCODE_INSTALL_DIR > $XDG_BIN_DIR > $HOME/bin > $HOME/.opencode/bin +/// 额外扫描 Go 安装路径(~/go/bin、$GOPATH/*/bin)。 +fn opencode_extra_search_paths( + home: &Path, + opencode_install_dir: Option, + xdg_bin_dir: Option, + gopath: Option, +) -> Vec { + let mut paths = Vec::new(); + + push_env_single_dir(&mut paths, opencode_install_dir); + push_env_single_dir(&mut paths, xdg_bin_dir); + + if !home.as_os_str().is_empty() { + push_unique_path(&mut paths, home.join("bin")); + push_unique_path(&mut paths, home.join(".opencode").join("bin")); + push_unique_path(&mut paths, home.join("go").join("bin")); + } + + extend_from_path_list(&mut paths, gopath, Some("bin")); + + paths +} + +fn tool_executable_candidates(tool: &str, dir: &Path) -> Vec { + #[cfg(target_os = "windows")] + { + vec![ + dir.join(format!("{tool}.cmd")), + dir.join(format!("{tool}.exe")), + dir.join(tool), + ] + } + + #[cfg(not(target_os = "windows"))] + { + vec![dir.join(tool)] + } +} + /// 扫描常见路径查找 CLI fn scan_cli_version(tool: &str) -> (Option, Option) { use std::process::Command; @@ -320,88 +393,99 @@ fn scan_cli_version(tool: &str) -> (Option, Option) { let home = dirs::home_dir().unwrap_or_default(); // 常见的安装路径(原生安装优先) - let mut search_paths: Vec = vec![ - home.join(".local/bin"), // Native install (official recommended) - home.join(".npm-global/bin"), - home.join("n/bin"), // n version manager - home.join(".volta/bin"), // Volta package manager - ]; + let mut search_paths: Vec = Vec::new(); + if !home.as_os_str().is_empty() { + push_unique_path(&mut search_paths, home.join(".local/bin")); + push_unique_path(&mut search_paths, home.join(".npm-global/bin")); + push_unique_path(&mut search_paths, home.join("n/bin")); + push_unique_path(&mut search_paths, home.join(".volta/bin")); + } #[cfg(target_os = "macos")] { - search_paths.push(std::path::PathBuf::from("/opt/homebrew/bin")); - search_paths.push(std::path::PathBuf::from("/usr/local/bin")); + push_unique_path( + &mut search_paths, + std::path::PathBuf::from("/opt/homebrew/bin"), + ); + push_unique_path( + &mut search_paths, + std::path::PathBuf::from("/usr/local/bin"), + ); } #[cfg(target_os = "linux")] { - search_paths.push(std::path::PathBuf::from("/usr/local/bin")); - search_paths.push(std::path::PathBuf::from("/usr/bin")); + push_unique_path( + &mut search_paths, + std::path::PathBuf::from("/usr/local/bin"), + ); + push_unique_path(&mut search_paths, std::path::PathBuf::from("/usr/bin")); } #[cfg(target_os = "windows")] { if let Some(appdata) = dirs::data_dir() { - search_paths.push(appdata.join("npm")); + push_unique_path(&mut search_paths, appdata.join("npm")); } - search_paths.push(std::path::PathBuf::from("C:\\Program Files\\nodejs")); + push_unique_path( + &mut search_paths, + std::path::PathBuf::from("C:\\Program Files\\nodejs"), + ); } - // 添加 fnm 路径支持 let fnm_base = home.join(".local/state/fnm_multishells"); if fnm_base.exists() { if let Ok(entries) = std::fs::read_dir(&fnm_base) { for entry in entries.flatten() { let bin_path = entry.path().join("bin"); if bin_path.exists() { - search_paths.push(bin_path); + push_unique_path(&mut search_paths, bin_path); } } } } - // 扫描 nvm 目录下的所有 node 版本 let nvm_base = home.join(".nvm/versions/node"); if nvm_base.exists() { if let Ok(entries) = std::fs::read_dir(&nvm_base) { for entry in entries.flatten() { let bin_path = entry.path().join("bin"); if bin_path.exists() { - search_paths.push(bin_path); + push_unique_path(&mut search_paths, bin_path); } } } } - // 添加 Go 路径支持 (opencode 使用 go install 安装) if tool == "opencode" { - search_paths.push(home.join("go/bin")); // go install 默认路径 - if let Ok(gopath) = std::env::var("GOPATH") { - search_paths.push(std::path::PathBuf::from(gopath).join("bin")); + let extra_paths = opencode_extra_search_paths( + &home, + std::env::var_os("OPENCODE_INSTALL_DIR"), + std::env::var_os("XDG_BIN_DIR"), + std::env::var_os("GOPATH"), + ); + + for path in extra_paths { + push_unique_path(&mut search_paths, path); } } - // 在每个路径中查找工具 + let current_path = std::env::var("PATH").unwrap_or_default(); + for path in &search_paths { - let tool_path = if cfg!(target_os = "windows") { - path.join(format!("{tool}.cmd")) - } else { - path.join(tool) - }; + #[cfg(target_os = "windows")] + let new_path = format!("{};{}", path.display(), current_path); - if tool_path.exists() { - // 构建 PATH 环境变量,确保 node 可被找到 - let current_path = std::env::var("PATH").unwrap_or_default(); + #[cfg(not(target_os = "windows"))] + let new_path = format!("{}:{}", path.display(), current_path); - #[cfg(target_os = "windows")] - let new_path = format!("{};{}", path.display(), current_path); - - #[cfg(not(target_os = "windows"))] - let new_path = format!("{}:{}", path.display(), current_path); + for tool_path in tool_executable_candidates(tool, path) { + if !tool_path.exists() { + continue; + } #[cfg(target_os = "windows")] let output = { - // 使用 cmd /C 包装执行,确保子进程也在隐藏的控制台中运行 Command::new("cmd") .args(["/C", &format!("\"{}\" --version", tool_path.display())]) .env("PATH", &new_path) @@ -971,3 +1055,67 @@ pub async fn set_window_theme(window: tauri::Window, theme: String) -> Result<() window.set_theme(tauri_theme).map_err(|e| e.to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn opencode_extra_search_paths_includes_install_and_fallback_dirs() { + let home = PathBuf::from("/home/tester"); + let install_dir = Some(std::ffi::OsString::from("/custom/opencode/bin")); + let xdg_bin_dir = Some(std::ffi::OsString::from("/xdg/bin")); + let gopath = + std::env::join_paths([PathBuf::from("/go/path1"), PathBuf::from("/go/path2")]).ok(); + + let paths = opencode_extra_search_paths(&home, install_dir, xdg_bin_dir, gopath); + + assert_eq!(paths[0], PathBuf::from("/custom/opencode/bin")); + assert_eq!(paths[1], PathBuf::from("/xdg/bin")); + assert!(paths.contains(&PathBuf::from("/home/tester/bin"))); + assert!(paths.contains(&PathBuf::from("/home/tester/.opencode/bin"))); + assert!(paths.contains(&PathBuf::from("/home/tester/go/bin"))); + assert!(paths.contains(&PathBuf::from("/go/path1/bin"))); + assert!(paths.contains(&PathBuf::from("/go/path2/bin"))); + } + + #[test] + fn opencode_extra_search_paths_deduplicates_repeated_entries() { + let home = PathBuf::from("/home/tester"); + let same_dir = Some(std::ffi::OsString::from("/same/path")); + + let paths = opencode_extra_search_paths(&home, same_dir.clone(), same_dir.clone(), None); + + let count = paths + .iter() + .filter(|path| **path == PathBuf::from("/same/path")) + .count(); + assert_eq!(count, 1); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn tool_executable_candidates_non_windows_uses_plain_binary_name() { + let dir = PathBuf::from("/usr/local/bin"); + let candidates = tool_executable_candidates("opencode", &dir); + + assert_eq!(candidates, vec![PathBuf::from("/usr/local/bin/opencode")]); + } + + #[cfg(target_os = "windows")] + #[test] + fn tool_executable_candidates_windows_includes_cmd_exe_and_plain_name() { + let dir = PathBuf::from("C:\\tools"); + let candidates = tool_executable_candidates("opencode", &dir); + + assert_eq!( + candidates, + vec![ + PathBuf::from("C:\\tools\\opencode.cmd"), + PathBuf::from("C:\\tools\\opencode.exe"), + PathBuf::from("C:\\tools\\opencode"), + ] + ); + } +}