Merge pull request #349 from linanwanttodo/main

feat: 支持 Linux 平台使用(CA 安装适配 + 跨平台静默启动),并修复 api_key 泄漏
This commit is contained in:
leokun
2026-08-26 00:56:43 +08:00
committed by GitHub
17 changed files with 305 additions and 103 deletions
+48 -13
View File
@@ -15,7 +15,7 @@ use axum::{
};
use tauri::{
async_runtime::JoinHandle, webview::Color, AppHandle, Manager, RunEvent, WebviewUrl,
WebviewWindowBuilder,
WebviewWindow, WebviewWindowBuilder,
};
use tauri_plugin_opener::OpenerExt;
use tokio_util::sync::CancellationToken;
@@ -30,6 +30,7 @@ use crate::frontend;
use crate::tray;
pub(crate) const MAIN_WINDOW_LABEL: &str = "main";
const AUTOSTART_ARG: &str = "--autostart";
struct DesktopRuntime {
shutdown: CancellationToken,
@@ -52,12 +53,30 @@ fn open_terminal_with_command(command: String) -> tauri::Result<()> {
.spawn()?;
Ok(())
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[cfg(target_os = "linux")]
{
let _ = command;
const TERMINALS: &[(&str, &[&str])] = &[
("x-terminal-emulator", &["-e"]),
("gnome-terminal", &["--"]),
("konsole", &["-e"]),
("xfce4-terminal", &["--execute"]),
("alacritty", &["-e"]),
("kitty", &[]),
];
let script = format!("{command}; exec bash");
for (terminal, separator) in TERMINALS {
let mut process = Command::new(terminal);
process.args(*separator);
process.arg("bash").arg("-c").arg(&script);
match process.spawn() {
Ok(_) => return Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(error.into()),
}
}
Err(tauri::Error::from(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"terminal guidance is unsupported on this platform",
std::io::ErrorKind::NotFound,
"no supported terminal emulator found",
)))
}
}
@@ -96,7 +115,10 @@ fn desktop_api_router(app: AppHandle) -> Router {
.layer(Extension(app))
}
fn create_main_window(app: &AppHandle, address: std::net::SocketAddr) -> tauri::Result<()> {
fn create_main_window(
app: &AppHandle,
address: std::net::SocketAddr,
) -> tauri::Result<WebviewWindow> {
let url = format!("http://{address}/__byok-api__/")
.parse()
.expect("local frontend URL");
@@ -108,18 +130,20 @@ fn create_main_window(app: &AppHandle, address: std::net::SocketAddr) -> tauri::
.background_color(Color(20, 20, 20, 255))
.decorations(cfg!(target_os = "macos"))
.shadow(true)
.resizable(true);
.resizable(true)
.visible(false);
#[cfg(target_os = "macos")]
let builder = builder
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true);
builder.build()?;
Ok(())
builder.build()
}
pub fn run() {
let started_by_autostart = std::env::args_os().any(|arg| arg == AUTOSTART_ARG);
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
@@ -130,17 +154,19 @@ pub fn run() {
let app = tauri::Builder::default()
.invoke_handler(tauri::generate_handler![open_terminal_with_command])
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
.plugin(tauri_plugin_single_instance::init(|app, args, _| {
if !args.iter().any(|arg| arg == AUTOSTART_ARG) {
tray::show_main_window(app);
}
}))
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
.setup(move |app| {
app.handle().plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
Some(vec![AUTOSTART_ARG]),
))?;
let config = Config::desktop()?;
#[cfg(dev)]
@@ -160,6 +186,9 @@ pub fn run() {
let listener = tauri::async_runtime::block_on(server.bind())?;
let address = listener.local_addr()?;
tauri::async_runtime::block_on(server.harness().cleanup_stale_settings())?;
let silent_start = tauri::async_runtime::block_on(server.store().desktop_settings())
.map(|settings| settings.silent_start)
.unwrap_or(false);
let shutdown = CancellationToken::new();
let server_shutdown = shutdown.clone();
let app_handle = app.handle().clone();
@@ -176,7 +205,13 @@ pub fn run() {
server: Mutex::new(Some(task)),
exiting: AtomicBool::new(false),
});
create_main_window(app.handle(), address)?;
let window = create_main_window(app.handle(), address)?;
if silent_start && started_by_autostart {
tracing::info!("silent autostart enabled; keeping the main window hidden");
} else {
window.show()?;
window.set_focus()?;
}
tray::create(app)?;
Ok(())
})
+7 -1
View File
@@ -66,7 +66,7 @@ export interface ModelConnectivityResult {
output: string;
}
export type CaState = "missing" | "untrusted" | "ready" | "invalid" | "unsupported";
export type CaState = "missing" | "untrusted" | "ready" | "invalid";
export type IntegrationState = "disabled" | "enabled" | "degraded";
export interface CursorHarnessStatus {
platform: string;
@@ -114,6 +114,10 @@ export interface TabSettings {
address: string;
}
export interface DesktopSettings {
silent_start: boolean;
}
export interface OverviewMetrics {
llm_calls: number;
successful_calls: number;
@@ -310,4 +314,6 @@ export const api = {
setProxySettings: (settings: ProxySettingsInput) => request<ProxySettings>("/settings/proxy", { method: "PUT", body: JSON.stringify(settings) }),
tabSettings: () => request<TabSettings>("/settings/tab"),
setTabSettings: (settings: TabSettings) => request<TabSettings>("/settings/tab", { method: "PUT", body: JSON.stringify(settings) }),
desktopSettings: () => request<DesktopSettings>("/settings/desktop"),
setDesktopSettings: (settings: DesktopSettings) => request<DesktopSettings>("/settings/desktop", { method: "PUT", body: JSON.stringify(settings) }),
};
@@ -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>
+90 -61
View File
@@ -431,8 +431,8 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 22,
"column": 107
"line": 21,
"column": 65
}
]
},
@@ -484,7 +484,7 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 38,
"line": 37,
"column": 59
},
{
@@ -576,7 +576,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 87,
"line": 120,
"column": 27
}
]
@@ -789,7 +789,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 51,
"line": 72,
"column": 29
}
]
@@ -803,7 +803,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 83,
"line": 116,
"column": 13
}
]
@@ -1320,6 +1320,18 @@
}
]
},
"47d1c20aa017ff05": {
"source": "开机启动时不显示主窗口,仅保留系统托盘图标。",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 102,
"column": 17
}
]
},
"48b970b568a7f8f9": {
"source": "代理设置",
"kind": "text",
@@ -1355,7 +1367,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 82,
"line": 115,
"column": 13
}
]
@@ -1514,8 +1526,8 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 23,
"column": 222
"line": 22,
"column": 205
}
]
},
@@ -1665,7 +1677,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 40,
"line": 48,
"column": 40
}
]
@@ -1789,7 +1801,7 @@
"refs": [
{
"file": "api.ts",
"line": 290,
"line": 294,
"column": 43
}
]
@@ -1801,7 +1813,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 80,
"line": 113,
"column": 18
}
]
@@ -1844,7 +1856,7 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 36,
"line": 35,
"column": 14
}
]
@@ -1974,7 +1986,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 51,
"line": 72,
"column": 78
}
]
@@ -2015,18 +2027,6 @@
}
]
},
"722e156c20650a3f": {
"source": "请使用 macOS 或 Windows。",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 22,
"column": 26
}
]
},
"7392e20d61abaa07": {
"source": "额外保存完整请求和流响应;默认只保存时间、状态与用量。",
"kind": "text",
@@ -2082,7 +2082,7 @@
"refs": [
{
"file": "api.ts",
"line": 285,
"line": 289,
"column": 43
}
]
@@ -2185,8 +2185,8 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 23,
"column": 138
"line": 22,
"column": 121
}
]
},
@@ -2197,7 +2197,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 91,
"line": 124,
"column": 37
}
]
@@ -2221,7 +2221,7 @@
"refs": [
{
"file": "api.ts",
"line": 231,
"line": 235,
"column": 21
}
]
@@ -2250,12 +2250,12 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 68,
"line": 89,
"column": 18
},
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 74,
"line": 95,
"column": 16
}
]
@@ -2272,6 +2272,18 @@
}
]
},
"864597982c308d72": {
"source": "已开启静默启动",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 61,
"column": 25
}
]
},
"86b7355ec3bd55ef": {
"source": "隐藏 API Key",
"kind": "text",
@@ -2508,18 +2520,6 @@
}
]
},
"95f76d30c25d5eda": {
"source": "当前系统暂不支持安装 CA",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 21,
"column": 28
}
]
},
"966498853d801a52": {
"source": "TAB 选择",
"kind": "text",
@@ -2558,8 +2558,8 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 23,
"column": 205
"line": 22,
"column": 188
}
]
},
@@ -2616,6 +2616,18 @@
}
]
},
"9e356080c56877f8": {
"source": "已关闭静默启动",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 61,
"column": 40
}
]
},
"9e46da6923836182": {
"source": "如:2026-08-23 09:00、1小时前",
"kind": "text",
@@ -2683,11 +2695,28 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 65,
"line": 86,
"column": 29
}
]
},
"a1b8c98f29374a2f": {
"source": "静默启动",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 101,
"column": 18
},
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 107,
"column": 16
}
]
},
"a2901cd2743c1921": {
"source": "模型服务的 API 根地址;修改后会同步更新该上游模型的路由身份。",
"kind": "text",
@@ -2734,8 +2763,8 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 23,
"column": 170
"line": 22,
"column": 153
}
]
},
@@ -2906,7 +2935,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 40,
"line": 48,
"column": 25
}
]
@@ -2918,7 +2947,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 87,
"line": 120,
"column": 39
}
]
@@ -3561,7 +3590,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 69,
"line": 90,
"column": 17
}
]
@@ -3962,8 +3991,8 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 21,
"column": 68
"line": 20,
"column": 33
}
]
},
@@ -4121,7 +4150,7 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 37,
"line": 36,
"column": 12
}
]
@@ -4296,7 +4325,7 @@
"refs": [
{
"file": "components/settings/AppLifecycleSettingsCard.tsx",
"line": 91,
"line": 124,
"column": 25
}
]
@@ -4308,8 +4337,8 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 21,
"column": 89
"line": 20,
"column": 54
}
]
},
@@ -4422,8 +4451,8 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
"line": 22,
"column": 73
"line": 21,
"column": 31
}
]
},
+4 -2
View File
@@ -88,6 +88,7 @@
"43cb41d62de2d179": "Proxy requires authentication",
"461d6a57900c2ed7": "Connectivity test failed: {error}",
"470049252e54de6a": "Success rate: {rate}",
"47d1c20aa017ff05": "Hide the main window on startup and keep only the tray icon.",
"48b970b568a7f8f9": "Proxy settings",
"48d8db17bae06246": "{count} total",
"492042ed1fdc29ed": "Version {version} is ready to install",
@@ -137,7 +138,6 @@
"6e86570183c3cdd0": "You're up to date",
"7005693f4f050bce": "Cache I/O {cost}",
"71eb2c6fce991a29": "Add provider",
"722e156c20650a3f": "Use macOS or Windows.",
"7392e20d61abaa07": "Also store complete requests and streamed responses; by default only timing, status, and usage are stored.",
"75844712d53a24de": "Values must be strings; when editing, null keeps the original value of the corresponding sensitive Header.",
"788db1cfec2a3db5": "Theme",
@@ -157,6 +157,7 @@
"83fcfb4c1f2c1641": "Fetch models",
"842b9f11cdd96bda": "Launch at login",
"84924374710e03bd": "Base URL must be a valid URL",
"864597982c308d72": "Silent start enabled",
"86b7355ec3bd55ef": "Hide API Key",
"8716e1344b0daddb": "Cursor official",
"878a8ab176429a86": "View instructions",
@@ -174,7 +175,6 @@
"946b3ffc02f026c0": "Delete this model?",
"94803f35c825e47a": "Full request URL",
"94df1e7f04815daf": "Used only for display; does not change the model name sent to the provider.",
"95f76d30c25d5eda": "CA installation is not supported on this system",
"966498853d801a52": "TAB connection",
"9850ed41a5bfbb0c": "{count} selected",
"997ec8201c2adeda": "Open terminal to install CA",
@@ -182,12 +182,14 @@
"9ac0ac940982895d": "Select provider",
"9c41b3a9e12ac994": "Reasoning effort",
"9d8f2ef4e85ea665": "Custom context tokens",
"9e356080c56877f8": "Silent start disabled",
"9e46da6923836182": "For example: 2026-08-23 09:00, 1 hour ago",
"9f6fee1aba17a565": "Language",
"9fb48101d237ff96": "Last week",
"a026f37e613cf48b": "Output Tokens",
"a03a1a0cb35414f8": " must be an integer from 0 to 65535",
"a1a42cd9b16e2162": "Application",
"a1b8c98f29374a2f": "Silent start",
"a2901cd2743c1921": "The API root URL for the model service. Changing it also updates the routing identity of this provider's models.",
"a3030bf8f16dc63c": "Save",
"a363743025795ec7": "I've initialized it — refresh",
+4 -2
View File
@@ -88,6 +88,7 @@
"43cb41d62de2d179": "代理需要认证",
"461d6a57900c2ed7": "连通性测试失败:{error}",
"470049252e54de6a": "成功占比:{rate}",
"47d1c20aa017ff05": "开机启动时不显示主窗口,仅保留系统托盘图标。",
"48b970b568a7f8f9": "代理设置",
"48d8db17bae06246": "共 {count} 条",
"492042ed1fdc29ed": "版本 {version} 可以安装",
@@ -137,7 +138,6 @@
"6e86570183c3cdd0": "当前已是最新版本",
"7005693f4f050bce": "缓存读写 {cost}",
"71eb2c6fce991a29": "添加上游",
"722e156c20650a3f": "请使用 macOS 或 Windows。",
"7392e20d61abaa07": "额外保存完整请求和流响应;默认只保存时间、状态与用量。",
"75844712d53a24de": "值必须是字符串;编辑时 null 表示保留对应敏感 Header 的原值。",
"788db1cfec2a3db5": "主题",
@@ -157,6 +157,7 @@
"83fcfb4c1f2c1641": "获取模型",
"842b9f11cdd96bda": "开机启动",
"84924374710e03bd": "Base URL 必须是有效地址",
"864597982c308d72": "已开启静默启动",
"86b7355ec3bd55ef": "隐藏 API Key",
"8716e1344b0daddb": "Cursor 官方",
"878a8ab176429a86": "查看说明",
@@ -174,7 +175,6 @@
"946b3ffc02f026c0": "确定删除这个模型吗?",
"94803f35c825e47a": "请求完整地址",
"94df1e7f04815daf": "仅用于界面展示,不会改变发送给上游的模型名称。",
"95f76d30c25d5eda": "当前系统暂不支持安装 CA",
"966498853d801a52": "TAB 选择",
"9850ed41a5bfbb0c": "已选 {count} 项",
"997ec8201c2adeda": "打开终端安装 CA",
@@ -182,12 +182,14 @@
"9ac0ac940982895d": "选择上游",
"9c41b3a9e12ac994": "思考强度",
"9d8f2ef4e85ea665": "自定义上下文 tokens",
"9e356080c56877f8": "已关闭静默启动",
"9e46da6923836182": "如:2026-08-23 09:00、1小时前",
"9f6fee1aba17a565": "语言",
"9fb48101d237ff96": "近一周",
"a026f37e613cf48b": "输出 Token",
"a03a1a0cb35414f8": "必须是 0–65535 之间的整数",
"a1a42cd9b16e2162": "应用设置",
"a1b8c98f29374a2f": "静默启动",
"a2901cd2743c1921": "模型服务的 API 根地址;修改后会同步更新该上游模型的路由身份。",
"a3030bf8f16dc63c": "保存",
"a363743025795ec7": "我已初始化,刷新",
+9
View File
@@ -3,6 +3,7 @@ import { isTauri } from "@tauri-apps/api/core";
import { disable, enable, isEnabled } from "@tauri-apps/plugin-autostart";
import { relaunch } from "@tauri-apps/plugin-process";
import { check, type Update } from "@tauri-apps/plugin-updater";
import { api } from "../api";
export function hasNativeAppLifecycle(): boolean {
return isTauri();
@@ -20,6 +21,14 @@ export async function writeAutostart(enabled: boolean): Promise<void> {
await (enabled ? enable() : disable());
}
export async function readSilentStart(): Promise<boolean> {
return (await api.desktopSettings()).silent_start;
}
export async function writeSilentStart(silentStart: boolean): Promise<void> {
await api.setDesktopSettings({ silent_start: silentStart });
}
export async function checkForUpdate(): Promise<Update | null> {
return check();
}
@@ -170,7 +170,11 @@ export function CursorSettingsPage() {
}
};
const openCaTerminal = () => {
if (caCommand) void api.openCursorCaInstallTerminal(caCommand);
if (caCommand) {
api
.openCursorCaInstallTerminal(caCommand)
.catch((cause) => message(cause instanceof Error ? cause.message : String(cause)));
}
setCaCommand(null);
setWaitingForCaRefresh(true);
};
+4
View File
@@ -84,6 +84,10 @@ impl App {
self.harness.clone()
}
pub fn store(&self) -> Store {
self.store.clone()
}
pub async fn serve(self) -> Result<()> {
let listener = self.bind().await?;
let shutdown = CancellationToken::new();
+4
View File
@@ -164,6 +164,10 @@ pub fn api_router(service: ControlService) -> Router {
"/__byok-api__/api/settings/tab",
get(settings::get_tab).put(settings::update_tab),
)
.route(
"/__byok-api__/api/settings/desktop",
get(settings::get_desktop).put(settings::update_desktop),
)
.route(
"/__byok-api__/api/harness/cursor/status",
get(harness::status),
+10 -1
View File
@@ -22,7 +22,8 @@ use crate::{
},
provider::{ModelEvent, Provider},
store::{
PortSettings, ProxySettings, ProxySettingsInput, StatisticsStorage, Store, TabSettings,
DesktopSettings, PortSettings, ProxySettings, ProxySettingsInput, StatisticsStorage, Store,
TabSettings,
},
Error, Result,
};
@@ -497,6 +498,14 @@ impl ControlService {
pub async fn set_tab_settings(&self, settings: TabSettings) -> Result<TabSettings> {
self.cursor_harness.set_tab_settings(settings).await
}
pub async fn desktop_settings(&self) -> Result<DesktopSettings> {
self.store.desktop_settings().await
}
pub async fn set_desktop_settings(&self, settings: DesktopSettings) -> Result<()> {
self.store.set_desktop_settings(settings).await
}
}
fn official_call(trace: CursorRunTraceSummary) -> CallSummary {
+14 -1
View File
@@ -2,7 +2,8 @@ use crate::Result;
use axum::{extract::State, Json};
use crate::store::{
PortSettings, ProxySettings, ProxySettingsInput, StatisticsStorage, TabSettings,
DesktopSettings, PortSettings, ProxySettings, ProxySettingsInput, StatisticsStorage,
TabSettings,
};
use super::{ControlService, ObservabilitySettings};
@@ -60,3 +61,15 @@ pub async fn update_tab(
) -> Result<Json<TabSettings>> {
Ok(Json(service.set_tab_settings(settings).await?))
}
pub async fn get_desktop(State(service): State<ControlService>) -> Result<Json<DesktopSettings>> {
Ok(Json(service.desktop_settings().await?))
}
pub async fn update_desktop(
State(service): State<ControlService>,
Json(settings): Json<DesktopSettings>,
) -> Result<Json<DesktopSettings>> {
service.set_desktop_settings(settings).await?;
get_desktop(State(service)).await
}
@@ -409,6 +409,10 @@ fn creates_subagent(call: &ToolCall) -> bool {
)
}
fn missing(name: &str) -> Error {
Error::Protocol(format!("{name} returned no result"))
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
@@ -508,7 +512,3 @@ mod tests {
assert!(content.contains("cannot find value `name`"));
}
}
fn missing(name: &str) -> Error {
Error::Protocol(format!("{name} returned no result"))
}
+34 -11
View File
@@ -46,9 +46,6 @@ impl CaManager {
}
pub fn state(&self) -> Result<CaState> {
if !matches!(std::env::consts::OS, "macos" | "windows") {
return Ok(CaState::Unsupported);
}
let cert = fs::read_to_string(self.cert_path());
let key = fs::read_to_string(self.key_path());
match (cert, key) {
@@ -93,18 +90,21 @@ impl CaManager {
"certutil -addstore -f Root \"{}\"",
self.cert_path().display()
)),
"linux" => {
let anchor = linux_anchor_file();
Some(format!(
"sudo cp '{}' '{}' && sudo {}",
path,
anchor.display(),
linux_refresh_command()
))
}
_ => None,
}
}
pub fn initialize_local(&self) -> Result<()> {
match self.state()? {
CaState::Unsupported => {
return Err(Error::Config(format!(
"CA installation is not supported on {}",
std::env::consts::OS
)))
}
CaState::Invalid => {
return Err(Error::Config("CA files are incomplete or invalid".into()))
}
@@ -210,8 +210,31 @@ fn is_installed(cert: &str) -> Result<bool> {
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn is_installed(_cert: &str) -> Result<bool> {
Ok(false)
fn is_installed(cert: &str) -> Result<bool> {
match fs::read_to_string(linux_anchor_file()) {
Ok(installed) => Ok(installed.trim() == cert.trim()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(error.into()),
}
}
const LINUX_ANCHOR_NAME: &str = "cursor-byok-local-ca.crt";
fn linux_anchor_file() -> PathBuf {
if PathBuf::from("/etc/pki/ca-trust/source/anchors").is_dir() {
PathBuf::from("/etc/pki/ca-trust/source/anchors").join(LINUX_ANCHOR_NAME)
} else if PathBuf::from("/etc/ca-certificates/trust-source/anchors").is_dir() {
PathBuf::from("/etc/ca-certificates/trust-source/anchors").join(LINUX_ANCHOR_NAME)
} else {
PathBuf::from("/usr/local/share/ca-certificates").join(LINUX_ANCHOR_NAME)
}
}
fn linux_refresh_command() -> &'static str {
match linux_anchor_file().parent().and_then(|dir| dir.to_str()) {
Some("/usr/local/share/ca-certificates") => "update-ca-certificates",
_ => "update-ca-trust extract",
}
}
#[cfg(test)]
-1
View File
@@ -31,7 +31,6 @@ pub enum CaState {
Untrusted,
Ready,
Invalid,
Unsupported,
}
#[derive(Clone, Debug, Serialize)]
+31
View File
@@ -8,6 +8,7 @@ const PORT_SETTINGS_KEY: &str = "network_ports";
const PROXY_SETTINGS_KEY: &str = "outbound_proxy";
const TAB_SETTINGS_KEY: &str = "cursor_tab";
const INSTALLATION_ID_KEY: &str = "installation_id";
const DESKTOP_SETTINGS_KEY: &str = "desktop_lifecycle";
pub const PUBLIC_TAB_SERVICE_URL: &str = "https://tab.leokun.cn";
@@ -46,6 +47,12 @@ pub struct TabSettings {
pub address: String,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct DesktopSettings {
#[serde(default)]
pub silent_start: bool,
}
impl TabSettings {
pub fn service_url(&self) -> Option<&str> {
match self.mode {
@@ -243,6 +250,30 @@ impl Store {
settings.proxy_port = port;
self.set_port_settings(settings).await
}
pub async fn desktop_settings(&self) -> Result<DesktopSettings> {
let value = sqlx::query_scalar::<_, String>(
"SELECT value_json FROM service_settings WHERE setting_key = ?",
)
.bind(DESKTOP_SETTINGS_KEY)
.fetch_optional(&self.pool)
.await?;
value
.map(|value| serde_json::from_str(&value).map_err(Into::into))
.unwrap_or_else(|| Ok(DesktopSettings::default()))
}
pub async fn set_desktop_settings(&self, settings: DesktopSettings) -> Result<()> {
sqlx::query(
"INSERT INTO service_settings(setting_key, value_json, updated_at_ms) VALUES (?, ?, ?) ON CONFLICT(setting_key) DO UPDATE SET value_json = excluded.value_json, updated_at_ms = excluded.updated_at_ms",
)
.bind(DESKTOP_SETTINGS_KEY)
.bind(serde_json::to_string(&settings)?)
.bind(now_ms())
.execute(&self.pool)
.await?;
Ok(())
}
}
#[cfg(test)]