refactor: better custom name

This commit is contained in:
zhom
2026-03-02 11:29:17 +04:00
parent 8a96d18e46
commit 97b1225d40
17 changed files with 525 additions and 64 deletions
+14
View File
@@ -39,6 +39,7 @@ pub struct ApiProfile {
pub group_id: Option<String>,
pub tags: Vec<String>,
pub is_running: bool,
pub proxy_bypass_rules: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
@@ -79,6 +80,7 @@ pub struct UpdateProfileRequest {
pub group_id: Option<String>,
pub tags: Option<Vec<String>>,
pub extension_group_id: Option<String>,
pub proxy_bypass_rules: Option<Vec<String>>,
}
#[derive(Clone)]
@@ -487,6 +489,7 @@ async fn get_profiles() -> Result<Json<ApiProfilesResponse>, StatusCode> {
group_id: profile.group_id.clone(),
tags: profile.tags.clone(),
is_running: profile.process_id.is_some(), // Simple check based on process_id
proxy_bypass_rules: profile.proxy_bypass_rules.clone(),
})
.collect();
@@ -541,6 +544,7 @@ async fn get_profile(
group_id: profile.group_id.clone(),
tags: profile.tags.clone(),
is_running: profile.process_id.is_some(), // Simple check based on process_id
proxy_bypass_rules: profile.proxy_bypass_rules.clone(),
},
}))
} else {
@@ -639,6 +643,7 @@ async fn create_profile(
group_id: profile.group_id,
tags: profile.tags,
is_running: false,
proxy_bypass_rules: profile.proxy_bypass_rules,
},
}))
}
@@ -756,6 +761,15 @@ async fn update_profile(
}
}
if let Some(proxy_bypass_rules) = request.proxy_bypass_rules {
if profile_manager
.update_profile_proxy_bypass_rules(&state.app_handle, &id, proxy_bypass_rules)
.is_err()
{
return Err(StatusCode::BAD_REQUEST);
}
}
// Return updated profile
get_profile(Path(id), State(state)).await
}
+47
View File
@@ -2726,6 +2726,53 @@ pub async fn kill_browser_profile(
profile.name,
profile.id
);
// Auto-update non-running profiles and cleanup unused binaries
let browser_for_update = profile.browser.clone();
let app_handle_for_update = app_handle.clone();
tauri::async_runtime::spawn(async move {
let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance();
let mut versions = registry.get_downloaded_versions(&browser_for_update);
if !versions.is_empty() {
versions.sort_by(|a, b| crate::api_client::compare_versions(b, a));
let latest_version = &versions[0];
let auto_updater = crate::auto_updater::AutoUpdater::instance();
match auto_updater
.auto_update_profile_versions(
&app_handle_for_update,
&browser_for_update,
latest_version,
)
.await
{
Ok(updated) => {
if !updated.is_empty() {
log::info!(
"Auto-updated {} profiles after stop: {:?}",
updated.len(),
updated
);
}
}
Err(e) => {
log::error!("Failed to auto-update profile versions after stop: {e}");
}
}
}
match registry.cleanup_unused_binaries() {
Ok(cleaned) => {
if !cleaned.is_empty() {
log::info!("Cleaned up unused binaries after stop: {:?}", cleaned);
}
}
Err(e) => {
log::error!("Failed to cleanup unused binaries after stop: {e}");
}
}
});
Ok(())
}
Err(e) => {
+45
View File
@@ -1033,6 +1033,51 @@ impl Downloader {
tokens.remove(&download_key);
}
// Auto-update non-running profiles to the new version and cleanup unused binaries
{
let browser_for_update = browser_str.clone();
let version_for_update = version.clone();
let app_handle_for_update = app_handle.clone();
tauri::async_runtime::spawn(async move {
let auto_updater = crate::auto_updater::AutoUpdater::instance();
match auto_updater
.auto_update_profile_versions(
&app_handle_for_update,
&browser_for_update,
&version_for_update,
)
.await
{
Ok(updated) => {
if !updated.is_empty() {
log::info!(
"Auto-updated {} profiles to {} {}: {:?}",
updated.len(),
browser_for_update,
version_for_update,
updated
);
}
}
Err(e) => {
log::error!("Failed to auto-update profile versions: {e}");
}
}
let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance();
match registry.cleanup_unused_binaries() {
Ok(cleaned) => {
if !cleaned.is_empty() {
log::info!("Cleaned up unused binaries after download: {:?}", cleaned);
}
}
Err(e) => {
log::error!("Failed to cleanup unused binaries: {e}");
}
}
});
}
Ok(version)
}
}
+75
View File
@@ -725,6 +725,27 @@ impl McpServer {
"required": ["profile_id"]
}),
},
McpTool {
name: "update_profile_proxy_bypass_rules".to_string(),
description:
"Update proxy bypass rules for a profile. Requests matching these rules will connect directly, bypassing the proxy."
.to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"profile_id": {
"type": "string",
"description": "The UUID of the profile to update"
},
"rules": {
"type": "array",
"items": { "type": "string" },
"description": "Array of bypass rules. Supports hostnames (e.g. 'example.com'), IP addresses, and regex patterns."
}
},
"required": ["profile_id", "rules"]
}),
},
McpTool {
name: "list_extensions".to_string(),
description: "List all managed browser extensions. Requires Pro subscription.".to_string(),
@@ -889,6 +910,11 @@ impl McpServer {
// Fingerprint management
"get_profile_fingerprint" => self.handle_get_profile_fingerprint(&arguments).await,
"update_profile_fingerprint" => self.handle_update_profile_fingerprint(&arguments).await,
"update_profile_proxy_bypass_rules" => {
self
.handle_update_profile_proxy_bypass_rules(&arguments)
.await
}
// Extension management
"list_extensions" => self.handle_list_extensions().await,
"list_extension_groups" => self.handle_list_extension_groups().await,
@@ -2141,6 +2167,54 @@ impl McpServer {
}))
}
async fn handle_update_profile_proxy_bypass_rules(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let profile_id = arguments
.get("profile_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing profile_id".to_string(),
})?;
let rules: Vec<String> = arguments
.get("rules")
.and_then(|v| v.as_array())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing rules array".to_string(),
})?
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
let profile = ProfileManager::instance()
.update_profile_proxy_bypass_rules(app_handle, profile_id, rules.clone())
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to update proxy bypass rules: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!(
"Proxy bypass rules updated for profile '{}': {} rule(s) configured",
profile.name,
rules.len()
)
}]
}))
}
async fn handle_list_extensions(&self) -> Result<serde_json::Value, McpError> {
if !CLOUD_AUTH.has_active_paid_subscription().await {
return Err(McpError {
@@ -2366,6 +2440,7 @@ mod tests {
// Fingerprint tools
assert!(tool_names.contains(&"get_profile_fingerprint"));
assert!(tool_names.contains(&"update_profile_fingerprint"));
assert!(tool_names.contains(&"update_profile_proxy_bypass_rules"));
// Extension tools
assert!(tool_names.contains(&"list_extensions"));
assert!(tool_names.contains(&"list_extension_groups"));
+7 -3
View File
@@ -849,6 +849,7 @@ impl ProfileManager {
pub fn clone_profile(
&self,
profile_id: &str,
custom_name: Option<String>,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
let profile_uuid =
uuid::Uuid::parse_str(profile_id).map_err(|_| format!("Invalid profile ID: {profile_id}"))?;
@@ -865,7 +866,10 @@ impl ProfileManager {
}
let new_id = uuid::Uuid::new_v4();
let clone_name = self.generate_clone_name(&source.name)?;
let clone_name = match custom_name {
Some(name) if !name.trim().is_empty() => name.trim().to_string(),
_ => self.generate_clone_name(&source.name)?,
};
let profiles_dir = self.get_profiles_dir();
let source_dir = profiles_dir.join(source.id.to_string());
@@ -2185,9 +2189,9 @@ pub async fn update_wayfern_config(
}
#[tauri::command]
pub fn clone_profile(profile_id: String) -> Result<BrowserProfile, String> {
pub fn clone_profile(profile_id: String, name: Option<String>) -> Result<BrowserProfile, String> {
ProfileManager::instance()
.clone_profile(&profile_id)
.clone_profile(&profile_id, name)
.map_err(|e| format!("Failed to clone profile: {e}"))
}
+114
View File
@@ -238,6 +238,46 @@ fn create_test_profile_bundle(temp_dir: &Path) -> Vec<u8> {
encoder.finish().unwrap()
}
fn create_test_profile_bundle_with_bypass_rules(temp_dir: &Path, bypass_rules: &[&str]) -> Vec<u8> {
use flate2::write::GzEncoder;
use flate2::Compression;
use tar::Builder;
let metadata = json!({
"id": "test-bypass-profile-id",
"name": "Bypass Rules Profile",
"browser": "camoufox",
"version": "120.0.0",
"release_type": "stable",
"sync_enabled": true,
"tags": [],
"proxy_bypass_rules": bypass_rules
});
let profile_dir = temp_dir.join("bypass_profile");
fs::create_dir_all(&profile_dir).unwrap();
fs::write(profile_dir.join("test_file.txt"), "bypass test content").unwrap();
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
{
let mut tar = Builder::new(&mut encoder);
let metadata_json = serde_json::to_string_pretty(&metadata).unwrap();
let mut header = tar::Header::new_gnu();
header.set_size(metadata_json.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar
.append_data(&mut header, "metadata.json", metadata_json.as_bytes())
.unwrap();
tar.append_dir_all("profile", &profile_dir).unwrap();
tar.finish().unwrap();
}
encoder.finish().unwrap()
}
fn extract_bundle(data: &[u8], target_dir: &Path) -> serde_json::Value {
use flate2::read::GzDecoder;
use tar::Archive;
@@ -727,3 +767,77 @@ async fn test_delta_sync_only_changed_files() {
client.delete(&file2_key, None).await.unwrap();
client.delete(&file3_key, None).await.unwrap();
}
#[tokio::test]
async fn test_profile_bypass_rules_sync() {
ensure_sync_server_available().await;
let client = TestClient::new();
let temp_dir = TempDir::new().unwrap();
let profile_id = uuid::Uuid::new_v4().to_string();
let test_key = format!("profiles/{}.tar.gz", profile_id);
let bypass_rules = vec!["example.com", "192.168.1.0/24", ".*\\.internal\\.net"];
let bundle = create_test_profile_bundle_with_bypass_rules(temp_dir.path(), &bypass_rules);
let presign = client
.presign_upload(&test_key, "application/gzip")
.await
.unwrap();
client
.upload_bytes(&presign.url, &bundle, "application/gzip")
.await
.unwrap();
let stat = client.stat(&test_key).await.unwrap();
assert!(stat.exists);
// Download and verify bypass rules survive the round-trip
let download_presign = client.presign_download(&test_key).await.unwrap();
let downloaded = client.download_bytes(&download_presign.url).await.unwrap();
assert_eq!(downloaded.len(), bundle.len());
let extract_dir = temp_dir.path().join("extracted");
fs::create_dir_all(&extract_dir).unwrap();
let metadata = extract_bundle(&downloaded, &extract_dir);
assert_eq!(metadata["name"], "Bypass Rules Profile");
assert_eq!(metadata["browser"], "camoufox");
let synced_rules = metadata["proxy_bypass_rules"]
.as_array()
.expect("proxy_bypass_rules should be an array");
assert_eq!(synced_rules.len(), 3);
assert_eq!(synced_rules[0], "example.com");
assert_eq!(synced_rules[1], "192.168.1.0/24");
assert_eq!(synced_rules[2], ".*\\.internal\\.net");
// Also verify empty bypass rules are handled correctly
let empty_bundle = create_test_profile_bundle_with_bypass_rules(temp_dir.path(), &[]);
let empty_key = format!("profiles/{}.tar.gz", uuid::Uuid::new_v4());
let presign2 = client
.presign_upload(&empty_key, "application/gzip")
.await
.unwrap();
client
.upload_bytes(&presign2.url, &empty_bundle, "application/gzip")
.await
.unwrap();
let download_presign2 = client.presign_download(&empty_key).await.unwrap();
let downloaded2 = client.download_bytes(&download_presign2.url).await.unwrap();
let extract_dir2 = temp_dir.path().join("extracted2");
fs::create_dir_all(&extract_dir2).unwrap();
let metadata2 = extract_bundle(&downloaded2, &extract_dir2);
let empty_rules = metadata2["proxy_bypass_rules"]
.as_array()
.expect("proxy_bypass_rules should be an array");
assert!(empty_rules.is_empty());
// Cleanup
client.delete(&test_key, None).await.unwrap();
client.delete(&empty_key, None).await.unwrap();
}