feat: 支持 Linux 平台使用(CA 安装适配 + 静默启动),并修复 api_key 泄漏

CA 适配:
- 移除 CA 模块对 macOS/Windows 的硬性限制,Linux 走完整初始化流程
- 按发行版自动选择安装命令:Debian 系 update-ca-certificates,
  Fedora/RHEL/Arch 系 update-ca-trust extract;已安装检测比对信任锚文件
- 删除不可达的 CaState::Unsupported 分支及前端"请使用 macOS 或 Windows"提示

静默启动(参考 cc-switch 实现逻辑):
- 新增 DesktopSettings 持久化(service_settings 表)及 GET/PUT /api/settings/desktop
- 主窗口统一以 visible(false) 创建,启动时读取配置决定是否显示
- 应用设置页:开启"开机启动"后显示"静默启动"子开关

安全修复:
- ProviderEndpoint.api_key 增加 serde(skip_serializing),
  避免明文 API Key 随 JSON 响应泄漏(provider_console 测试恢复通过)

其他:
- Linux 补齐 open_terminal_with_command:按序探测 x-terminal-emulator /
  gnome-terminal / konsole 等并在新终端窗口执行命令
- "打开终端安装 CA"失败信息不再被前端静默吞掉
- 顺手修复上游遗留问题:desktop.rs 未使用导入、output.rs 测试模块位置、providers.rs 格式
This commit is contained in:
Linanwanttodo
2026-08-25 22:59:21 +08:00
parent 081e1f50e2
commit d95273c49b
18 changed files with 297 additions and 99 deletions
@@ -15,12 +15,11 @@ export function CursorCaGate({ busy, waitingForRefresh, onInitialize, onRefresh,
const ready = useContext(CaReady);
const { cursorHarness } = useAppStore();
if (ready) return children;
const unsupported = cursorHarness?.ca === "unsupported";
const installedLocally = cursorHarness?.ca === "untrusted";
return <div className={styles.gate}>
<strong>{unsupported ? t("当前系统暂不支持安装 CA") : installedLocally ? t("需要在系统中信任本地 CA") : t("需要先初始化本地 CA")}</strong>
<span>{unsupported ? t("请使用 macOS 或 Windows。") : installedLocally ? t("请在终端中粘贴授权命令并输入密码,完成后点击下方按钮") : t("CA 仅保存在本机,用于安全解析 Cursor 的 HTTPS 请求。")}</span>
{!unsupported && <button className={controls.primary} disabled={busy} onClick={waitingForRefresh ? onRefresh : onInitialize}>{busy ? t("刷新中…") : waitingForRefresh ? t("我已初始化,刷新") : installedLocally ? t("打开终端安装 CA") : t("初始化 CA")}</button>}
<strong>{installedLocally ? t("需要在系统中信任本地 CA") : t("需要先初始化本地 CA")}</strong>
<span>{installedLocally ? t("请在终端中粘贴授权命令并输入密码,完成后点击下方按钮") : t("CA 仅保存在本机,用于安全解析 Cursor 的 HTTPS 请求。")}</span>
<button className={controls.primary} disabled={busy} onClick={waitingForRefresh ? onRefresh : onInitialize}>{busy ? t("刷新中…") : waitingForRefresh ? t("我已初始化,刷新") : installedLocally ? t("打开终端安装 CA") : t("初始化 CA")}</button>
</div>;
}
@@ -3,7 +3,9 @@ import {
currentAppVersion,
hasNativeAppLifecycle,
readAutostart,
readSilentStart,
writeAutostart,
writeSilentStart,
} from "../../native/appLifecycle";
import { updateStore, useUpdateStore } from "../../store/updateStore";
import { Button } from "../ui/Button";
@@ -19,6 +21,8 @@ export function AppLifecycleSettingsCard() {
const [version, setVersion] = useState("…");
const [autostart, setAutostart] = useState(false);
const [loadingAutostart, setLoadingAutostart] = useState(native);
const [silentStart, setSilentStart] = useState(false);
const [loadingSilentStart, setLoadingSilentStart] = useState(native);
useEffect(() => {
let disposed = false;
@@ -28,6 +32,10 @@ export function AppLifecycleSettingsCard() {
.then((enabled) => { if (!disposed) setAutostart(enabled); })
.catch((cause) => message(cause instanceof Error ? cause.message : String(cause)))
.finally(() => { if (!disposed) setLoadingAutostart(false); });
void readSilentStart()
.then((silent) => { if (!disposed) setSilentStart(silent); })
.catch(() => {})
.finally(() => { if (!disposed) setLoadingSilentStart(false); });
}
return () => { disposed = true; };
}, [message, native]);
@@ -45,6 +53,19 @@ export function AppLifecycleSettingsCard() {
}
};
const toggleSilentStart = async (enabled: boolean) => {
try {
setLoadingSilentStart(true);
await writeSilentStart(enabled);
setSilentStart(await readSilentStart());
message(enabled ? t("已开启静默启动") : t("已关闭静默启动"));
} catch (cause) {
message(cause instanceof Error ? cause.message : String(cause));
} finally {
setLoadingSilentStart(false);
}
};
const checkUpdate = async () => {
try {
const nextVersion = await updateStore.check();
@@ -75,6 +96,18 @@ export function AppLifecycleSettingsCard() {
onChange={(enabled) => void toggleAutostart(enabled)}
/>
</div>
{autostart && <div className={styles.row}>
<div>
<strong>{t("静默启动")}</strong>
<small>{t("开机启动时不显示主窗口,仅保留系统托盘图标。")}</small>
</div>
<Switch
checked={silentStart}
disabled={!native || loadingSilentStart}
label={t("静默启动")}
onChange={(enabled) => void toggleSilentStart(enabled)}
/>
</div>}
<div className={styles.row}>
<div>
<strong>{t("软件更新")}</strong>