feat(easy-mode): improve API onboarding and agent configuration flow

- Fix management API 404 errors when saving API providers
- Simplify the model list and support custom model IDs
- Adjust API field order and default provider selection
- Reuse the full agent configuration page and restore agent detection
This commit is contained in:
lzt404 committed 2026-09-01 23:32:26 +08:00
1 parent 79b50b7a2b
commit dd008d64ba
6 files changed
+1777 -160

No files matched your search

+1 -2
View File
@@ -41,7 +41,6 @@ const pages = [
id: 'easy',
labelKey: 'app.nav.easy',
icon: Sparkles,
// The easy page is rendered separately so it can receive navigation callbacks.
component: HomePage,
},
{
@@ -419,7 +418,7 @@ function AppContent() {
<main className="content">
{isAlwaysAvailablePage(activePage.id) || coreRunning ? (
activePage.id === 'easy' ? (
<EasyModePage />
<EasyModePage onExit={() => select('home')} />
) : (
<ActivePage />
)
+39
View File
@@ -61,6 +61,45 @@ export const en: Record<MessageKey, string> = {
'easyMode.guide.step3Description': 'Choose a model from “Available models”, select an installed agent client, and apply the configuration.',
'easyMode.navigation.back': 'Previous',
'easyMode.navigation.next': 'Next',
'easyMode.status.ready': 'Ready',
'easyMode.status.connectedModels': '{count} available models detected',
'easyMode.status.noModelsYet': 'No models connected yet. Please sign in or add an API key first.',
'easyMode.oauth.loggedIn': 'Signed In',
'easyMode.oauth.relogin': 'Re-login',
'easyMode.oauth.loggingIn': 'Waiting for browser authorization...',
'easyMode.api.quickAdd': 'Quick Add API',
'easyMode.api.testAndSave': 'Test & Save',
'easyMode.api.testing': 'Testing connection...',
'easyMode.api.fetchModels': 'Fetch model list',
'easyMode.api.fetchingModels': 'Fetching model list...',
'easyMode.api.customModelId': 'Custom Model ID',
'easyMode.api.customModelPlaceholder': 'For example: gpt-5.6-sol or deepseek-v4-pro',
'easyMode.api.addModelId': 'Add Model ID',
'easyMode.api.credentialsRequired': 'Enter an API URL and API key first',
'easyMode.api.modelRequired': 'Fetch and select a model, or add a Model ID manually',
'easyMode.api.testSuccess': 'Connection successful! Found {count} models',
'easyMode.api.testFailed': 'Connection test failed',
'easyMode.api.platform': 'Platform',
'easyMode.api.baseUrl': 'API Base URL',
'easyMode.api.apiKey': 'API Key',
'easyMode.api.name': 'Remark Name (Optional)',
'easyMode.api.viewExisting': 'View all connected providers ({count})',
'easyMode.api.backToQuick': 'Back to Quick Add',
'easyMode.api.savedSuccess': 'API provider saved successfully!',
'easyMode.scenario.title': 'Choose how to use',
'easyMode.scenario.agentTab': 'Connect AI Agent (Recommended)',
'easyMode.scenario.apiTab': 'Copy Local API Endpoints',
'easyMode.agent.installedBadge': 'Installed on this machine',
'easyMode.agent.notDetected': 'Not detected',
'easyMode.agent.chooseModel': 'Select Model',
'easyMode.agent.applyConfig': '⚡ Apply Configuration',
'easyMode.agent.applySuccess': 'Configuration applied successfully!',
'easyMode.agent.launchClient': '🚀 Launch Client / Open Terminal',
'easyMode.agent.launching': 'Launching...',
'easyMode.complete.title': 'Setup Complete!',
'easyMode.complete.enterApp': 'Enter Dashboard',
'easyMode.apiCopy.desc': 'You can also use the local proxy URL in any OpenAI / Claude / Gemini compatible tool.',
'easyMode.apiCopy.copied': 'Copied to clipboard',
'error.render.title': 'Something went wrong while rendering this page',
'error.unknown': 'Unknown error',
'error.reload': 'Reload Page',
+41 -2
View File
@@ -37,7 +37,7 @@ export const zhCN = {
'easyMode.overview.coreTitle': '使用指导',
'easyMode.overview.providerTitle': '接入模型',
'easyMode.overview.agentTitle': '接入智能体',
'easyMode.setup.providerDescription': '可以通过 OAuth 登录自己的账号并使用账号额度,也可以通过 API 接入任意平台。',
'easyMode.setup.providerDescription': '可选择两种方式接入\n1.OAuth登录自己的订阅账号,使用自己账号的额度\n2.API URL的方式接入',
'easyMode.oauth.title': 'OAuth 登录',
'easyMode.oauth.description': '通过浏览器授权登录自己账号,并使用账号的额度。',
'easyMode.oauth.nextHint': '账号登录完成后,点击页面上方的“下一步”继续。',
@@ -52,13 +52,52 @@ export const zhCN = {
'easyMode.guide.title': '使用指导',
'easyMode.guide.description': '先确定连接方式,完成模型连接后,再从“可用模型”列表中选择智能体要使用的模型。',
'easyMode.guide.step1Title': '选择连接方式',
'easyMode.guide.step1Description': '可以通过 OAuth 登录自己的账号并使用账号额度,也可以通过 API 接入任意平台。',
'easyMode.guide.step1Description': '可选择两种方式接入\n1.OAuth登录自己的订阅账号,使用自己账号的额度\n2.API URL的方式接入',
'easyMode.guide.step2Title': '连接模型',
'easyMode.guide.step2Description': '这里可以通过 OAuth 完成账号授权,也可以填写第三方平台提供的 Base URL 和 API 密钥。保存后,模型会出现在“可用模型”列表中。',
'easyMode.guide.step3Title': '选择模型并接入智能体',
'easyMode.guide.step3Description': '在“可用模型”列表中选定模型,再选择已安装的智能体客户端并应用配置。',
'easyMode.navigation.back': '上一步',
'easyMode.navigation.next': '下一步',
'easyMode.status.ready': '已就绪',
'easyMode.status.connectedModels': '已检测到 {count} 个可用模型',
'easyMode.status.noModelsYet': '尚未连接模型,请先登录账号或添加 API',
'easyMode.oauth.loggedIn': '已登录',
'easyMode.oauth.relogin': '重新登录',
'easyMode.oauth.loggingIn': '等待浏览器授权...',
'easyMode.api.quickAdd': '快速接入 API',
'easyMode.api.testAndSave': '测试并保存',
'easyMode.api.testing': '正在测试连接...',
'easyMode.api.fetchModels': '获取模型列表',
'easyMode.api.fetchingModels': '正在获取模型列表...',
'easyMode.api.customModelId': '自定义 Model ID',
'easyMode.api.customModelPlaceholder': '例如:gpt-5.6-sol 或 deepseek-v4-pro',
'easyMode.api.addModelId': '添加 Model ID',
'easyMode.api.credentialsRequired': '请先填写 API URL 和 API 密钥',
'easyMode.api.modelRequired': '请先获取模型列表并选择模型,或手动添加 Model ID',
'easyMode.api.testSuccess': '连接成功!发现 {count} 个模型',
'easyMode.api.testFailed': '连接测试失败',
'easyMode.api.platform': '平台类型',
'easyMode.api.baseUrl': 'API Base URL',
'easyMode.api.apiKey': 'API 密钥 (API Key)',
'easyMode.api.name': '备注名称 (选填)',
'easyMode.api.viewExisting': '查看全部已接入列表 ({count})',
'easyMode.api.backToQuick': '返回快速接入',
'easyMode.api.savedSuccess': 'API 接入已保存!',
'easyMode.scenario.title': '选择你的使用方式',
'easyMode.scenario.agentTab': '接入 AI 智能体客户端 (推荐)',
'easyMode.scenario.apiTab': '复制本地 API 接口地址',
'easyMode.agent.installedBadge': '本机已安装',
'easyMode.agent.notDetected': '未检测到',
'easyMode.agent.chooseModel': '选择使用的模型',
'easyMode.agent.applyConfig': '⚡ 一键应用配置',
'easyMode.agent.applySuccess': '配置已成功应用!',
'easyMode.agent.launchClient': '🚀 启动客户端 / 打开终端',
'easyMode.agent.launching': '正在启动...',
'easyMode.complete.title': '配置就绪!',
'easyMode.complete.enterApp': '进入完整控制台',
'easyMode.apiCopy.desc': '你也可以直接将本地代理地址填入任何支持 OpenAI / Claude / Gemini 协议的工具中。',
'easyMode.apiCopy.copied': '已复制到剪贴板',
'error.render.title': '页面渲染出现异常',
'error.unknown': '未知错误',
'error.reload': '重新加载页面',
+6 -2
View File
@@ -629,7 +629,11 @@ function AgentModelPicker({
);
}
export function AgentsPage() {
type AgentsPageProps = {
embedded?: boolean;
};
export function AgentsPage({ embedded = false }: AgentsPageProps = {}) {
const { t } = useI18n();
const [selected, setSelected] = useState<AgentClientId>(readSelectedAgentClient);
const [activeSubpage, setActiveSubpage] = useState<AgentSubpageId>(DEFAULT_AGENT_SUBPAGE);
@@ -1442,7 +1446,7 @@ export function AgentsPage() {
) : oauthLoginRequiredAction ? t(`agents.oauthLoginRequired.${oauthLoginRequiredAction}Description`) : '';
return (
<section className="page management-page agents-page">
<section className={`page management-page agents-page${embedded ? ' agents-page-embedded' : ''}`}>
<header className="management-header">
<div>
<span>Agent Clients</span>
+678 -154
View File
@@ -1,198 +1,722 @@
import { useState } from 'react';
import { useI18n } from '../i18n';
import { ApiAccessPage } from './ApiAccessPage';
import { AgentsPage } from './AgentsPage';
import { OAuthLoginPage } from './ManagementPages';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { invoke } from "@tauri-apps/api/core";
import {
AlertCircle,
ArrowLeft,
ArrowRight,
Check,
CheckCircle2,
LoaderCircle,
Plus,
RefreshCw,
} from "lucide-react";
import { useI18n } from "../i18n";
import {
isRecord,
managementApi,
readBoolean,
readString,
responseList,
} from "../services/managementApi";
import {
fetchModels,
normalizeBaseUrl,
type ModelOption,
type ModelProvider,
} from "../services/modelService";
import { AgentsPage } from "./AgentsPage";
type AuthMethod = 'oauth' | 'api';
import codexIcon from "../assets/icons/codex.svg";
import claudeIcon from "../assets/icons/claude.svg";
import antigravityIcon from "../assets/icons/antigravity.svg";
import kimiIcon from "../assets/icons/kimi-light.svg";
import grokIcon from "../assets/icons/grok.svg";
import openaiIcon from "../assets/icons/openai-light.svg";
import deepseekIcon from "../assets/icons/deepseek.svg";
import geminiIcon from "../assets/icons/gemini.svg";
type AuthMethod = "oauth" | "api";
type SetupStep = 1 | 2;
type SetupView = 1 | 'oauth' | 'api' | 'agents';
const EASY_MODE_STATE_KEY = 'easy-cli-proxy-api.easy-mode.state';
type OAuthProviderId = "codex" | "claude" | "antigravity" | "kimi" | "xai";
type SavedEasyModeState = {
authMethod: AuthMethod;
providerReturnView: 1 | AuthMethod | null;
type OAuthProviderInfo = {
id: OAuthProviderId;
name: string;
icon: string;
description: string;
};
const defaultEasyModeState: SavedEasyModeState = {
authMethod: 'oauth',
providerReturnView: null,
const oauthProviders: OAuthProviderInfo[] = [
{ id: "codex", name: "Codex OAuth", icon: codexIcon, description: "OpenAI / ChatGPT 账号授权登录" },
{ id: "claude", name: "Claude OAuth", icon: claudeIcon, description: "Anthropic / Claude 账号授权登录" },
{ id: "antigravity", name: "Antigravity OAuth", icon: antigravityIcon, description: "Antigravity 账号授权登录" },
{ id: "kimi", name: "Kimi OAuth", icon: kimiIcon, description: "Moonshot / Kimi 账号授权登录" },
{ id: "xai", name: "xAI OAuth", icon: grokIcon, description: "xAI / Grok 账号授权登录" },
];
type ApiSection = "openai-compatibility" | "deepseek" | "claude" | "gemini" | "codex";
type ApiManagementSection = "openai-compatibility" | "claude-api-key" | "codex-api-key" | "gemini-api-key";
type ApiSectionOption = {
id: ApiSection;
managementSection: ApiManagementSection;
name: string;
provider: ModelProvider;
defaultBaseUrl: string;
icon: string;
};
function readSavedState(): SavedEasyModeState {
try {
const raw = window.localStorage.getItem(EASY_MODE_STATE_KEY);
if (!raw) return defaultEasyModeState;
const parsed = JSON.parse(raw) as {
authMethod?: unknown;
providerReturnView?: unknown;
};
return {
authMethod: parsed.authMethod === 'api' ? 'api' : 'oauth',
providerReturnView: parsed.providerReturnView === 1
|| parsed.providerReturnView === 'oauth'
|| parsed.providerReturnView === 'api'
? parsed.providerReturnView
: null,
};
} catch {
return defaultEasyModeState;
}
}
const apiSectionOptions: ApiSectionOption[] = [
{ id: "openai-compatibility", managementSection: "openai-compatibility", name: "OpenAI 格式", provider: "openai", defaultBaseUrl: "", icon: openaiIcon },
{ id: "claude", managementSection: "claude-api-key", name: "Anthropic 格式", provider: "claude", defaultBaseUrl: "", icon: claudeIcon },
{ id: "codex", managementSection: "codex-api-key", name: "Codex API", provider: "codex", defaultBaseUrl: "", icon: codexIcon },
{ id: "gemini", managementSection: "gemini-api-key", name: "Gemini 格式", provider: "gemini", defaultBaseUrl: "", icon: geminiIcon },
{ id: "deepseek", managementSection: "openai-compatibility", name: "DeepSeek", provider: "openai", defaultBaseUrl: "https://api.deepseek.com", icon: deepseekIcon },
];
function saveState(state: SavedEasyModeState) {
try {
window.localStorage.setItem(EASY_MODE_STATE_KEY, JSON.stringify(state));
} catch {
// Keep the selection in memory when persistent storage is unavailable.
}
}
const isDeepSeekRecord = (record: Record<string, unknown>) => {
const name = readString(record, "name").trim().toLowerCase();
const baseUrl = readString(record, "base-url", "baseUrl").trim().toLowerCase();
return name.includes("deepseek") || /^https?:\/\/api\.deepseek\.com(?:\/|$)/i.test(baseUrl);
};
export function EasyModePage() {
export function EasyModePage({
onExit,
}: {
onExit?: () => void;
}) {
const { t } = useI18n();
const initialState = readSavedState();
const [authMethod, setAuthMethod] = useState<AuthMethod>(initialState.authMethod);
const [providerReturnView, setProviderReturnView] = useState<SavedEasyModeState['providerReturnView']>(
initialState.providerReturnView,
);
const [view, setView] = useState<SetupView>(1);
const chooseAuthMethod = (method: AuthMethod) => {
setAuthMethod(method);
saveState({ authMethod: method, providerReturnView });
const [activeStep, setActiveStep] = useState<SetupStep>(1);
const [authMethod, setAuthMethod] = useState<AuthMethod>("oauth");
const [loadingSources, setLoadingSources] = useState(true);
const [authFiles, setAuthFiles] = useState<Record<string, unknown>[]>([]);
const [apiCounts, setApiCounts] = useState<Record<ApiSection, number>>({
"openai-compatibility": 0,
deepseek: 0,
claude: 0,
gemini: 0,
codex: 0,
});
const [availableModels, setAvailableModels] = useState<ModelOption[]>([]);
const [oauthLoggingIn, setOauthLoggingIn] = useState<OAuthProviderId | null>(null);
const [oauthNotice, setOauthNotice] = useState<{
tone: "success" | "error" | "info";
message: string;
} | null>(null);
const oauthPollTimer = useRef<number | null>(null);
const [selectedApiSection, setSelectedApiSection] = useState<ApiSection>("openai-compatibility");
const [apiBaseUrl, setApiBaseUrl] = useState("");
const [apiKey, setApiKey] = useState("");
const [apiRemark, setApiRemark] = useState("");
const [apiTesting, setApiTesting] = useState(false);
const [apiTestedModels, setApiTestedModels] = useState<ModelOption[]>([]);
const [apiSelectedModels, setApiSelectedModels] = useState<ModelOption[]>([]);
const [apiCustomModelId, setApiCustomModelId] = useState("");
const [apiTestError, setApiTestError] = useState("");
const [apiSaving, setApiSaving] = useState(false);
const [apiSaveNotice, setApiSaveNotice] = useState<string | null>(null);
const refreshSourceStatus = useCallback(async () => {
setLoadingSources(true);
try {
const authFilesPayload = await managementApi.get("/auth-files");
const files = responseList(authFilesPayload, "files");
setAuthFiles(files);
const counts: Record<ApiSection, number> = {
"openai-compatibility": 0,
deepseek: 0,
claude: 0,
gemini: 0,
codex: 0,
};
const allFetchedModels: ModelOption[] = [];
const configPayload = await managementApi.get("/config");
const recordsBySection: Record<ApiManagementSection, Record<string, unknown>[]> = {
"openai-compatibility": responseList(configPayload, "openai-compatibility"),
"claude-api-key": responseList(configPayload, "claude-api-key"),
"codex-api-key": responseList(configPayload, "codex-api-key"),
"gemini-api-key": responseList(configPayload, "gemini-api-key"),
};
for (const section of apiSectionOptions) {
const sourceList = recordsBySection[section.managementSection];
const list = section.id === "deepseek"
? sourceList.filter(isDeepSeekRecord)
: section.id === "openai-compatibility"
? sourceList.filter((record) => !isDeepSeekRecord(record))
: sourceList;
counts[section.id] = list.length;
list.forEach((item) => {
if (Array.isArray(item.models)) {
item.models.forEach((m) => {
const name = typeof m === "string" ? m : readString(m, "name", "id");
if (name && !allFetchedModels.some((x) => x.name === name)) {
allFetchedModels.push({ name });
}
});
}
});
}
setApiCounts(counts);
setAvailableModels(allFetchedModels);
} catch (e) {
console.warn("Failed to refresh source status", e);
} finally {
setLoadingSources(false);
}
}, []);
useEffect(() => {
void refreshSourceStatus();
return () => {
if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current);
};
}, []);
useEffect(() => {
if (!oauthNotice) return undefined;
const timer = window.setTimeout(() => {
setOauthNotice(null);
}, 4500);
return () => window.clearTimeout(timer);
}, [oauthNotice]);
const handleStartOAuth = async (provider: OAuthProviderId) => {
if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current);
setOauthLoggingIn(provider);
setOauthNotice(null);
try {
const result = await invoke<{
url?: string;
state?: string;
opened?: boolean;
openError?: string;
}>("start_oauth_login", {
provider,
browser: "default",
});
if (!result.state) {
setOauthNotice({
tone: "error",
message: "获取授权状态失败,请重试",
});
setOauthLoggingIn(null);
return;
}
const stateKey = result.state;
oauthPollTimer.current = window.setInterval(async () => {
try {
const pollRes = await invoke<{ status: string; error?: string }>(
"get_oauth_status",
{ state: stateKey },
);
const status = (pollRes.status || "").toLowerCase();
if (status === "ok") {
if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current);
setOauthLoggingIn(null);
setOauthNotice({
tone: "success",
message: "登录成功!已成功授权并获取账号额度。",
});
void refreshSourceStatus();
} else if (status === "error") {
if (oauthPollTimer.current) window.clearInterval(oauthPollTimer.current);
setOauthLoggingIn(null);
setOauthNotice({
tone: "error",
message: pollRes.error ? `授权失败: ${pollRes.error}` : "授权失败,请重试",
});
}
} catch {}
}, 1500);
} catch (err) {
setOauthLoggingIn(null);
setOauthNotice({
tone: "error",
message: String(err),
});
}
};
const handleApiSectionChange = (sec: ApiSection) => {
setSelectedApiSection(sec);
const opt = apiSectionOptions.find((o) => o.id === sec);
if (opt) setApiBaseUrl(opt.defaultBaseUrl);
setApiTestedModels([]);
setApiSelectedModels([]);
setApiCustomModelId("");
setApiTestError("");
setApiSaveNotice(null);
};
const openSelectedProvider = () => {
setView(authMethod);
};
const handleTestApi = async () => {
if (!apiBaseUrl.trim()) {
setApiTestError("请先填写 API URL");
return;
}
setApiTesting(true);
setApiTestError("");
setApiTestedModels([]);
const leaveProviderForAgents = () => {
const currentProviderView: 1 | AuthMethod = view === 1 || view === 'oauth' || view === 'api'
? view
: providerReturnView ?? 1;
setProviderReturnView(currentProviderView);
saveState({ authMethod, providerReturnView: currentProviderView });
setView('agents');
};
const opt = apiSectionOptions.find((o) => o.id === selectedApiSection);
const providerType = opt ? opt.provider : "openai";
const previousView: SetupView | null = view === 'oauth' || view === 'api'
? 1
: view === 'agents'
? providerReturnView ?? 1
: null;
const continueSetup = () => {
if (view === 1) setView(authMethod);
else if (view === 'oauth' || view === 'api') leaveProviderForAgents();
};
const openProgressStep = (step: SetupStep) => {
if (step === 1) {
setView(view === 'agents' ? providerReturnView ?? 1 : view === 'oauth' || view === 'api' ? view : 1);
} else {
leaveProviderForAgents();
try {
const models = await fetchModels(
providerType,
apiBaseUrl.trim(),
apiKey.trim(),
undefined,
{},
10000,
);
if (models.length > 0) {
setApiTestedModels(models);
setApiSelectedModels((current) => {
const fetchedKeys = new Set(models.map((model) => model.name.trim().toLowerCase()));
const customModels = current.filter((model) => !fetchedKeys.has(model.name.trim().toLowerCase()));
return [...models, ...customModels];
});
} else {
setApiTestError("未获取到任何模型,请检查 API URL 和 API 密钥");
}
} catch (err) {
setApiTestError(String(err));
} finally {
setApiTesting(false);
}
};
const stepTitles = [
t('easyMode.overview.providerTitle'),
t('easyMode.overview.agentTitle'),
];
const handleToggleApiModel = (model: ModelOption) => {
const key = model.name.trim().toLowerCase();
if (!key) return;
setApiSelectedModels((current) => {
const selected = current.some((item) => item.name.trim().toLowerCase() === key);
return selected
? current.filter((item) => item.name.trim().toLowerCase() !== key)
: [...current, model];
});
};
const handleAddCustomModel = () => {
const name = apiCustomModelId.trim();
if (!name) return;
const key = name.toLowerCase();
setApiSelectedModels((current) => {
if (current.some((model) => model.name.trim().toLowerCase() === key)) return current;
const fetchedModel = apiTestedModels.find((model) => model.name.trim().toLowerCase() === key);
return [...current, fetchedModel ?? { name }];
});
setApiCustomModelId("");
setApiTestError("");
};
const handleSaveApi = async () => {
if (!apiBaseUrl.trim()) {
setApiTestError("请先填写 API URL");
return;
}
if (apiSelectedModels.length === 0) {
setApiTestError(t("easyMode.api.modelRequired"));
return;
}
setApiSaving(true);
setApiSaveNotice(null);
setApiTestError("");
try {
const selectedOption = apiSectionOptions.find((option) => option.id === selectedApiSection);
const managementSection = selectedOption?.managementSection ?? "openai-compatibility";
const configPayload = await managementApi.get("/config");
const list = responseList(configPayload, managementSection);
const models = apiSelectedModels.map((model) => ({ name: model.name.trim() }));
const newEntry = managementSection === "openai-compatibility"
? {
name: apiRemark.trim() || `${selectedApiSection} (${list.length + 1})`,
"base-url": normalizeBaseUrl(apiBaseUrl.trim()),
"api-key-entries": [
{ "api-key": apiKey.trim() },
],
models,
}
: {
"api-key": apiKey.trim(),
"base-url": normalizeBaseUrl(apiBaseUrl.trim()),
models,
};
await managementApi.put(`/${managementSection}`, [...list, newEntry]);
setApiSaveNotice("API 接入已保存成功!");
void refreshSourceStatus();
} catch (err) {
setApiTestError(String(err));
} finally {
setApiSaving(false);
}
};
const isOAuthLoggedIn = (providerId: OAuthProviderId) => {
const norm = providerId === "claude" ? "claude" : providerId === "codex" ? "codex" : providerId;
return authFiles.some((f) => {
const p = readString(f, "provider", "type").toLowerCase();
return p.includes(norm) || (norm === "codex" && p.includes("openai")) || (norm === "claude" && p.includes("anthropic"));
});
};
const totalLoggedInOAuth = oauthProviders.filter((p) => isOAuthLoggedIn(p.id)).length;
const totalApiProviders = Object.values(apiCounts).reduce((a, b) => a + b, 0);
const hasConnectedSource = totalLoggedInOAuth > 0 || totalApiProviders > 0;
const displayedApiModels = useMemo(() => {
const fetchedKeys = new Set(apiTestedModels.map((model) => model.name.trim().toLowerCase()));
return [
...apiTestedModels,
...apiSelectedModels.filter((model) => !fetchedKeys.has(model.name.trim().toLowerCase())),
];
}, [apiSelectedModels, apiTestedModels]);
return (
<section className="page simple-mode-page">
<div className="simple-mode-progress" aria-label={t('easyMode.steps.label')}>
{stepTitles.map((title, index) => {
const step = (index + 1) as SetupStep;
const active = step === 1
? view === 1 || view === 'oauth' || view === 'api'
: view === 'agents';
return (
<button
type="button"
key={title}
className={`simple-mode-progress-item${active ? ' active' : ''}`}
aria-current={active ? 'step' : undefined}
onClick={() => openProgressStep(step)}
>
<span>
<strong>{title}</strong>
</span>
</button>
);
})}
</div>
<div className="simple-mode-flow-navigation">
{previousView !== null ? (
<button
type="button"
className="secondary-button"
onClick={() => setView(previousView)}
>
{t('easyMode.navigation.back')}
</button>
) : null}
{view !== 'agents' ? (
<button
type="button"
className="primary-button"
onClick={continueSetup}
>
{t('easyMode.navigation.next')}
</button>
) : null}
</div>
{view === 'oauth' || view === 'api' ? (
<div className="simple-mode-provider-hint" role="note">
{t(view === 'oauth' ? 'easyMode.oauth.nextHint' : 'easyMode.api.nextHint')}
</div>
) : null}
{view === 1 ? (
{/* 第一步:接入模型 */}
{activeStep === 1 ? (
<section className="panel simple-mode-task">
<div className="simple-mode-task-heading">
<div>
<h2>{t('easyMode.overview.providerTitle')}</h2>
<p>{t('easyMode.setup.providerDescription')}</p>
<h2>{t("easyMode.overview.providerTitle")}</h2>
<p>{t("easyMode.setup.providerDescription")}</p>
</div>
</div>
<div className="simple-mode-choice-grid">
<button
type="button"
className={`simple-mode-choice${authMethod === 'oauth' ? ' selected' : ''}`}
aria-pressed={authMethod === 'oauth'}
onClick={() => chooseAuthMethod('oauth')}
className={`simple-mode-choice${authMethod === "oauth" ? " selected" : ""}`}
aria-pressed={authMethod === "oauth"}
onClick={() => setAuthMethod("oauth")}
>
<strong>{t('easyMode.oauth.title')}</strong>
<span>{t('easyMode.oauth.description')}</span>
<div className="simple-mode-choice-heading">
<div className="simple-mode-choice-title">
<strong>{t("easyMode.oauth.title")}</strong>
{totalLoggedInOAuth > 0 ? (
<span className="state-pill success" style={{ fontSize: "12px" }}>
已登录 {totalLoggedInOAuth} 个账号
</span>
) : (
<span className="state-pill neutral" style={{ fontSize: "12px" }}>推荐</span>
)}
</div>
</div>
<span>{t("easyMode.oauth.description")}</span>
<small>支持 Codex、Claude、Antigravity、Kimi、xAI</small>
</button>
<button
type="button"
className={`simple-mode-choice${authMethod === 'api' ? ' selected' : ''}`}
aria-pressed={authMethod === 'api'}
onClick={() => chooseAuthMethod('api')}
className={`simple-mode-choice${authMethod === "api" ? " selected" : ""}`}
aria-pressed={authMethod === "api"}
onClick={() => setAuthMethod("api")}
>
<strong>{t('easyMode.api.title')}</strong>
<span>{t('easyMode.api.description')}</span>
<div className="simple-mode-choice-heading">
<div className="simple-mode-choice-title">
<strong>{t("easyMode.api.title")}</strong>
{totalApiProviders > 0 ? (
<span className="state-pill success" style={{ fontSize: "12px" }}>
已接入 {totalApiProviders} 个平台
</span>
) : null}
</div>
</div>
<span>{t("easyMode.api.description")}</span>
<small>支持 OpenAI、Anthropic、Codex、Gemini、DeepSeek</small>
</button>
</div>
{authMethod === "oauth" ? (
<div className="simple-mode-embedded-box">
{oauthNotice ? (
<div
className={`config-toast ${oauthNotice.tone}`}
>
{oauthNotice.tone === "success" ? <CheckCircle2 size={16} /> : <AlertCircle size={16} />}
<span>{oauthNotice.message}</span>
</div>
) : null}
<div className="simple-mode-provider-grid">
{oauthProviders.map((provider) => {
const loggedIn = isOAuthLoggedIn(provider.id);
const isLogging = oauthLoggingIn === provider.id;
return (
<article
key={provider.id}
className={`client-api-card simple-mode-provider-card${loggedIn ? " connected" : ""}`}
>
<div className="simple-mode-provider-card-head">
<span className="client-api-logo simple-mode-provider-logo">
<img src={provider.icon} alt="" />
</span>
<div className="simple-mode-provider-copy">
<strong>{provider.name}</strong>
<span>
{provider.description}
</span>
</div>
</div>
<div className="simple-mode-provider-card-foot">
{loggedIn ? (
<span className="state-pill success" style={{ fontSize: "12px" }}>
<Check size={13} style={{ marginRight: 4 }} />
{t("easyMode.oauth.loggedIn")}
</span>
) : (
<span className="state-pill neutral" style={{ fontSize: "12px" }}>未授权</span>
)}
<button
type="button"
className={loggedIn ? "secondary-button" : "primary-button"}
disabled={isLogging}
onClick={() => void handleStartOAuth(provider.id)}
>
{isLogging ? (
<>
<LoaderCircle size={14} className="spin" style={{ marginRight: 6 }} />
{t("easyMode.oauth.loggingIn")}
</>
) : loggedIn ? (
t("easyMode.oauth.relogin")
) : (
"开始登录"
)}
</button>
</div>
</article>
);
})}
</div>
</div>
) : null}
{authMethod === "api" ? (
<div className="simple-mode-embedded-box">
{apiSaveNotice ? (
<div
className="config-toast success"
style={{ position: "static", transform: "none", margin: 0 }}
>
<CheckCircle2 size={16} />
<span>{apiSaveNotice}</span>
</div>
) : null}
{apiTestError ? (
<div
className="config-toast error"
style={{ position: "static", transform: "none", margin: 0 }}
>
<AlertCircle size={16} />
<span>{apiTestError}</span>
</div>
) : null}
<div className="simple-mode-api-form">
<div className="simple-mode-api-platforms">
{apiSectionOptions.map((opt) => (
<button
type="button"
key={opt.id}
className={`secondary-button simple-mode-api-platform${selectedApiSection === opt.id ? " active" : ""}`}
onClick={() => handleApiSectionChange(opt.id)}
>
<img className="simple-mode-api-platform-icon" src={opt.icon} alt="" />
{opt.name}
</button>
))}
</div>
<div className="simple-mode-field">
<label>
{t("easyMode.api.name")}
</label>
<input
type="text"
className="text-input"
value={apiRemark}
onChange={(e) => setApiRemark(e.target.value)}
placeholder="例如:主力 DeepSeek V3、中转 API"
/>
</div>
<div className="simple-mode-api-fields">
<div className="simple-mode-field">
<label>
{t("easyMode.api.baseUrl")}
</label>
<input
type="text"
className="text-input"
value={apiBaseUrl}
onChange={(e) => setApiBaseUrl(e.target.value)}
placeholder="https://..."
/>
</div>
<div className="simple-mode-field">
<label>
{t("easyMode.api.apiKey")}
</label>
<input
type="password"
className="text-input"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-..."
/>
</div>
</div>
<div className="simple-mode-api-model-card">
<div className="simple-mode-api-model-toolbar">
<input
type="text"
className="text-input"
value={apiCustomModelId}
onChange={(e) => setApiCustomModelId(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddCustomModel();
}
}}
placeholder={t("easyMode.api.customModelPlaceholder")}
aria-label={t("easyMode.api.customModelId")}
/>
<button
type="button"
className="secondary-button"
disabled={!apiCustomModelId.trim()}
onClick={handleAddCustomModel}
>
<Plus size={14} />
{t("easyMode.api.addModelId")}
</button>
<button
type="button"
className="secondary-button"
disabled={apiTesting || !apiBaseUrl.trim()}
onClick={() => void handleTestApi()}
>
{apiTesting ? <LoaderCircle size={14} className="spin" /> : <RefreshCw size={14} />}
{apiTesting ? t("easyMode.api.fetchingModels") : t("easyMode.api.fetchModels")}
</button>
</div>
{displayedApiModels.length > 0 ? (
<div className="simple-mode-api-model-options">
{displayedApiModels.map((model) => {
const key = model.name.trim().toLowerCase();
const selected = apiSelectedModels.some(
(item) => item.name.trim().toLowerCase() === key,
);
return (
<label
className={`simple-mode-api-model-option${selected ? " selected" : ""}`}
key={model.name}
>
<input
type="checkbox"
checked={selected}
onChange={() => handleToggleApiModel(model)}
/>
<span title={model.name}>{model.name}</span>
</label>
);
})}
</div>
) : null}
</div>
<div className="simple-mode-api-actions">
<button
type="button"
className="primary-button"
disabled={apiSaving || !apiBaseUrl.trim() || apiSelectedModels.length === 0}
onClick={() => void handleSaveApi()}
>
{apiSaving ? <LoaderCircle size={14} className="spin" /> : <Plus size={14} />}
保存并接入
</button>
</div>
</div>
</div>
) : null}
<div className="simple-mode-task-footer" style={{ marginTop: 10 }}>
<div className="simple-mode-selection">
{hasConnectedSource ? (
<span style={{ color: "var(--ui-accent-strong)", fontWeight: 600 }}>
{t("easyMode.status.connectedModels", { count: Math.max(availableModels.length, 1) })}
</span>
) : (
<span className="muted">{t("easyMode.status.noModelsYet")}</span>
)}
</div>
<button
type="button"
className="primary-button"
style={{ minHeight: 42, padding: "0 22px", fontSize: "15px" }}
disabled={!hasConnectedSource}
onClick={() => setActiveStep(2)}
>
{t("easyMode.navigation.next")}: 配置智能体
<ArrowRight size={16} style={{ marginLeft: 6 }} />
</button>
</div>
</section>
) : null}
{view === 'oauth' || view === 'api' ? (
<section className="panel simple-mode-embedded-task">
<div className="simple-mode-embedded-content">
{view === 'oauth' ? <OAuthLoginPage /> : <ApiAccessPage />}
</div>
</section>
) : null}
{/* 第二步:接入智能体 */}
{activeStep === 2 ? (
<section className="panel simple-mode-task">
<AgentsPage embedded />
{view === 'agents' ? (
<section className="panel simple-mode-embedded-task">
<div className="simple-mode-embedded-content">
<AgentsPage />
<div className="simple-mode-task-footer" style={{ marginTop: 14 }}>
<button
type="button"
className="secondary-button"
onClick={() => setActiveStep(1)}
>
<ArrowLeft size={16} style={{ marginRight: 6 }} />
{t("easyMode.navigation.back")}
</button>
<button
type="button"
className="primary-button"
style={{ minHeight: 42, padding: "0 22px", fontSize: "15px" }}
onClick={() => onExit?.()}
>
<CheckCircle2 size={16} style={{ marginRight: 6 }} />
{t("easyMode.complete.enterApp")}
</button>
</div>
</section>
) : null}
+1012
View File
File diff suppressed because it is too large. Load diff