import { useEffect, useMemo, useState } from "react"; import { api, type Model, type ProviderSelection, type TabSettings } from "../api"; import { CursorCaGate, CursorCaProvider, CursorModelGate, CursorModelProvider } from "../components/cursor/CursorGates"; import { CursorModelEditor, emptyCursorModelDraft, type CursorModelDraft } from "../components/cursor/CursorModelEditor"; import { CursorModelTestResult, type CursorModelTestState } from "../components/cursor/CursorModelTestResult"; import { TabSettingsCard } from "../components/cursor/TabSettingsCard"; import styles from "../components/cursor/CursorSettings.module.scss"; import { PageContent } from "../components/layout/PageContent"; import controls from "../components/ui/Controls.module.scss"; import { Icon } from "../components/ui/Icon"; import { Modal } from "../components/ui/Modal"; import { TitledCard } from "../components/ui/TitledCard"; import { TooltipTrigger } from "../components/ui/TooltipTrigger"; import { addIcon, claudeIcon, editIcon, openAiIcon, trashIcon } from "../components/ui/icons"; import { useMessage } from "../components/ui/message"; import { PageActions } from "../layouts/PageActions"; import { appStore, useAppStore } from "../store/appStore"; export function CursorSettingsPage() { const { providers, models, cursorHarness, cursorBusy } = useAppStore(); const message = useMessage(); const [draft, setDraft] = useState(null); const [editing, setEditing] = useState(null); const [modelOptions, setModelOptions] = useState([]); const [discovering, setDiscovering] = useState(false); const [caCommand, setCaCommand] = useState(null); const [waitingForCaRefresh, setWaitingForCaRefresh] = useState(false); const [deleting, setDeleting] = useState(null); const [tabDraft, setTabDraft] = useState(null); const [savingTab, setSavingTab] = useState(false); const [testingModelHashes, setTestingModelHashes] = useState>(() => new Set()); const [modelTestResults, setModelTestResults] = useState>(() => new Map()); const [savingAndTesting, setSavingAndTesting] = useState(false); const [batchTesting, setBatchTesting] = useState(false); const grouped = useMemo(() => providers.map((provider) => ({ provider, models: models.filter((model) => model.provider_id === provider.provider_id) })).filter((group) => group.models.length > 0), [providers, models]); const caReady = cursorHarness?.ca === "ready"; useEffect(() => { if (!caCommand) return; void api.copyCursorText(caCommand); }, [caCommand]); useEffect(() => { void api.tabSettings() .then(setTabDraft) .catch((cause) => message(cause instanceof Error ? cause.message : String(cause))); }, [message]); const initializeCa = async () => { const status = await appStore.initializeCursorCa(); if (status?.ca === "untrusted" && status.ca_install_command) setCaCommand(status.ca_install_command); }; const openNew = () => { setEditing(null); setModelOptions([]); setDraft(emptyCursorModelDraft()); }; const openEdit = (model: Model) => { const next = emptyCursorModelDraft(); next.providerMode = String(model.provider_id); next.model = { model_id: model.model_id, display_name: model.display_name, enabled: model.enabled, sort_order: model.sort_order, endpoint_type: model.endpoint_type, request_url: model.request_url, context_window_tokens: null, max_output_tokens: null, reasoning_enabled: model.reasoning_enabled, reasoning_effort: null, supports_image_generation: model.supports_image_generation, }; next.modelIds = [model.model_id]; next.customRequestUrl = Boolean(model.request_url); setEditing(model); setModelOptions([model.model_id]); setDraft(next); }; const providerSelection = (value: CursorModelDraft): ProviderSelection => value.providerMode === "new" ? { kind: "new", input: { ...value.provider, name: providerName(value.provider.base_url), custom_headers: parseHeaders(value.headersText), extra_params: parseObject(value.extraText, t("额外参数")) } } : { kind: "existing", provider_id: Number(value.providerMode) }; const discover = async () => { if (!draft) return; setDiscovering(true); try { const discovered = [...new Set((await api.discoverCursorModels(providerSelection(draft))).models)]; const existing = draft.providerMode === "new" ? new Set() : new Set(models.filter((model) => model.provider_id === Number(draft.providerMode)).map((model) => model.model_id)); setModelOptions(editing ? discovered : discovered.filter((modelId) => !existing.has(modelId))); } catch (cause) { message(cause instanceof Error ? cause.message : String(cause)); } finally { setDiscovering(false); } }; const save = async () => { if (!draft) return; try { const modelInputs = cursorModelInputs(draft, editing !== null); const saved = editing ? await appStore.updateCursorModel(editing.model_hash, modelInputs[0]) : await appStore.createCursorModels(providerSelection(draft), modelInputs); if (saved) { setDraft(null); setEditing(null); } } catch (cause) { message(cause instanceof Error ? cause.message : String(cause)); } }; const testModel = async (model: Model, notify = true) => { setTestingModelHashes((current) => new Set(current).add(model.model_hash)); try { const result = await api.testModel(model.model_hash); setModelTestResults((current) => new Map(current).set(model.model_hash, { status: "success", result })); if (notify) message(t("模型 {model} 连通性测试成功({duration} ms)", { model: model.display_name, duration: result.duration_ms })); return true; } catch (cause) { const error = cause instanceof Error ? cause.message : String(cause); setModelTestResults((current) => new Map(current).set(model.model_hash, { status: "error", error })); if (notify) message(t("连通性测试失败:{error}", { error }), { duration: 5000 }); return false; } finally { setTestingModelHashes((current) => { const next = new Set(current); next.delete(model.model_hash); return next; }); } }; const testSingleModel = async (model: Model) => { await testModel(model); await appStore.refresh(); }; const saveAndTest = async () => { if (!draft || !editing) return; setSavingAndTesting(true); try { const [input] = cursorModelInputs(draft, true); const saved = await appStore.updateCursorModel(editing.model_hash, input); if (!saved) { const error = appStore.getSnapshot().error; if (error) message(error); return; } setEditing(saved); await testSingleModel(saved); } catch (cause) { message(cause instanceof Error ? cause.message : String(cause)); } finally { setSavingAndTesting(false); } }; const testAllModels = async () => { if (!models.length || batchTesting) return; const targets = [...models]; setBatchTesting(true); try { const results = await Promise.all(targets.map((model) => testModel(model, false))); await appStore.refresh(); const successful = results.filter(Boolean).length; const failed = targets.length - successful; message(failed === 0 ? t("全部 {count} 个模型连通性测试成功", { count: targets.length }) : t("连通性测试完成:成功 {successful},失败 {failed}", { successful, failed }), { duration: failed === 0 ? 2400 : 5000 }); } finally { setBatchTesting(false); } }; const list =
{grouped.map(({ provider, models: childModels }) => {provider.name}
}>
{childModels.map((model) =>
{model.display_name}{model.model_id} · {model.model_hash}
{/* {model.enabled ? t("已启用") : t("已停用")} */} {modelTestResults.get(model.model_hash) && }
)}
)}; const refreshCa = async () => { await appStore.refresh(); if (appStore.getSnapshot().cursorHarness?.ca !== "ready") { setWaitingForCaRefresh(false); } }; const openCaTerminal = () => { if (caCommand) void api.openCursorCaInstallTerminal(caCommand); setCaCommand(null); setWaitingForCaRefresh(true); }; const saveTab = async () => { if (!tabDraft) return; try { if (tabDraft.mode === "custom" && !tabDraft.address.trim()) throw new Error(t("TAB 服务地址不能为空")); setSavingTab(true); setTabDraft(await api.setTabSettings(tabDraft)); message(t("TAB 设置已保存")); } catch (cause) { message(cause instanceof Error ? cause.message : String(cause)); } finally { setSavingTab(false); } }; const content = void initializeCa()} onRefresh={() => void refreshCa()}>
{tabDraft && void saveTab()} />} {list}
; return <> {models.length > 0 && } setDraft(null)} onSubmit={() => void save()} secondaryAction={editing ? : undefined}> {draft && <> void discover()} /> {editing && modelTestResults.get(editing.model_hash) &&
} }
setCaCommand(null)} onSubmit={openCaTerminal}>
{t("需要授权安装证书")} {t("安装命令已自动复制。点击“打开终端”,将命令粘贴到终端中执行,并按提示输入密码。")}
{caCommand}
setDeleting(null)} onSubmit={() => { if (deleting) void appStore.deleteModel(deleting.model_hash); setDeleting(null); }}>

{t("确定删除这个模型吗?")}

; } function parseObject(text: string, label: string): Record { let parsed: unknown; try { parsed = JSON.parse(text || "{}"); } catch { throw new Error(t("{label} 必须是有效 JSON", { label })); } if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error(t("{label} 必须是 JSON 对象", { label })); return parsed as Record; } function cursorModelInputs(draft: CursorModelDraft, editing: boolean) { const modelIds = editing ? [draft.model.model_id.trim()] : [...new Set(draft.modelIds.map((modelId) => modelId.trim()).filter(Boolean))]; if (!modelIds.length) throw new Error(t("请至少选择或输入一个模型")); if (editing && !draft.model.display_name.trim()) throw new Error(t("Model ID 和显示名称不能为空")); if (draft.customRequestUrl && !draft.model.request_url.trim()) throw new Error(t("请求完整地址不能为空")); return modelIds.map((modelId, index) => ({ ...draft.model, model_id: modelId, display_name: modelIds.length === 1 ? draft.model.display_name.trim() || modelId : modelId, sort_order: draft.model.sort_order + index, })); } function providerName(baseUrl: string): string { try { return new URL(baseUrl.trim()).hostname; } catch { throw new Error(t("Base URL 必须是有效地址")); } } function parseHeaders(text: string): Record { const parsed = parseObject(text, t("自定义 Headers")); if (Object.values(parsed).some((value) => typeof value !== "string")) throw new Error(t("自定义 Headers 的值必须都是字符串")); return parsed as Record; }