chore(lint): clean up frontend and Rust lint issues
This commit is contained in:
@@ -8,9 +8,9 @@ use tauri::State;
|
||||
|
||||
use crate::acp::binary_cache;
|
||||
use crate::acp::error::AcpError;
|
||||
use crate::acp::opencode_plugins::{self, PluginCheckSummary};
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
use crate::acp::manager::ConnectionManager;
|
||||
use crate::acp::opencode_plugins::{self, PluginCheckSummary};
|
||||
use crate::acp::preflight::{self, PreflightResult};
|
||||
use crate::acp::registry;
|
||||
use crate::acp::types::{
|
||||
@@ -154,9 +154,7 @@ pub(crate) fn verify_agent_installed(agent_type: AgentType) -> Result<(), AcpErr
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
registry::AgentDistribution::Binary {
|
||||
cmd, platforms, ..
|
||||
} => {
|
||||
registry::AgentDistribution::Binary { cmd, platforms, .. } => {
|
||||
let platform = registry::current_platform();
|
||||
if !platforms.iter().any(|p| p.platform == platform) {
|
||||
return Err(AcpError::PlatformNotSupported(format!(
|
||||
@@ -212,7 +210,11 @@ async fn npm_list_version(
|
||||
prefix: Option<&std::path::Path>,
|
||||
) -> Option<String> {
|
||||
let mut cmd = crate::process::tokio_command(npm_path);
|
||||
cmd.arg("list").arg("-g").arg(package_name).arg("--json").arg("--depth=0");
|
||||
cmd.arg("list")
|
||||
.arg("-g")
|
||||
.arg(package_name)
|
||||
.arg("--json")
|
||||
.arg("--depth=0");
|
||||
if let Some(p) = prefix {
|
||||
cmd.arg(format!("--prefix={}", p.display()));
|
||||
}
|
||||
@@ -266,9 +268,9 @@ async fn run_npm_streaming(
|
||||
cmd.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
|
||||
let mut child = cmd.spawn().map_err(|e| {
|
||||
AcpError::protocol(format!("failed to spawn npm: {e}"))
|
||||
})?;
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| AcpError::protocol(format!("failed to spawn npm: {e}")))?;
|
||||
|
||||
let stdout = child.stdout.take();
|
||||
let stderr = child.stderr.take();
|
||||
@@ -283,9 +285,7 @@ async fn run_npm_streaming(
|
||||
let reader = BufReader::new(out);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
emit_agent_install_event(
|
||||
&emitter, &task_id, AgentInstallEventKind::Log, &line,
|
||||
);
|
||||
emit_agent_install_event(&emitter, &task_id, AgentInstallEventKind::Log, &line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,9 +300,7 @@ async fn run_npm_streaming(
|
||||
let reader = BufReader::new(err);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
emit_agent_install_event(
|
||||
&emitter, &task_id, AgentInstallEventKind::Log, &line,
|
||||
);
|
||||
emit_agent_install_event(&emitter, &task_id, AgentInstallEventKind::Log, &line);
|
||||
if !collected.is_empty() {
|
||||
collected.push('\n');
|
||||
}
|
||||
@@ -316,9 +314,10 @@ async fn run_npm_streaming(
|
||||
let (_, stderr_result) = tokio::join!(stdout_handle, stderr_handle);
|
||||
let collected_stderr = stderr_result.unwrap_or_default();
|
||||
|
||||
let status = child.wait().await.map_err(|e| {
|
||||
AcpError::protocol(format!("failed to wait for npm process: {e}"))
|
||||
})?;
|
||||
let status = child
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| AcpError::protocol(format!("failed to wait for npm process: {e}")))?;
|
||||
|
||||
Ok((status.success(), collected_stderr))
|
||||
}
|
||||
@@ -331,49 +330,58 @@ async fn install_npm_global_package_streaming(
|
||||
let registry_arg = format!("--registry={NPM_OFFICIAL_REGISTRY}");
|
||||
|
||||
emit_agent_install_event(
|
||||
emitter, task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
format!("$ npm install -g {package}"),
|
||||
);
|
||||
|
||||
let (success, stderr) = run_npm_streaming(
|
||||
&["install", "-g", ®istry_arg, package],
|
||||
task_id,
|
||||
emitter,
|
||||
).await?;
|
||||
let (success, stderr) =
|
||||
run_npm_streaming(&["install", "-g", ®istry_arg, package], task_id, emitter).await?;
|
||||
|
||||
if !success {
|
||||
// EACCES: permission denied — retry with a user-local --prefix so
|
||||
// we don't require root/sudo on macOS / Linux.
|
||||
if stderr.contains("EACCES") {
|
||||
emit_agent_install_event(
|
||||
emitter, task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
"Permission denied, retrying with user prefix...",
|
||||
);
|
||||
return install_npm_to_user_prefix_streaming(
|
||||
package, ®istry_arg, task_id, emitter,
|
||||
).await;
|
||||
return install_npm_to_user_prefix_streaming(package, ®istry_arg, task_id, emitter)
|
||||
.await;
|
||||
}
|
||||
|
||||
// EEXIST: file conflict — retry with --force to overwrite
|
||||
if stderr.contains("EEXIST") {
|
||||
emit_agent_install_event(
|
||||
emitter, task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
"File conflict, retrying with --force...",
|
||||
);
|
||||
let (retry_success, retry_stderr) = run_npm_streaming(
|
||||
&["install", "-g", "--force", ®istry_arg, package],
|
||||
task_id,
|
||||
emitter,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
if !retry_success {
|
||||
if retry_stderr.contains("EACCES") {
|
||||
emit_agent_install_event(
|
||||
emitter, task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
"Permission denied on --force retry, falling back to user prefix...",
|
||||
);
|
||||
return install_npm_to_user_prefix_streaming(
|
||||
package, ®istry_arg, task_id, emitter,
|
||||
).await;
|
||||
package,
|
||||
®istry_arg,
|
||||
task_id,
|
||||
emitter,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let err = retry_stderr.trim().to_string();
|
||||
let msg = if err.is_empty() {
|
||||
@@ -424,7 +432,9 @@ async fn install_npm_to_user_prefix_streaming(
|
||||
let prefix_arg = format!("--prefix={}", prefix.display());
|
||||
|
||||
emit_agent_install_event(
|
||||
emitter, task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
format!("$ npm install -g --prefix={} {package}", prefix.display()),
|
||||
);
|
||||
|
||||
@@ -432,21 +442,32 @@ async fn install_npm_to_user_prefix_streaming(
|
||||
&["install", "-g", &prefix_arg, registry_arg, package],
|
||||
task_id,
|
||||
emitter,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !success {
|
||||
// EEXIST in the user prefix: retry with --force to overwrite stale files
|
||||
// from a previous installation.
|
||||
if stderr.contains("EEXIST") {
|
||||
emit_agent_install_event(
|
||||
emitter, task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
"File conflict in user prefix, retrying with --force...",
|
||||
);
|
||||
let (force_success, force_stderr) = run_npm_streaming(
|
||||
&["install", "-g", "--force", &prefix_arg, registry_arg, package],
|
||||
&[
|
||||
"install",
|
||||
"-g",
|
||||
"--force",
|
||||
&prefix_arg,
|
||||
registry_arg,
|
||||
package,
|
||||
],
|
||||
task_id,
|
||||
emitter,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
if !force_success {
|
||||
let err = force_stderr.trim().to_string();
|
||||
let msg = if err.is_empty() {
|
||||
@@ -854,10 +875,7 @@ fn persist_cline_local_config(config_patch_json: Option<&str>) -> Result<(), Acp
|
||||
act_model_key.to_string(),
|
||||
serde_json::Value::String(model.clone()),
|
||||
);
|
||||
gs_obj.insert(
|
||||
plan_model_key.to_string(),
|
||||
serde_json::Value::String(model),
|
||||
);
|
||||
gs_obj.insert(plan_model_key.to_string(), serde_json::Value::String(model));
|
||||
}
|
||||
None => {
|
||||
gs_obj.remove(act_model_key);
|
||||
@@ -888,9 +906,8 @@ fn persist_cline_local_config(config_patch_json: Option<&str>) -> Result<(), Acp
|
||||
}
|
||||
|
||||
if let Some(parent) = gs_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| {
|
||||
AcpError::protocol(format!("create cline data directory failed: {e}"))
|
||||
})?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| AcpError::protocol(format!("create cline data directory failed: {e}")))?;
|
||||
}
|
||||
let serialized_gs = serde_json::to_string_pretty(&gs)
|
||||
.map_err(|e| AcpError::protocol(format!("serialize cline globalState failed: {e}")))?;
|
||||
@@ -917,10 +934,7 @@ fn persist_cline_local_config(config_patch_json: Option<&str>) -> Result<(), Acp
|
||||
let key_field = cline_api_key_field_for_provider(&provider);
|
||||
match trim_non_empty(runtime.api_key) {
|
||||
Some(api_key) => {
|
||||
secrets_obj.insert(
|
||||
key_field.to_string(),
|
||||
serde_json::Value::String(api_key),
|
||||
);
|
||||
secrets_obj.insert(key_field.to_string(), serde_json::Value::String(api_key));
|
||||
}
|
||||
None => {
|
||||
secrets_obj.remove(key_field);
|
||||
@@ -928,9 +942,8 @@ fn persist_cline_local_config(config_patch_json: Option<&str>) -> Result<(), Acp
|
||||
}
|
||||
|
||||
if let Some(parent) = secrets_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| {
|
||||
AcpError::protocol(format!("create cline data directory failed: {e}"))
|
||||
})?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| AcpError::protocol(format!("create cline data directory failed: {e}")))?;
|
||||
}
|
||||
let serialized_secrets = serde_json::to_string_pretty(&secrets)
|
||||
.map_err(|e| AcpError::protocol(format!("serialize cline secrets failed: {e}")))?;
|
||||
@@ -1719,7 +1732,11 @@ fn trim_non_empty(value: Option<String>) -> Option<String> {
|
||||
/// Shared by runtime env resolution, model-provider cascade, and config patching.
|
||||
fn agent_env_keys(agent_type: AgentType) -> (&'static str, &'static str, &'static str) {
|
||||
match agent_type {
|
||||
AgentType::ClaudeCode => ("ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_MODEL"),
|
||||
AgentType::ClaudeCode => (
|
||||
"ANTHROPIC_BASE_URL",
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"ANTHROPIC_MODEL",
|
||||
),
|
||||
AgentType::Gemini => ("GOOGLE_GEMINI_BASE_URL", "GEMINI_API_KEY", "GEMINI_MODEL"),
|
||||
_ => ("OPENAI_BASE_URL", "OPENAI_API_KEY", "OPENAI_MODEL"),
|
||||
}
|
||||
@@ -1814,11 +1831,17 @@ fn cascade_update_agent_config(
|
||||
AgentType::ClaudeCode | AgentType::Gemini => {
|
||||
// Write into config.env (not root-level)
|
||||
let mut env = serde_json::Map::new();
|
||||
env.insert(url_key.to_string(), serde_json::Value::String(api_url.to_string()));
|
||||
env.insert(key_key.to_string(), serde_json::Value::String(api_key.to_string()));
|
||||
env.insert(
|
||||
url_key.to_string(),
|
||||
serde_json::Value::String(api_url.to_string()),
|
||||
);
|
||||
env.insert(
|
||||
key_key.to_string(),
|
||||
serde_json::Value::String(api_key.to_string()),
|
||||
);
|
||||
let patch = serde_json::json!({ "env": env });
|
||||
let patch_str = serde_json::to_string(&patch)
|
||||
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||
let patch_str =
|
||||
serde_json::to_string(&patch).map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||
persist_agent_local_config_json(agent_type, Some(&patch_str))?;
|
||||
}
|
||||
AgentType::OpenClaw => {
|
||||
@@ -1855,7 +1878,10 @@ fn cascade_update_agent_config(
|
||||
if api_url.trim().is_empty() {
|
||||
table.remove("api_base_url");
|
||||
} else {
|
||||
table.insert("api_base_url".to_string(), toml::Value::String(api_url.to_string()));
|
||||
table.insert(
|
||||
"api_base_url".to_string(),
|
||||
toml::Value::String(api_url.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
let toml_str = toml::to_string_pretty(&toml_value)
|
||||
@@ -1882,8 +1908,8 @@ fn cascade_update_agent_config(
|
||||
persist_opencode_auth_json(&auth_str)?;
|
||||
|
||||
let patch = serde_json::json!({ "apiBaseUrl": api_url });
|
||||
let patch_str = serde_json::to_string(&patch)
|
||||
.map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||
let patch_str =
|
||||
serde_json::to_string(&patch).map_err(|e| AcpError::protocol(e.to_string()))?;
|
||||
persist_agent_local_config_json(agent_type, Some(&patch_str))?;
|
||||
}
|
||||
AgentType::Cline => {}
|
||||
@@ -2106,17 +2132,11 @@ pub(crate) async fn acp_get_agent_status_core(
|
||||
true,
|
||||
setting.as_ref().and_then(|m| m.installed_version.clone()),
|
||||
),
|
||||
registry::AgentDistribution::Binary {
|
||||
platforms, cmd, ..
|
||||
} => {
|
||||
let detected =
|
||||
binary_cache::detect_installed_version(agent_type, cmd)
|
||||
.ok()
|
||||
.flatten();
|
||||
(
|
||||
platforms.iter().any(|p| p.platform == platform),
|
||||
detected,
|
||||
)
|
||||
registry::AgentDistribution::Binary { platforms, cmd, .. } => {
|
||||
let detected = binary_cache::detect_installed_version(agent_type, cmd)
|
||||
.ok()
|
||||
.flatten();
|
||||
(platforms.iter().any(|p| p.platform == platform), detected)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2137,9 +2157,7 @@ pub async fn acp_get_agent_status(
|
||||
acp_get_agent_status_core(agent_type, &db).await
|
||||
}
|
||||
|
||||
pub(crate) async fn acp_list_agents_core(
|
||||
db: &AppDatabase,
|
||||
) -> Result<Vec<AcpAgentInfo>, AcpError> {
|
||||
pub(crate) async fn acp_list_agents_core(db: &AppDatabase) -> Result<Vec<AcpAgentInfo>, AcpError> {
|
||||
let platform = registry::current_platform();
|
||||
let agent_types = registry::all_acp_agents();
|
||||
|
||||
@@ -2417,9 +2435,17 @@ pub async fn acp_update_agent_preferences(
|
||||
) -> Result<(), AcpError> {
|
||||
let emitter = EventEmitter::Tauri(app);
|
||||
acp_update_agent_preferences_core(
|
||||
agent_type, enabled, env, config_json, opencode_auth_json,
|
||||
codex_auth_json, codex_config_toml, &db, &emitter,
|
||||
).await
|
||||
agent_type,
|
||||
enabled,
|
||||
env,
|
||||
config_json,
|
||||
opencode_auth_json,
|
||||
codex_auth_json,
|
||||
codex_config_toml,
|
||||
&db,
|
||||
&emitter,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn acp_update_agent_env_core(
|
||||
@@ -2557,7 +2583,12 @@ pub async fn acp_update_agent_config(
|
||||
) -> Result<(), AcpError> {
|
||||
let emitter = EventEmitter::Tauri(app);
|
||||
acp_update_agent_config_core(
|
||||
agent_type, config_json, opencode_auth_json, codex_auth_json, codex_config_toml, &emitter,
|
||||
agent_type,
|
||||
config_json,
|
||||
opencode_auth_json,
|
||||
codex_auth_json,
|
||||
codex_config_toml,
|
||||
&emitter,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2589,17 +2620,25 @@ pub(crate) async fn acp_download_agent_binary_core(
|
||||
})?;
|
||||
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
format!("Downloading {} v{version} for {platform}", meta.name),
|
||||
);
|
||||
|
||||
let emitter_clone = emitter.clone();
|
||||
let task_id_clone = task_id.clone();
|
||||
let _ = binary_cache::ensure_binary_for_agent_with_progress(
|
||||
agent_type, version, fallback.url, cmd,
|
||||
agent_type,
|
||||
version,
|
||||
fallback.url,
|
||||
cmd,
|
||||
move |msg| {
|
||||
emit_agent_install_event(
|
||||
&emitter_clone, &task_id_clone, AgentInstallEventKind::Log, msg,
|
||||
&emitter_clone,
|
||||
&task_id_clone,
|
||||
AgentInstallEventKind::Log,
|
||||
msg,
|
||||
);
|
||||
},
|
||||
)
|
||||
@@ -2607,21 +2646,26 @@ pub(crate) async fn acp_download_agent_binary_core(
|
||||
emit_acp_agents_updated(emitter, "binary_downloaded", Some(agent_type));
|
||||
Ok(())
|
||||
}
|
||||
registry::AgentDistribution::Npx { .. } => Err(
|
||||
AcpError::protocol("download is only supported for binary agents"),
|
||||
),
|
||||
registry::AgentDistribution::Npx { .. } => Err(AcpError::protocol(
|
||||
"download is only supported for binary agents",
|
||||
)),
|
||||
};
|
||||
|
||||
match &result {
|
||||
Ok(()) => {
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Completed,
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Completed,
|
||||
format!("{} installed successfully", meta.name),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Failed, e.to_string(),
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Failed,
|
||||
e.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2645,12 +2689,9 @@ pub(crate) async fn acp_detect_agent_local_version_core(
|
||||
) -> Result<Option<String>, AcpError> {
|
||||
let detected = detect_local_version(agent_type).await;
|
||||
if let Some(version) = detected.clone() {
|
||||
let _ = agent_setting_service::set_installed_version(
|
||||
conn,
|
||||
agent_type,
|
||||
Some(version.clone()),
|
||||
)
|
||||
.await;
|
||||
let _ =
|
||||
agent_setting_service::set_installed_version(conn, agent_type, Some(version.clone()))
|
||||
.await;
|
||||
return Ok(Some(version));
|
||||
}
|
||||
|
||||
@@ -2699,13 +2740,17 @@ pub(crate) async fn acp_prepare_npx_agent_core(
|
||||
.and_then(|m| m.installed_version);
|
||||
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
format!("Installing {} ({package})", meta.name),
|
||||
);
|
||||
install_npm_global_package_streaming(package, &task_id, emitter).await?;
|
||||
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
"Detecting installed version...",
|
||||
);
|
||||
let resolved = detect_local_version(agent_type)
|
||||
@@ -2741,13 +2786,18 @@ pub(crate) async fn acp_prepare_npx_agent_core(
|
||||
match &result {
|
||||
Ok(version) => {
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Completed,
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Completed,
|
||||
format!("{} v{version} installed successfully", meta.name),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Failed, e.to_string(),
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Failed,
|
||||
e.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2777,7 +2827,9 @@ pub(crate) async fn acp_uninstall_agent_core(
|
||||
|
||||
let meta = registry::get_agent_meta(agent_type);
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Log,
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Log,
|
||||
format!("Uninstalling {}...", meta.name),
|
||||
);
|
||||
|
||||
@@ -2802,13 +2854,18 @@ pub(crate) async fn acp_uninstall_agent_core(
|
||||
match &result {
|
||||
Ok(()) => {
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Completed,
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Completed,
|
||||
format!("{} uninstalled successfully", meta.name),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
emit_agent_install_event(
|
||||
emitter, &task_id, AgentInstallEventKind::Failed, e.to_string(),
|
||||
emitter,
|
||||
&task_id,
|
||||
AgentInstallEventKind::Failed,
|
||||
e.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2868,9 +2925,7 @@ pub async fn acp_list_agent_skills(
|
||||
let Some(spec) = skill_storage_spec(agent_type) else {
|
||||
return Ok(AgentSkillsListResult {
|
||||
supported: false,
|
||||
message: Some(format!(
|
||||
"{agent_type} 暂不支持在设置页管理 Skills"
|
||||
)),
|
||||
message: Some(format!("{agent_type} 暂不支持在设置页管理 Skills")),
|
||||
locations: Vec::new(),
|
||||
skills: Vec::new(),
|
||||
});
|
||||
@@ -3033,8 +3088,7 @@ pub async fn acp_delete_agent_skill(
|
||||
}
|
||||
|
||||
pub(crate) async fn opencode_list_plugins_core() -> Result<PluginCheckSummary, AcpError> {
|
||||
opencode_plugins::check_opencode_plugins(None)
|
||||
.map_err(|e| AcpError::Protocol(e))
|
||||
opencode_plugins::check_opencode_plugins(None).map_err(AcpError::Protocol)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
@@ -3049,7 +3103,7 @@ pub(crate) async fn opencode_install_plugins_core(
|
||||
) -> Result<(), AcpError> {
|
||||
opencode_plugins::install_missing_plugins(names, task_id, emitter)
|
||||
.await
|
||||
.map_err(|e| AcpError::Protocol(e))
|
||||
.map_err(AcpError::Protocol)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
@@ -3068,13 +3122,11 @@ pub(crate) async fn opencode_uninstall_plugin_core(
|
||||
) -> Result<PluginCheckSummary, AcpError> {
|
||||
opencode_plugins::uninstall_plugin(name)
|
||||
.await
|
||||
.map_err(|e| AcpError::Protocol(e))
|
||||
.map_err(AcpError::Protocol)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn opencode_uninstall_plugin(
|
||||
name: String,
|
||||
) -> Result<PluginCheckSummary, AcpError> {
|
||||
pub async fn opencode_uninstall_plugin(name: String) -> Result<PluginCheckSummary, AcpError> {
|
||||
opencode_uninstall_plugin_core(name).await
|
||||
}
|
||||
|
||||
@@ -3108,7 +3160,10 @@ struct DeviceCodeUserCodeResp {
|
||||
device_auth_id: String,
|
||||
#[serde(alias = "usercode")]
|
||||
user_code: String,
|
||||
#[serde(default = "default_interval", deserialize_with = "deserialize_interval")]
|
||||
#[serde(
|
||||
default = "default_interval",
|
||||
deserialize_with = "deserialize_interval"
|
||||
)]
|
||||
interval: u64,
|
||||
}
|
||||
|
||||
@@ -3118,11 +3173,8 @@ fn default_interval() -> u64 {
|
||||
|
||||
fn extract_jwt_account_id(jwt: &str) -> Option<String> {
|
||||
let payload = jwt.split('.').nth(1)?;
|
||||
let decoded = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
|
||||
payload,
|
||||
)
|
||||
.ok()?;
|
||||
let decoded =
|
||||
base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, payload).ok()?;
|
||||
let value: serde_json::Value = serde_json::from_slice(&decoded).ok()?;
|
||||
value
|
||||
.get("https://api.openai.com/auth")
|
||||
@@ -3138,11 +3190,13 @@ where
|
||||
use serde::de;
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
match &value {
|
||||
serde_json::Value::Number(n) => n.as_u64().ok_or_else(|| {
|
||||
de::Error::custom(format!("invalid interval number: {n}"))
|
||||
}),
|
||||
serde_json::Value::Number(n) => n
|
||||
.as_u64()
|
||||
.ok_or_else(|| de::Error::custom(format!("invalid interval number: {n}"))),
|
||||
serde_json::Value::String(s) => s.trim().parse::<u64>().map_err(de::Error::custom),
|
||||
_ => Err(de::Error::custom(format!("unexpected interval type: {value}"))),
|
||||
_ => Err(de::Error::custom(format!(
|
||||
"unexpected interval type: {value}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3161,9 +3215,7 @@ struct OAuthTokenResp {
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
pub(crate) async fn codex_request_device_code_core()
|
||||
-> Result<CodexDeviceCodeResponse, AcpError>
|
||||
{
|
||||
pub(crate) async fn codex_request_device_code_core() -> Result<CodexDeviceCodeResponse, AcpError> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{CODEX_OAUTH_ISSUER}/api/accounts/deviceauth/usercode");
|
||||
let body = serde_json::json!({ "client_id": CODEX_OAUTH_CLIENT_ID });
|
||||
@@ -3187,8 +3239,11 @@ pub(crate) async fn codex_request_device_code_core()
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| AcpError::protocol(format!("read device code response failed: {e}")))?;
|
||||
let uc: DeviceCodeUserCodeResp = serde_json::from_str(&raw_body)
|
||||
.map_err(|e| AcpError::protocol(format!("parse device code response failed: {e} | body: {raw_body}")))?;
|
||||
let uc: DeviceCodeUserCodeResp = serde_json::from_str(&raw_body).map_err(|e| {
|
||||
AcpError::protocol(format!(
|
||||
"parse device code response failed: {e} | body: {raw_body}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(CodexDeviceCodeResponse {
|
||||
user_code: uc.user_code,
|
||||
@@ -3199,9 +3254,7 @@ pub(crate) async fn codex_request_device_code_core()
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn codex_request_device_code()
|
||||
-> Result<CodexDeviceCodeResponse, AcpError>
|
||||
{
|
||||
pub async fn codex_request_device_code() -> Result<CodexDeviceCodeResponse, AcpError> {
|
||||
codex_request_device_code_core().await
|
||||
}
|
||||
|
||||
|
||||
@@ -98,20 +98,19 @@ pub async fn connect_chat_channel_core(
|
||||
.map_err(AppCommandError::from)?
|
||||
.ok_or_else(|| AppCommandError::not_found(format!("Chat channel {id} not found")))?;
|
||||
|
||||
let channel_type: ChannelType =
|
||||
serde_json::from_value(serde_json::Value::String(model.channel_type.clone()))
|
||||
.map_err(|_| {
|
||||
AppCommandError::configuration_invalid(format!(
|
||||
"Invalid channel type: {}",
|
||||
model.channel_type
|
||||
))
|
||||
})?;
|
||||
let channel_type: ChannelType = serde_json::from_value(serde_json::Value::String(
|
||||
model.channel_type.clone(),
|
||||
))
|
||||
.map_err(|_| {
|
||||
AppCommandError::configuration_invalid(format!(
|
||||
"Invalid channel type: {}",
|
||||
model.channel_type
|
||||
))
|
||||
})?;
|
||||
|
||||
let config: serde_json::Value =
|
||||
serde_json::from_str(&model.config_json).map_err(|e| {
|
||||
AppCommandError::configuration_invalid("Invalid config JSON")
|
||||
.with_detail(e.to_string())
|
||||
})?;
|
||||
let config: serde_json::Value = serde_json::from_str(&model.config_json).map_err(|e| {
|
||||
AppCommandError::configuration_invalid("Invalid config JSON").with_detail(e.to_string())
|
||||
})?;
|
||||
|
||||
let token = crate::keyring_store::get_channel_token(id).ok_or_else(|| {
|
||||
eprintln!("[connect_chat_channel] channel {id}: Token not set in keyring");
|
||||
@@ -138,29 +137,25 @@ pub async fn connect_chat_channel_core(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn test_chat_channel_core(
|
||||
db: &AppDatabase,
|
||||
id: i32,
|
||||
) -> Result<(), AppCommandError> {
|
||||
pub async fn test_chat_channel_core(db: &AppDatabase, id: i32) -> Result<(), AppCommandError> {
|
||||
let model = chat_channel_service::get_by_id(&db.conn, id)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?
|
||||
.ok_or_else(|| AppCommandError::not_found(format!("Chat channel {id} not found")))?;
|
||||
|
||||
let channel_type: ChannelType =
|
||||
serde_json::from_value(serde_json::Value::String(model.channel_type.clone()))
|
||||
.map_err(|_| {
|
||||
AppCommandError::configuration_invalid(format!(
|
||||
"Invalid channel type: {}",
|
||||
model.channel_type
|
||||
))
|
||||
})?;
|
||||
let channel_type: ChannelType = serde_json::from_value(serde_json::Value::String(
|
||||
model.channel_type.clone(),
|
||||
))
|
||||
.map_err(|_| {
|
||||
AppCommandError::configuration_invalid(format!(
|
||||
"Invalid channel type: {}",
|
||||
model.channel_type
|
||||
))
|
||||
})?;
|
||||
|
||||
let config: serde_json::Value =
|
||||
serde_json::from_str(&model.config_json).map_err(|e| {
|
||||
AppCommandError::configuration_invalid("Invalid config JSON")
|
||||
.with_detail(e.to_string())
|
||||
})?;
|
||||
let config: serde_json::Value = serde_json::from_str(&model.config_json).map_err(|e| {
|
||||
AppCommandError::configuration_invalid("Invalid config JSON").with_detail(e.to_string())
|
||||
})?;
|
||||
|
||||
let token = crate::keyring_store::get_channel_token(id)
|
||||
.ok_or_else(|| AppCommandError::configuration_missing("Token not set"))?;
|
||||
@@ -215,18 +210,20 @@ pub async fn list_chat_channel_messages_core(
|
||||
) -> Result<Vec<ChatChannelMessageLogInfo>, AppCommandError> {
|
||||
let limit = limit.unwrap_or(50);
|
||||
let offset = offset.unwrap_or(0);
|
||||
let rows = chat_channel_message_log_service::list_by_channel(&db.conn, channel_id, limit, offset)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(rows.into_iter().map(ChatChannelMessageLogInfo::from).collect())
|
||||
let rows =
|
||||
chat_channel_message_log_service::list_by_channel(&db.conn, channel_id, limit, offset)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(ChatChannelMessageLogInfo::from)
|
||||
.collect())
|
||||
}
|
||||
|
||||
const COMMAND_PREFIX_KEY: &str = "chat_command_prefix";
|
||||
const DEFAULT_COMMAND_PREFIX: &str = "/";
|
||||
|
||||
pub async fn get_chat_command_prefix_core(
|
||||
db: &AppDatabase,
|
||||
) -> Result<String, AppCommandError> {
|
||||
pub async fn get_chat_command_prefix_core(db: &AppDatabase) -> Result<String, AppCommandError> {
|
||||
let val = crate::db::service::app_metadata_service::get_value(&db.conn, COMMAND_PREFIX_KEY)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
@@ -238,10 +235,7 @@ pub async fn set_chat_command_prefix_core(
|
||||
prefix: String,
|
||||
) -> Result<(), AppCommandError> {
|
||||
let trimmed = prefix.trim();
|
||||
if trimmed.is_empty()
|
||||
|| trimmed.len() > 3
|
||||
|| trimmed.chars().any(|c| c.is_alphanumeric())
|
||||
{
|
||||
if trimmed.is_empty() || trimmed.len() > 3 || trimmed.chars().any(|c| c.is_alphanumeric()) {
|
||||
return Err(AppCommandError::invalid_input(
|
||||
"Prefix must be 1-3 non-alphanumeric characters",
|
||||
));
|
||||
@@ -254,9 +248,7 @@ pub async fn set_chat_command_prefix_core(
|
||||
|
||||
const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language";
|
||||
|
||||
pub async fn get_chat_message_language_core(
|
||||
db: &AppDatabase,
|
||||
) -> Result<String, AppCommandError> {
|
||||
pub async fn get_chat_message_language_core(db: &AppDatabase) -> Result<String, AppCommandError> {
|
||||
let val = crate::db::service::app_metadata_service::get_value(&db.conn, MESSAGE_LANGUAGE_KEY)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
@@ -360,14 +352,20 @@ pub async fn weixin_check_qrcode_core(
|
||||
if result.status == "confirmed" {
|
||||
eprintln!(
|
||||
"[Weixin] QR confirmed for channel {channel_id}, bot_token={}, base_url={}",
|
||||
result.bot_token.as_deref().map(|t| if t.len() > 8 { &t[..8] } else { t }).unwrap_or("None"),
|
||||
result
|
||||
.bot_token
|
||||
.as_deref()
|
||||
.map(|t| if t.len() > 8 { &t[..8] } else { t })
|
||||
.unwrap_or("None"),
|
||||
result.base_url.as_deref().unwrap_or("None"),
|
||||
);
|
||||
if let Some(ref token) = result.bot_token {
|
||||
save_chat_channel_token_core(channel_id, token)?;
|
||||
eprintln!("[Weixin] Token saved for channel {channel_id}");
|
||||
} else {
|
||||
eprintln!("[Weixin] WARNING: No bot_token in confirmed response for channel {channel_id}");
|
||||
eprintln!(
|
||||
"[Weixin] WARNING: No bot_token in confirmed response for channel {channel_id}"
|
||||
);
|
||||
}
|
||||
if let Some(ref base_url) = result.base_url {
|
||||
let config_json = serde_json::json!({ "base_url": base_url }).to_string();
|
||||
@@ -415,7 +413,16 @@ pub async fn create_chat_channel(
|
||||
daily_report_enabled: bool,
|
||||
daily_report_time: Option<String>,
|
||||
) -> Result<ChatChannelInfo, AppCommandError> {
|
||||
create_chat_channel_core(&db, name, channel_type, config_json, enabled, daily_report_enabled, daily_report_time).await
|
||||
create_chat_channel_core(
|
||||
&db,
|
||||
name,
|
||||
channel_type,
|
||||
config_json,
|
||||
enabled,
|
||||
daily_report_enabled,
|
||||
daily_report_time,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -431,7 +438,17 @@ pub async fn update_chat_channel(
|
||||
daily_report_enabled: Option<bool>,
|
||||
daily_report_time: Option<Option<String>>,
|
||||
) -> Result<ChatChannelInfo, AppCommandError> {
|
||||
update_chat_channel_core(&db, id, name, enabled, config_json, event_filter_json, daily_report_enabled, daily_report_time).await
|
||||
update_chat_channel_core(
|
||||
&db,
|
||||
id,
|
||||
name,
|
||||
enabled,
|
||||
config_json,
|
||||
event_filter_json,
|
||||
daily_report_enabled,
|
||||
daily_report_time,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
@@ -446,7 +463,10 @@ pub async fn delete_chat_channel(
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[tauri::command]
|
||||
pub async fn save_chat_channel_token(channel_id: i32, token: String) -> Result<(), AppCommandError> {
|
||||
pub async fn save_chat_channel_token(
|
||||
channel_id: i32,
|
||||
token: String,
|
||||
) -> Result<(), AppCommandError> {
|
||||
save_chat_channel_token_core(channel_id, &token)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::collections::{HashMap, HashSet};
|
||||
use crate::app_error::AppCommandError;
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
use crate::db::entities::conversation;
|
||||
use crate::db::service::{conversation_service, folder_service};
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
use crate::db::service::import_service;
|
||||
use crate::db::service::{conversation_service, folder_service};
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
use crate::db::AppDatabase;
|
||||
use crate::models::*;
|
||||
@@ -288,8 +288,7 @@ pub async fn get_folder_conversation_core(
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
let (turns, session_stats, resolved_ext_id) = if let Some(ref ext_id) = summary.external_id
|
||||
{
|
||||
let (turns, session_stats, resolved_ext_id) = if let Some(ref ext_id) = summary.external_id {
|
||||
let at = summary.agent_type;
|
||||
let eid = ext_id.clone();
|
||||
let db_created_at = summary.created_at;
|
||||
@@ -317,7 +316,10 @@ pub async fn get_folder_conversation_core(
|
||||
// ID after session/new fallback overwrote the original
|
||||
// (Gemini CLI). Fall back to matching by folder_path
|
||||
// and started_at from the parsed conversation list.
|
||||
if matches!(at, AgentType::OpenClaw | AgentType::Cline | AgentType::Gemini) {
|
||||
if matches!(
|
||||
at,
|
||||
AgentType::OpenClaw | AgentType::Cline | AgentType::Gemini
|
||||
) {
|
||||
if let Ok(all) = parser.list_conversations() {
|
||||
// Filter by folder_path first, then find the closest
|
||||
// started_at match within 300 seconds of db_created_at.
|
||||
@@ -333,17 +335,14 @@ pub async fn get_folder_conversation_core(
|
||||
(c.started_at - db_created_at).num_seconds().unsigned_abs()
|
||||
})
|
||||
.filter(|c| {
|
||||
let diff = (c.started_at - db_created_at).num_seconds().unsigned_abs();
|
||||
let diff =
|
||||
(c.started_at - db_created_at).num_seconds().unsigned_abs();
|
||||
diff < 300
|
||||
});
|
||||
if let Some(conv) = matched {
|
||||
let new_ext_id = conv.id.clone();
|
||||
if let Ok(d) = parser.get_conversation(&new_ext_id) {
|
||||
return Ok((
|
||||
d.turns,
|
||||
d.session_stats,
|
||||
Some(new_ext_id),
|
||||
));
|
||||
return Ok((d.turns, d.session_stats, Some(new_ext_id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,8 +366,7 @@ pub async fn get_folder_conversation_core(
|
||||
// If we resolved a different external_id (e.g. ACP UUID → parser branch ID),
|
||||
// update the database so future lookups are direct.
|
||||
if let Some(new_ext_id) = resolved_ext_id {
|
||||
let _ =
|
||||
conversation_service::update_external_id(conn, conversation_id, new_ext_id).await;
|
||||
let _ = conversation_service::update_external_id(conn, conversation_id, new_ext_id).await;
|
||||
}
|
||||
|
||||
let mut summary = summary;
|
||||
|
||||
@@ -23,11 +23,11 @@ use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::acp::types::AgentSkillScope;
|
||||
use crate::commands::acp::{
|
||||
preferred_scope_skill_dir, remove_skill_entry, scoped_skill_dirs, skill_storage_spec,
|
||||
validate_skill_id,
|
||||
};
|
||||
use crate::acp::types::AgentSkillScope;
|
||||
use crate::models::agent::AgentType;
|
||||
|
||||
// ─── Embedded bundle ────────────────────────────────────────────────────
|
||||
@@ -373,9 +373,7 @@ fn create_link_raw(src: &Path, dst: &Path) -> io::Result<bool> {
|
||||
copy_dir_recursive(src, dst).map_err(|copy_err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"junction failed ({junction_err}); copy fallback failed ({copy_err})"
|
||||
),
|
||||
format!("junction failed ({junction_err}); copy fallback failed ({copy_err})"),
|
||||
)
|
||||
})?;
|
||||
Ok(true)
|
||||
@@ -518,9 +516,7 @@ fn ensure_central_experts_installed_blocking() -> InstallReport {
|
||||
report.pending_user_review.push(meta.id.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
report
|
||||
.errors
|
||||
.push(format!("{}: {}", meta.id, e));
|
||||
report.errors.push(format!("{}: {}", meta.id, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -639,10 +635,10 @@ fn extract_bundle_dir(
|
||||
.path()
|
||||
.to_str()
|
||||
.ok_or_else(|| ExpertsError::Io("non-utf8 path in bundle".into()))?;
|
||||
let rel_within =
|
||||
rel.strip_prefix(bundle_prefix)
|
||||
.and_then(|s| s.strip_prefix('/'))
|
||||
.unwrap_or(rel);
|
||||
let rel_within = rel
|
||||
.strip_prefix(bundle_prefix)
|
||||
.and_then(|s| s.strip_prefix('/'))
|
||||
.unwrap_or(rel);
|
||||
let out_path = target.join(rel_within);
|
||||
if let Some(parent) = out_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
@@ -686,8 +682,7 @@ pub async fn experts_list() -> Result<Vec<ExpertListItem>, ExpertsError> {
|
||||
pub async fn experts_list_for_agent(
|
||||
agent_type: AgentType,
|
||||
) -> Result<Vec<ExpertListItem>, ExpertsError> {
|
||||
let _ = skill_storage_spec(agent_type)
|
||||
.ok_or(ExpertsError::UnsupportedAgent(agent_type))?;
|
||||
let _ = skill_storage_spec(agent_type).ok_or(ExpertsError::UnsupportedAgent(agent_type))?;
|
||||
|
||||
let dirs = scoped_skill_dirs(agent_type, AgentSkillScope::Global, None)
|
||||
.map_err(|_| ExpertsError::UnsupportedAgent(agent_type))?;
|
||||
@@ -727,8 +722,8 @@ pub async fn experts_list_for_agent(
|
||||
pub async fn experts_get_install_status(
|
||||
expert_id: String,
|
||||
) -> Result<Vec<ExpertInstallStatus>, ExpertsError> {
|
||||
let expert_id = validate_skill_id(&expert_id)
|
||||
.map_err(|e| ExpertsError::Metadata(e.to_string()))?;
|
||||
let expert_id =
|
||||
validate_skill_id(&expert_id).map_err(|e| ExpertsError::Metadata(e.to_string()))?;
|
||||
let _ = find_metadata(&expert_id)?; // ensure it exists in the bundle
|
||||
let expected = expert_central_path(&expert_id);
|
||||
let agents = supported_agents();
|
||||
@@ -776,8 +771,8 @@ pub async fn experts_link_to_agent(
|
||||
expert_id: String,
|
||||
agent_type: AgentType,
|
||||
) -> Result<ExpertInstallStatus, ExpertsError> {
|
||||
let expert_id = validate_skill_id(&expert_id)
|
||||
.map_err(|e| ExpertsError::Metadata(e.to_string()))?;
|
||||
let expert_id =
|
||||
validate_skill_id(&expert_id).map_err(|e| ExpertsError::Metadata(e.to_string()))?;
|
||||
let _ = find_metadata(&expert_id)?;
|
||||
let central = expert_central_path(&expert_id);
|
||||
if !central.exists() {
|
||||
@@ -819,9 +814,8 @@ pub async fn experts_link_to_agent(
|
||||
}
|
||||
ExpertLinkState::NotLinked => {
|
||||
// Shouldn't happen after AlreadyExists, but retry once.
|
||||
create_link_raw(¢ral, &link_path).map_err(|e| ExpertsError::Io(format!(
|
||||
"retry link failed: {e}"
|
||||
)))?;
|
||||
create_link_raw(¢ral, &link_path)
|
||||
.map_err(|e| ExpertsError::Io(format!("retry link failed: {e}")))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -846,8 +840,8 @@ pub async fn experts_unlink_from_agent(
|
||||
expert_id: String,
|
||||
agent_type: AgentType,
|
||||
) -> Result<(), ExpertsError> {
|
||||
let expert_id = validate_skill_id(&expert_id)
|
||||
.map_err(|e| ExpertsError::Metadata(e.to_string()))?;
|
||||
let expert_id =
|
||||
validate_skill_id(&expert_id).map_err(|e| ExpertsError::Metadata(e.to_string()))?;
|
||||
|
||||
let _guard = mutation_lock().lock().await;
|
||||
|
||||
@@ -865,10 +859,14 @@ pub async fn experts_unlink_from_agent(
|
||||
continue;
|
||||
}
|
||||
let state = classify_link(&candidate, ¢ral);
|
||||
if matches!(state, ExpertLinkState::LinkedToCodeg | ExpertLinkState::Broken) {
|
||||
if matches!(
|
||||
state,
|
||||
ExpertLinkState::LinkedToCodeg | ExpertLinkState::Broken
|
||||
) {
|
||||
// Safe to remove a link to our central store or a broken link.
|
||||
remove_skill_entry(&candidate)
|
||||
.map_err(|e| ExpertsError::Io(format!("remove link {}: {e}", candidate.display())))?;
|
||||
remove_skill_entry(&candidate).map_err(|e| {
|
||||
ExpertsError::Io(format!("remove link {}: {e}", candidate.display()))
|
||||
})?;
|
||||
removed = true;
|
||||
} else if state == ExpertLinkState::LinkedElsewhere {
|
||||
return Err(ExpertsError::ForeignLink {
|
||||
@@ -893,8 +891,8 @@ pub async fn experts_unlink_from_agent(
|
||||
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn experts_read_content(expert_id: String) -> Result<String, ExpertsError> {
|
||||
let expert_id = validate_skill_id(&expert_id)
|
||||
.map_err(|e| ExpertsError::Metadata(e.to_string()))?;
|
||||
let expert_id =
|
||||
validate_skill_id(&expert_id).map_err(|e| ExpertsError::Metadata(e.to_string()))?;
|
||||
let _ = find_metadata(&expert_id)?;
|
||||
let path = expert_central_path(&expert_id).join("SKILL.md");
|
||||
if !path.exists() {
|
||||
|
||||
@@ -1192,11 +1192,7 @@ pub async fn git_checkout(path: String, branch_name: String) -> Result<(), AppCo
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn git_reset(
|
||||
path: String,
|
||||
commit: String,
|
||||
mode: String,
|
||||
) -> Result<(), AppCommandError> {
|
||||
pub async fn git_reset(path: String, commit: String, mode: String) -> Result<(), AppCommandError> {
|
||||
let mode = mode.trim().to_lowercase();
|
||||
let mode_flag = match mode.as_str() {
|
||||
"soft" | "mixed" | "hard" | "keep" => format!("--{mode}"),
|
||||
@@ -2128,10 +2124,7 @@ pub async fn git_delete_branch(
|
||||
.map_err(AppCommandError::io)?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(git_command_error(
|
||||
&format!("branch {flag}"),
|
||||
&output.stderr,
|
||||
));
|
||||
return Err(git_command_error(&format!("branch {flag}"), &output.stderr));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
@@ -1735,9 +1735,7 @@ fn upsert_openclaw_server(id: &str, spec: &Value) -> Result<(), AppCommandError>
|
||||
let mcp = obj
|
||||
.get_mut("mcp")
|
||||
.and_then(Value::as_object_mut)
|
||||
.ok_or_else(|| {
|
||||
mcp_configuration_invalid(format!("invalid mcp in {}", path.display()))
|
||||
})?;
|
||||
.ok_or_else(|| mcp_configuration_invalid(format!("invalid mcp in {}", path.display())))?;
|
||||
|
||||
if !mcp.get("servers").map(Value::is_object).unwrap_or(false) {
|
||||
mcp.insert("servers".to_string(), Value::Object(Map::new()));
|
||||
|
||||
@@ -18,9 +18,7 @@ fn validate_agent_types(agent_types: &[String]) -> Result<(), AppCommandError> {
|
||||
}
|
||||
for at in agent_types {
|
||||
let _: AgentType = serde_json::from_value(serde_json::Value::String(at.clone()))
|
||||
.map_err(|_| {
|
||||
AppCommandError::invalid_input(format!("Invalid agent type: {at}"))
|
||||
})?;
|
||||
.map_err(|_| AppCommandError::invalid_input(format!("Invalid agent type: {at}")))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -32,20 +30,28 @@ fn validate_fields(
|
||||
) -> Result<(), AppCommandError> {
|
||||
if let Some(n) = name {
|
||||
if n.len() > 256 {
|
||||
return Err(AppCommandError::invalid_input("Name must be 256 characters or less"));
|
||||
return Err(AppCommandError::invalid_input(
|
||||
"Name must be 256 characters or less",
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(u) = api_url {
|
||||
if u.len() > 2048 {
|
||||
return Err(AppCommandError::invalid_input("API URL must be 2048 characters or less"));
|
||||
return Err(AppCommandError::invalid_input(
|
||||
"API URL must be 2048 characters or less",
|
||||
));
|
||||
}
|
||||
if !u.starts_with("http://") && !u.starts_with("https://") {
|
||||
return Err(AppCommandError::invalid_input("API URL must start with http:// or https://"));
|
||||
return Err(AppCommandError::invalid_input(
|
||||
"API URL must start with http:// or https://",
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(k) = api_key {
|
||||
if k.len() > 4096 {
|
||||
return Err(AppCommandError::invalid_input("API Key must be 4096 characters or less"));
|
||||
return Err(AppCommandError::invalid_input(
|
||||
"API Key must be 4096 characters or less",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -72,15 +78,9 @@ pub async fn create_model_provider_core(
|
||||
let agent_types_json = serde_json::to_string(&agent_types)
|
||||
.map_err(|e| AppCommandError::invalid_input(e.to_string()))?;
|
||||
|
||||
let model = model_provider_service::create(
|
||||
&db.conn,
|
||||
name,
|
||||
api_url,
|
||||
api_key,
|
||||
agent_types_json,
|
||||
)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
let model = model_provider_service::create(&db.conn, name, api_url, api_key, agent_types_json)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(ModelProviderInfo::from(model))
|
||||
}
|
||||
|
||||
@@ -122,8 +122,12 @@ pub async fn update_model_provider_core(
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
// Cascade credential changes to all dependent agent settings and config files.
|
||||
let url_changed = api_url.as_deref().is_some_and(|u| u != old_provider.api_url);
|
||||
let key_changed = api_key.as_deref().is_some_and(|k| k != old_provider.api_key);
|
||||
let url_changed = api_url
|
||||
.as_deref()
|
||||
.is_some_and(|u| u != old_provider.api_url);
|
||||
let key_changed = api_key
|
||||
.as_deref()
|
||||
.is_some_and(|k| k != old_provider.api_key);
|
||||
if url_changed || key_changed {
|
||||
let final_url = api_url.as_deref().unwrap_or(&old_provider.api_url);
|
||||
let final_key = api_key.as_deref().unwrap_or(&old_provider.api_key);
|
||||
@@ -135,10 +139,7 @@ pub async fn update_model_provider_core(
|
||||
Ok(ModelProviderInfo::from(model))
|
||||
}
|
||||
|
||||
pub async fn delete_model_provider_core(
|
||||
db: &AppDatabase,
|
||||
id: i32,
|
||||
) -> Result<(), AppCommandError> {
|
||||
pub async fn delete_model_provider_core(db: &AppDatabase, id: i32) -> Result<(), AppCommandError> {
|
||||
// Check if any agent settings reference this provider.
|
||||
let dependents = agent_setting_service::find_by_model_provider_id(&db.conn, id)
|
||||
.await
|
||||
|
||||
@@ -28,12 +28,7 @@ pub async fn send_notification(
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
let _ = app
|
||||
.notification()
|
||||
.builder()
|
||||
.title(title)
|
||||
.body(body)
|
||||
.show();
|
||||
let _ = app.notification().builder().title(title).body(body).show();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -29,9 +29,7 @@ async fn detect_one(name: &str) -> PackageManagerInfo {
|
||||
|
||||
match result {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
PackageManagerInfo {
|
||||
name: name.to_string(),
|
||||
installed: true,
|
||||
@@ -76,7 +74,9 @@ pub async fn create_shadcn_project(
|
||||
return Err(AppCommandError::invalid_input("Template is required"));
|
||||
}
|
||||
if target_dir.is_empty() {
|
||||
return Err(AppCommandError::invalid_input("Target directory is required"));
|
||||
return Err(AppCommandError::invalid_input(
|
||||
"Target directory is required",
|
||||
));
|
||||
}
|
||||
|
||||
let full_path = PathBuf::from(&target_dir).join(&project_name);
|
||||
|
||||
@@ -139,7 +139,11 @@ pub async fn update_system_language_settings(
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
let emitter = crate::web::event_bridge::EventEmitter::Tauri(app);
|
||||
crate::web::event_bridge::emit_event(&emitter, LANGUAGE_SETTINGS_UPDATED_EVENT, settings.clone());
|
||||
crate::web::event_bridge::emit_event(
|
||||
&emitter,
|
||||
LANGUAGE_SETTINGS_UPDATED_EVENT,
|
||||
settings.clone(),
|
||||
);
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
@@ -32,16 +32,14 @@ pub(crate) fn prepare_credential_env(
|
||||
}
|
||||
};
|
||||
|
||||
let helper_script = match git_credential::create_credential_helper_script(
|
||||
app_data_dir,
|
||||
&app_binary,
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("[TERM] failed to create credential helper script: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let helper_script =
|
||||
match git_credential::create_credential_helper_script(app_data_dir, &app_binary) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("[TERM] failed to create credential helper script: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let helper_path_str = helper_script.to_string_lossy().to_string();
|
||||
|
||||
|
||||
@@ -24,9 +24,7 @@ async fn run_git_version(git_path: &str) -> Result<GitDetectResult, AppCommandEr
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AppCommandError::not_found(format!("Cannot execute git at: {git_path}"))
|
||||
})?;
|
||||
.map_err(|_| AppCommandError::not_found(format!("Cannot execute git at: {git_path}")))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(GitDetectResult {
|
||||
@@ -108,9 +106,7 @@ pub(crate) async fn detect_git_core(
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn detect_git(
|
||||
db: State<'_, AppDatabase>,
|
||||
) -> Result<GitDetectResult, AppCommandError> {
|
||||
pub async fn detect_git(db: State<'_, AppDatabase>) -> Result<GitDetectResult, AppCommandError> {
|
||||
detect_git_core(&db.conn).await
|
||||
}
|
||||
|
||||
@@ -145,9 +141,7 @@ async fn load_git_settings(
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn get_git_settings(
|
||||
db: State<'_, AppDatabase>,
|
||||
) -> Result<GitSettings, AppCommandError> {
|
||||
pub async fn get_git_settings(db: State<'_, AppDatabase>) -> Result<GitSettings, AppCommandError> {
|
||||
load_git_settings(&db.conn).await
|
||||
}
|
||||
|
||||
@@ -221,27 +215,21 @@ pub async fn update_github_accounts(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn save_account_token(
|
||||
account_id: String,
|
||||
token: String,
|
||||
) -> Result<(), AppCommandError> {
|
||||
pub async fn save_account_token(account_id: String, token: String) -> Result<(), AppCommandError> {
|
||||
crate::keyring_store::set_token(&account_id, &token)
|
||||
.map_err(|e| AppCommandError::io_error("Failed to save token to keyring").with_detail(e))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn get_account_token(
|
||||
account_id: String,
|
||||
) -> Result<Option<String>, AppCommandError> {
|
||||
pub async fn get_account_token(account_id: String) -> Result<Option<String>, AppCommandError> {
|
||||
Ok(crate::keyring_store::get_token(&account_id))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn delete_account_token(
|
||||
account_id: String,
|
||||
) -> Result<(), AppCommandError> {
|
||||
crate::keyring_store::delete_token(&account_id)
|
||||
.map_err(|e| AppCommandError::io_error("Failed to delete token from keyring").with_detail(e))
|
||||
pub async fn delete_account_token(account_id: String) -> Result<(), AppCommandError> {
|
||||
crate::keyring_store::delete_token(&account_id).map_err(|e| {
|
||||
AppCommandError::io_error("Failed to delete token from keyring").with_detail(e)
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -283,7 +271,9 @@ pub async fn validate_github_token(
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppCommandError::network("Failed to connect to GitHub API").with_detail(e.to_string()))?;
|
||||
.map_err(|e| {
|
||||
AppCommandError::network("Failed to connect to GitHub API").with_detail(e.to_string())
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status().as_u16();
|
||||
@@ -315,13 +305,9 @@ pub async fn validate_github_token(
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let user = response
|
||||
.json::<GitHubUserResponse>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppCommandError::network("Failed to parse GitHub API response")
|
||||
.with_detail(e.to_string())
|
||||
})?;
|
||||
let user = response.json::<GitHubUserResponse>().await.map_err(|e| {
|
||||
AppCommandError::network("Failed to parse GitHub API response").with_detail(e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(GitHubTokenValidation {
|
||||
success: true,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicU8, Ordering as AtomicOrdering};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::{AtomicU8, Ordering as AtomicOrdering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use sea_orm::DatabaseConnection;
|
||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
use crate::app_error::AppCommandError;
|
||||
use crate::db::AppDatabase;
|
||||
use crate::db::service::app_metadata_service;
|
||||
use crate::db::AppDatabase;
|
||||
use crate::models::FolderDetail;
|
||||
|
||||
/// Base traffic-light position (logical px) at 100 % zoom.
|
||||
@@ -123,12 +123,15 @@ fn is_system_dark_mode() -> bool {
|
||||
// Output: " AppsUseLightTheme REG_DWORD 0x0"
|
||||
// Extract the last token on the matching line to avoid
|
||||
// substring false-positives (e.g. "0x00000001" contains "0x0").
|
||||
stdout.lines().find(|l| l.contains("AppsUseLightTheme")).map(|line| {
|
||||
line.split_whitespace()
|
||||
.last()
|
||||
.map(|val| val == "0x0" || val == "0x00000000")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
stdout
|
||||
.lines()
|
||||
.find(|l| l.contains("AppsUseLightTheme"))
|
||||
.map(|line| {
|
||||
line.split_whitespace()
|
||||
.last()
|
||||
.map(|val| val == "0x0" || val == "0x00000000")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
@@ -263,6 +266,12 @@ impl SettingsWindowState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SettingsWindowState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CommitWindowState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -284,6 +293,12 @@ impl CommitWindowState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CommitWindowState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_settings_route(section: Option<&str>) -> &'static str {
|
||||
match section {
|
||||
Some("appearance") => "settings/appearance",
|
||||
@@ -508,6 +523,12 @@ impl MergeWindowState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MergeWindowState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn open_merge_window(
|
||||
@@ -738,11 +759,9 @@ pub async fn open_project_boot_window(
|
||||
.inner_size(1400.0, 900.0)
|
||||
.min_inner_size(1100.0, 700.0)
|
||||
.center();
|
||||
let window = apply_platform_window_style(builder)
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
AppCommandError::window("Failed to open project boot window", e.to_string())
|
||||
})?;
|
||||
let window = apply_platform_window_style(builder).build().map_err(|e| {
|
||||
AppCommandError::window("Failed to open project boot window", e.to_string())
|
||||
})?;
|
||||
post_window_setup(&window);
|
||||
|
||||
Ok(())
|
||||
@@ -764,12 +783,8 @@ pub async fn update_traffic_light_position(
|
||||
CURRENT_ZOOM.store(clamped, AtomicOrdering::Relaxed);
|
||||
|
||||
// Persist to DB so the next launch reads the correct value.
|
||||
let _ = app_metadata_service::upsert_value(
|
||||
&db.conn,
|
||||
ZOOM_LEVEL_DB_KEY,
|
||||
&clamped.to_string(),
|
||||
)
|
||||
.await;
|
||||
let _ =
|
||||
app_metadata_service::upsert_value(&db.conn, ZOOM_LEVEL_DB_KEY, &clamped.to_string()).await;
|
||||
|
||||
let _ = app;
|
||||
Ok(())
|
||||
@@ -786,13 +801,7 @@ pub async fn update_appearance_mode(
|
||||
) -> Result<(), AppCommandError> {
|
||||
CACHED_APPEARANCE_MODE.store(mode_from_str(&mode), AtomicOrdering::Relaxed);
|
||||
|
||||
let _ = app_metadata_service::upsert_value(
|
||||
&db.conn,
|
||||
APPEARANCE_MODE_DB_KEY,
|
||||
&mode,
|
||||
)
|
||||
.await;
|
||||
let _ = app_metadata_service::upsert_value(&db.conn, APPEARANCE_MODE_DB_KEY, &mode).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user