Files
cursor-byok/server/src/harness/mod.rs
T
Linanwanttodo d95273c49b 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 格式
2026-08-25 22:59:21 +08:00

214 lines
6.2 KiB
Rust

mod account;
mod ca;
mod proxy;
mod settings;
use std::{net::SocketAddr, sync::Arc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use crate::{
store::{Store, TabMode, TabSettings},
Error, Result,
};
use self::{ca::CaManager, proxy::ProxyRuntime};
pub(crate) fn proxy_host_allowed(host: &str) -> bool {
proxy::is_cursor_host(host)
}
fn integration_prerequisites_ready(ca: &CaState, backend_ready: bool) -> bool {
matches!(ca, CaState::Ready) && backend_ready
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CaState {
Missing,
Untrusted,
Ready,
Invalid,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IntegrationState {
Disabled,
Enabled,
Degraded,
}
#[derive(Clone, Debug, Serialize)]
pub struct CursorHarnessStatus {
pub platform: &'static str,
pub ca: CaState,
pub configured_models: usize,
pub enabled_models: usize,
pub integration: IntegrationState,
pub proxy_url: Option<String>,
pub ca_install_command: Option<String>,
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct SetEnabled {
pub enabled: bool,
}
#[derive(Clone)]
pub struct CursorHarness {
inner: Arc<Inner>,
}
struct Inner {
store: Store,
ca: CaManager,
ca_initialization: Mutex<()>,
backend_addr: RwLock<Option<SocketAddr>>,
tab_mode: Arc<RwLock<TabMode>>,
proxy: Mutex<ProxyRuntime>,
}
impl CursorHarness {
pub fn new(store: Store) -> Result<Self> {
Ok(Self {
inner: Arc::new(Inner {
store,
ca: CaManager::managed()?,
ca_initialization: Mutex::new(()),
backend_addr: RwLock::new(None),
tab_mode: Arc::new(RwLock::new(TabMode::default())),
proxy: Mutex::new(ProxyRuntime::default()),
}),
})
}
pub fn set_backend_addr(&self, addr: SocketAddr) {
*self.inner.backend_addr.write() = Some(addr);
}
pub async fn cleanup_stale_settings(&self) -> Result<()> {
settings::clear_stale_managed_settings()
}
pub async fn status(&self) -> Result<CursorHarnessStatus> {
let models = self.inner.store.provider_models(false).await?;
let configured_models = models.len();
let enabled_models = models.iter().filter(|model| model.enabled).count();
let ca = self.inner.ca.state()?;
if integration_prerequisites_ready(&ca, self.inner.backend_addr.read().is_some()) {
self.enable().await?;
}
let proxy = self.inner.proxy.lock().await;
let proxy_url = proxy.url();
let settings_applied = proxy_url
.as_deref()
.map(settings::settings_match)
.transpose()?
.unwrap_or(false);
let integration = match (proxy.running(), settings_applied) {
(false, false) => IntegrationState::Disabled,
(true, true) => IntegrationState::Enabled,
_ => IntegrationState::Degraded,
};
Ok(CursorHarnessStatus {
platform: std::env::consts::OS,
ca,
configured_models,
enabled_models,
integration,
proxy_url,
ca_install_command: self.inner.ca.install_command(),
})
}
pub async fn initialize_ca(&self) -> Result<CursorHarnessStatus> {
let _initialization = self.inner.ca_initialization.lock().await;
let manager = self.inner.ca.clone();
tokio::task::spawn_blocking(move || manager.initialize_local())
.await
.map_err(|error| Error::Store(format!("CA initialization task failed: {error}")))??;
self.status().await
}
pub async fn set_enabled(&self, enabled: bool) -> Result<CursorHarnessStatus> {
if enabled {
self.enable().await?;
} else {
self.disable().await?;
}
self.status().await
}
pub async fn set_tab_settings(&self, settings: TabSettings) -> Result<TabSettings> {
let saved = self.inner.store.set_tab_settings(settings).await?;
*self.inner.tab_mode.write() = saved.mode;
Ok(saved)
}
async fn enable(&self) -> Result<()> {
if !matches!(self.inner.ca.state()?, CaState::Ready) {
return Err(Error::Config(
"initialize and trust the CA before enabling Cursor".into(),
));
}
let backend_addr = self
.inner
.backend_addr
.read()
.ok_or_else(|| Error::Config("desktop management server is not ready".into()))?;
let mut proxy = self.inner.proxy.lock().await;
if proxy.running() {
if let Some(url) = proxy.url() {
apply_cursor_configuration(&url).await?;
}
return Ok(());
}
let ca = self.inner.ca.load()?;
let requested_port = self.inner.store.port_settings().await?.proxy_port;
*self.inner.tab_mode.write() = self.inner.store.tab_settings().await?.mode;
let (url, actual_port) = proxy
.start(
backend_addr,
ca,
requested_port,
self.inner.tab_mode.clone(),
)
.await?;
if let Err(error) = self.inner.store.set_proxy_port(actual_port).await {
proxy.stop().await;
return Err(error);
}
if let Err(error) = apply_cursor_configuration(&url).await {
proxy.stop().await;
return Err(error);
}
Ok(())
}
pub async fn disable(&self) -> Result<()> {
settings::clear_proxy_settings()?;
self.inner.proxy.lock().await.stop().await;
Ok(())
}
}
async fn apply_cursor_configuration(proxy_url: &str) -> Result<()> {
account::inject_if_missing().await?;
settings::write_proxy_settings(proxy_url)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn automatic_integration_requires_only_a_ready_ca_and_backend() {
assert!(integration_prerequisites_ready(&CaState::Ready, true));
assert!(!integration_prerequisites_ready(&CaState::Ready, false));
assert!(!integration_prerequisites_ready(&CaState::Missing, true));
}
}