refactor(provider): replace startup auto-import with manual import button

Remove the startup loop in lib.rs that auto-imported default providers
for Claude/Codex/Gemini. Move config snippet extraction logic into the
import_default_config command so it works when triggered manually.

Add an "Import Current Config" button to ProviderEmptyState, wired via
useMutation in ProviderList (shown only for standard apps). Update i18n
keys (zh/en/ja) with new button labels and revised empty state text.
This commit is contained in:
Jason
2026-02-20 22:01:18 +08:00
parent 5a72888852
commit 4c88174cb0
7 changed files with 81 additions and 59 deletions
+23 -1
View File
@@ -93,7 +93,29 @@ pub fn switch_provider(
}
fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
ProviderService::import_default_config(state, app_type)
let imported = ProviderService::import_default_config(state, app_type.clone())?;
if imported {
// Extract common config snippet (mirrors old startup logic in lib.rs)
if state
.db
.get_config_snippet(app_type.as_str())
.ok()
.flatten()
.is_none()
{
match ProviderService::extract_common_config_snippet(state, app_type.clone()) {
Ok(snippet) if !snippet.is_empty() && snippet != "{}" => {
let _ = state
.db
.set_config_snippet(app_type.as_str(), Some(snippet));
}
_ => {}
}
}
}
Ok(imported)
}
#[cfg_attr(not(feature = "test-hooks"), doc(hidden))]
+1 -46
View File
@@ -448,52 +448,7 @@ pub fn run() {
Err(e) => log::warn!("✗ Failed to read skills migration flag: {e}"),
}
// 2. 导入供应商配置(已有内置检查:该应用已有供应商则跳过
for app in [
crate::app_config::AppType::Claude,
crate::app_config::AppType::Codex,
crate::app_config::AppType::Gemini,
] {
match crate::services::provider::ProviderService::import_default_config(
&app_state,
app.clone(),
) {
Ok(true) => {
log::info!("✓ Imported default provider for {}", app.as_str());
// 首次运行:自动提取通用配置片段(仅当通用配置为空时)
if app_state
.db
.get_config_snippet(app.as_str())
.ok()
.flatten()
.is_none()
{
match crate::services::provider::ProviderService::extract_common_config_snippet(&app_state, app.clone()) {
Ok(snippet) if !snippet.is_empty() && snippet != "{}" => {
if let Err(e) = app_state.db.set_config_snippet(app.as_str(), Some(snippet)) {
log::warn!("✗ Failed to save common config snippet for {}: {e}", app.as_str());
} else {
log::info!("✓ Extracted common config snippet for {}", app.as_str());
}
}
Ok(_) => log::debug!("○ No common config to extract for {}", app.as_str()),
Err(e) => log::debug!("○ Failed to extract common config for {}: {e}", app.as_str()),
}
}
}
Ok(false) => {} // 已有供应商,静默跳过
Err(e) => {
log::debug!(
"○ No default provider to import for {}: {}",
app.as_str(),
e
);
}
}
}
// 2.1 OpenCode 供应商导入(累加式模式,需特殊处理)
// 2. OpenCode 供应商导入(累加式模式,需特殊处理
// OpenCode 与其他应用不同:配置文件中可同时存在多个供应商
// 需要遍历 provider 字段下的每个供应商并导入
match crate::services::provider::import_opencode_providers_from_live(&app_state) {
@@ -1,12 +1,16 @@
import { Users } from "lucide-react";
import { Download, Users } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
interface ProviderEmptyStateProps {
onCreate?: () => void;
onImport?: () => void;
}
export function ProviderEmptyState({ onCreate }: ProviderEmptyStateProps) {
export function ProviderEmptyState({
onCreate,
onImport,
}: ProviderEmptyStateProps) {
const { t } = useTranslation();
return (
@@ -18,11 +22,19 @@ export function ProviderEmptyState({ onCreate }: ProviderEmptyStateProps) {
<p className="mt-2 max-w-sm text-sm text-muted-foreground">
{t("provider.noProvidersDescription")}
</p>
{onCreate && (
<Button className="mt-6" onClick={onCreate}>
{t("provider.addProvider")}
</Button>
)}
<div className="mt-6 flex flex-col gap-2">
{onImport && (
<Button onClick={onImport}>
<Download className="mr-2 h-4 w-4" />
{t("provider.importCurrent")}
</Button>
)}
{onCreate && (
<Button variant={onImport ? "outline" : "default"} onClick={onCreate}>
{t("provider.addProvider")}
</Button>
)}
</div>
</div>
);
}
+29 -2
View File
@@ -15,7 +15,8 @@ import {
import { AnimatePresence, motion } from "framer-motion";
import { Search, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import type { Provider } from "@/types";
import type { AppId } from "@/lib/api";
import { providersApi } from "@/lib/api/providers";
@@ -179,6 +180,23 @@ export function ProviderList({
const [isSearchOpen, setIsSearchOpen] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
// Import current live config as default provider
const queryClient = useQueryClient();
const importMutation = useMutation({
mutationFn: () => providersApi.importDefault(appId),
onSuccess: (imported) => {
if (imported) {
queryClient.invalidateQueries({ queryKey: ["providers", appId] });
toast.success(t("provider.importCurrentDescription"));
} else {
toast.info(t("provider.noProviders"));
}
},
onError: (error: Error) => {
toast.error(error.message);
},
});
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const key = event.key.toLowerCase();
@@ -231,8 +249,17 @@ export function ProviderList({
);
}
// Only show import button for standard apps (not additive-mode apps like OpenCode/OpenClaw)
const showImportButton =
appId === "claude" || appId === "codex" || appId === "gemini";
if (sortedProviders.length === 0) {
return <ProviderEmptyState onCreate={onCreate} />;
return (
<ProviderEmptyState
onCreate={onCreate}
onImport={showImportButton ? () => importMutation.mutate() : undefined}
/>
);
}
const renderProviderList = () => (
+3 -1
View File
@@ -77,7 +77,9 @@
"tabProvider": "Provider",
"tabUniversal": "Universal",
"noProviders": "No providers added yet",
"noProvidersDescription": "Click the \"Add Provider\" button in the top right to configure your first API provider",
"noProvidersDescription": "Import your current live config below, or manually add a new provider",
"importCurrent": "Import Current Config",
"importCurrentDescription": "Import current live config as default provider",
"currentlyUsing": "Currently Using",
"enable": "Enable",
"inUse": "In Use",
+3 -1
View File
@@ -77,7 +77,9 @@
"tabProvider": "プロバイダー",
"tabUniversal": "統一プロバイダー",
"noProviders": "まだプロバイダーがありません",
"noProvidersDescription": "右上の「プロバイダーを追加」を押して最初の API プロバイダーを登録してください",
"noProvidersDescription": "下の「現在の設定をインポート」ボタンで既存の設定を取り込むか、新しいプロバイダーを手動で追加してください",
"importCurrent": "現在の設定をインポート",
"importCurrentDescription": "現在使用中の設定をデフォルトプロバイダーとしてインポート",
"currentlyUsing": "現在使用中",
"enable": "有効化",
"inUse": "使用中",
+3 -1
View File
@@ -77,7 +77,9 @@
"tabProvider": "供应商",
"tabUniversal": "统一供应商",
"noProviders": "还没有添加任何供应商",
"noProvidersDescription": "点击右上角的\"添加供应商\"按钮开始配置您的第一个API供应商",
"noProvidersDescription": "点击下方的\"导入当前配置\"按钮导入已有配置,或手动添加新的供应商",
"importCurrent": "导入当前配置",
"importCurrentDescription": "将当前正在使用的配置导入为默认供应商",
"currentlyUsing": "当前使用",
"enable": "启用",
"inUse": "使用中",