mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 03:56:45 +08:00
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:
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)]
|
||||
|
||||
@@ -31,7 +31,6 @@ pub enum CaState {
|
||||
Untrusted,
|
||||
Ready,
|
||||
Invalid,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
|
||||
@@ -51,6 +51,7 @@ pub struct ProviderEndpoint {
|
||||
pub name: String,
|
||||
pub provider_type: ProviderType,
|
||||
pub base_url: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub api_key: Option<String>,
|
||||
pub has_api_key: bool,
|
||||
pub custom_headers: serde_json::Value,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user