mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-06 21:52:51 +08:00
241 lines
9.8 KiB
TypeScript
241 lines
9.8 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { api, type ProxySettings, type ProxySettingsInput, type StatisticsStorage } from "../api";
|
||
import { PageContent } from "../components/layout/PageContent";
|
||
import { AppLifecycleSettingsCard } from "../components/settings/AppLifecycleSettingsCard";
|
||
import { ProxySettingsCard } from "../components/settings/ProxySettingsCard";
|
||
import { Button } from "../components/ui/Button";
|
||
import { Checkbox } from "../components/ui/Checkbox";
|
||
import { FormField, TextInput } from "../components/ui/FormControls";
|
||
import { Modal } from "../components/ui/Modal";
|
||
import { TitledCard } from "../components/ui/TitledCard";
|
||
import { useMessage } from "../components/ui/message";
|
||
import { appStore, useAppStore } from "../store/appStore";
|
||
import { themeOptions } from "../theme/theme";
|
||
import styles from "./SettingsPage.module.scss";
|
||
|
||
export function SettingsPage() {
|
||
const { detailed, ports, theme } = useAppStore();
|
||
const message = useMessage();
|
||
const [proxyPort, setProxyPort] = useState(String(ports.proxy_port));
|
||
const [servicePort, setServicePort] = useState(String(ports.service_port));
|
||
const [editingPorts, setEditingPorts] = useState(false);
|
||
const [savingPorts, setSavingPorts] = useState(false);
|
||
const [storage, setStorage] = useState<StatisticsStorage | null>(null);
|
||
const [confirmClear, setConfirmClear] = useState(false);
|
||
const [clearing, setClearing] = useState(false);
|
||
const [outboundProxy, setOutboundProxy] = useState<ProxySettings | null>(null);
|
||
const [proxyDraft, setProxyDraft] = useState<ProxySettingsInput>({ mode: "system", address: "", auth_enabled: false, username: "", password: "" });
|
||
const [editingProxy, setEditingProxy] = useState(false);
|
||
const [savingProxy, setSavingProxy] = useState(false);
|
||
useEffect(() => {
|
||
void Promise.all([api.statisticsStorage(), api.proxySettings()]).then(([nextStorage, nextProxy]) => {
|
||
setStorage(nextStorage);
|
||
setOutboundProxy(nextProxy);
|
||
setProxyDraft({ mode: nextProxy.mode, address: nextProxy.address, auth_enabled: nextProxy.auth_enabled, username: nextProxy.username, password: "" });
|
||
}).catch((cause) => message(cause instanceof Error ? cause.message : String(cause)));
|
||
}, [message]);
|
||
useEffect(() => {
|
||
setProxyPort(String(ports.proxy_port));
|
||
setServicePort(String(ports.service_port));
|
||
}, [ports.proxy_port, ports.service_port]);
|
||
|
||
const parsePort = (value: string, label: string) => {
|
||
const port = Number(value);
|
||
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
||
throw new Error(`${label}${t("必须是 0–65535 之间的整数")}`);
|
||
}
|
||
return port;
|
||
};
|
||
const savePorts = async () => {
|
||
try {
|
||
const next = {
|
||
proxy_port: parsePort(proxyPort, t("代理端口")),
|
||
service_port: parsePort(servicePort, t("服务端口")),
|
||
};
|
||
setSavingPorts(true);
|
||
if (await appStore.updatePorts(next)) {
|
||
setEditingPorts(false);
|
||
message(t("端口设置已保存,重启软件后生效"), { duration: 4_000 });
|
||
}
|
||
} catch (cause) {
|
||
message(cause instanceof Error ? cause.message : String(cause));
|
||
} finally {
|
||
setSavingPorts(false);
|
||
}
|
||
};
|
||
const editPorts = () => {
|
||
setProxyPort(String(ports.proxy_port));
|
||
setServicePort(String(ports.service_port));
|
||
setEditingPorts(true);
|
||
};
|
||
const cancelPortEdit = () => {
|
||
setProxyPort(String(ports.proxy_port));
|
||
setServicePort(String(ports.service_port));
|
||
setEditingPorts(false);
|
||
};
|
||
const clearStorage = async () => {
|
||
try {
|
||
setClearing(true);
|
||
setStorage(await api.clearStatisticsStorage());
|
||
setConfirmClear(false);
|
||
await appStore.refresh();
|
||
message(t("统计数据已清理"));
|
||
} catch (cause) {
|
||
message(cause instanceof Error ? cause.message : String(cause));
|
||
} finally {
|
||
setClearing(false);
|
||
}
|
||
};
|
||
const editProxy = () => {
|
||
if (!outboundProxy) return;
|
||
setProxyDraft({ mode: outboundProxy.mode, address: outboundProxy.address, auth_enabled: outboundProxy.auth_enabled, username: outboundProxy.username, password: "" });
|
||
setEditingProxy(true);
|
||
};
|
||
const cancelProxyEdit = () => {
|
||
if (outboundProxy) {
|
||
setProxyDraft({ mode: outboundProxy.mode, address: outboundProxy.address, auth_enabled: outboundProxy.auth_enabled, username: outboundProxy.username, password: "" });
|
||
}
|
||
setEditingProxy(false);
|
||
};
|
||
const saveProxy = async () => {
|
||
try {
|
||
setSavingProxy(true);
|
||
const saved = await api.setProxySettings({ ...proxyDraft, password: proxyDraft.password || undefined });
|
||
setOutboundProxy(saved);
|
||
setProxyDraft({ mode: saved.mode, address: saved.address, auth_enabled: saved.auth_enabled, username: saved.username, password: "" });
|
||
setEditingProxy(false);
|
||
message(t("代理设置已保存"));
|
||
} catch (cause) {
|
||
message(cause instanceof Error ? cause.message : String(cause));
|
||
} finally {
|
||
setSavingProxy(false);
|
||
}
|
||
};
|
||
const formatBytes = (bytes: number) => {
|
||
if (bytes < 1024) return `${bytes} B`;
|
||
const units = ["KB", "MB", "GB", "TB"];
|
||
let value = bytes / 1024;
|
||
let unit = 0;
|
||
while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; }
|
||
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
||
};
|
||
const content = (
|
||
<div className={styles.page}>
|
||
<TitledCard title={t("调用观测")}>
|
||
<div className={styles.settingRow}>
|
||
<div>
|
||
<strong>{t("详细模式")}</strong>
|
||
<small>
|
||
{t("额外保存完整请求和流响应;默认只保存时间、状态与用量。")}
|
||
</small>
|
||
</div>
|
||
<Checkbox
|
||
label={t("详细模式")}
|
||
checked={detailed}
|
||
onChange={(checked) => void appStore.updateDetailed(checked)}
|
||
/>
|
||
</div>
|
||
</TitledCard>
|
||
<TitledCard title={t("端口设置")} action={editingPorts ? (
|
||
<div className={styles.cardActions}>
|
||
<Button size="small" disabled={savingPorts} onClick={cancelPortEdit}>{t("取消")}</Button>
|
||
<Button variant="primary" size="small" disabled={savingPorts} onClick={() => void savePorts()}>{savingPorts ? t("保存中…") : t("保存")}</Button>
|
||
</div>
|
||
) : (
|
||
<button type="button" className={styles.textButton} onClick={editPorts}>{t("编辑")}</button>
|
||
)}>
|
||
<div className={styles.portSettings}>
|
||
<div className={styles.portFields}>
|
||
{editingPorts ? <><FormField
|
||
label={t("代理端口")}
|
||
hint={t("Cursor 使用的本地代理端口;填写 0 时启动时随机选择。")}
|
||
>
|
||
<TextInput
|
||
type="number"
|
||
min={0}
|
||
max={65535}
|
||
step={1}
|
||
value={proxyPort}
|
||
onChange={(event) => setProxyPort(event.target.value)}
|
||
/>
|
||
</FormField>
|
||
<FormField
|
||
label={t("服务端口")}
|
||
hint={t(
|
||
"桌面前端连接的本地管理服务端口;填写 0 时启动时随机选择。",
|
||
)}
|
||
>
|
||
<TextInput
|
||
type="number"
|
||
min={0}
|
||
max={65535}
|
||
step={1}
|
||
value={servicePort}
|
||
onChange={(event) => setServicePort(event.target.value)}
|
||
/>
|
||
</FormField></> : <>
|
||
<div className={styles.portValue}><strong>{t("代理端口")}</strong><span>{ports.proxy_port}</span></div>
|
||
<div className={styles.portValue}><strong>{t("服务端口")}</strong><span>{ports.service_port}</span></div>
|
||
</>}
|
||
</div>
|
||
<div className={styles.portFooter}>
|
||
<small>
|
||
{t(
|
||
"端口被占用时会自动选择新的随机端口并保存。修改后需要重启软件才会生效。",
|
||
)}
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</TitledCard>
|
||
<ProxySettingsCard settings={outboundProxy} draft={proxyDraft} editing={editingProxy} saving={savingProxy} onDraftChange={setProxyDraft} onEdit={editProxy} onCancel={cancelProxyEdit} onSave={() => void saveProxy()} />
|
||
<AppLifecycleSettingsCard />
|
||
<TitledCard title={t("主题")}>
|
||
<div className={styles.themeActions}>
|
||
{themeOptions.map(({ id, name }) => (
|
||
<button
|
||
className={theme === id ? styles.selected : ""}
|
||
key={id}
|
||
onClick={() => appStore.selectTheme(id)}
|
||
>
|
||
{name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</TitledCard>
|
||
<TitledCard title={t("存储管理")}>
|
||
<div className={styles.storageRow}>
|
||
<div>
|
||
<strong>{t("统计数据")}</strong>
|
||
<small>{storage ? formatBytes(storage.bytes) : t("计算中…")}</small>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className={styles.textButton}
|
||
onClick={() => setConfirmClear(true)}
|
||
>
|
||
{t("清理存储空间")}
|
||
</button>
|
||
</div>
|
||
</TitledCard>
|
||
<Modal
|
||
open={confirmClear}
|
||
title={t("确定要清理所有统计数据吗?")}
|
||
busy={clearing}
|
||
closeLabel={t("取消")}
|
||
submitLabel={t("确认清理")}
|
||
onClose={() => setConfirmClear(false)}
|
||
onSubmit={() => void clearStorage()}
|
||
>
|
||
<div className={styles.confirmContent}>
|
||
<small>
|
||
{t(
|
||
"所有调用记录和详细追踪数据都会被删除。供应商、模型、CA 和应用设置不会受到影响,此操作无法撤销。",
|
||
)}
|
||
</small>
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
return <PageContent title={t("设置")} sections={[{ key: "settings", estimatedHeight: 900, content }]} />;
|
||
}
|