diff --git a/server/src/cursor/services/account.rs b/server/src/cursor/services/account.rs index 40ded80..6c26d31 100644 --- a/server/src/cursor/services/account.rs +++ b/server/src/cursor/services/account.rs @@ -254,7 +254,7 @@ async fn forward_or( match proxy::forward_buffered(&upstream, request).await { Ok(response) if response.status.is_success() => Ok(response.into_response()), Ok(response) => { - tracing::warn!(status = %response.status, "Cursor identity upstream rejected request; using local identity"); + tracing::debug!(status = %response.status, "Cursor identity upstream rejected request; using local identity"); fallback() } Err(error) => { diff --git a/server/src/error.rs b/server/src/error.rs index e4285e7..157d261 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -57,6 +57,8 @@ impl IntoResponse for Error { | Self::Encode(_) | Self::Io(_) => StatusCode::INTERNAL_SERVER_ERROR, }; + // 所有回给 UI 的错误统一落日志,否则失败原因只出现在前端提示里。 + tracing::warn!(%status, error = %self, "request failed"); let code = match status { StatusCode::BAD_REQUEST => "invalid_argument", StatusCode::NOT_FOUND => "not_found", diff --git a/server/src/plugin/data.rs b/server/src/plugin/data.rs index 08c3601..aacd6ff 100644 --- a/server/src/plugin/data.rs +++ b/server/src/plugin/data.rs @@ -39,12 +39,15 @@ impl PluginDataStore { let path = self.path(plugin_id, key)?; let lock = self.lock(plugin_id); let _guard = lock.lock().await; - match tokio::fs::read(path).await { + match tokio::fs::read(&path).await { Ok(bytes) => Ok(serde_json::from_slice(&bytes)?), Err(error) if error.kind() == std::io::ErrorKind::NotFound => { Ok(serde_json::Value::Null) } - Err(error) => Err(error.into()), + Err(error) => Err(Error::Config(format!( + "plugin data read failed at {}: {error}", + path.display() + ))), } } @@ -57,6 +60,18 @@ impl PluginDataStore { let path = self.path(plugin_id, key)?; let lock = self.lock(plugin_id); let _guard = lock.lock().await; + self.write_locked(&path, key, value) + .await + // 带上具体路径,Windows 上的拒绝访问才能定位到是哪一步。 + .map_err(|error| { + Error::Config(format!( + "plugin data write failed at {}: {error}", + path.display() + )) + }) + } + + async fn write_locked(&self, path: &Path, key: &str, value: &serde_json::Value) -> Result<()> { let directory = path.parent().expect("plugin data path has a parent"); tokio::fs::create_dir_all(directory).await?; set_directory_permissions(directory)?; @@ -70,8 +85,8 @@ impl PluginDataStore { .await?; file.sync_all().await?; drop(file); - replace_file(&temporary, &path).await?; - set_file_permissions(&path)?; + replace_file(&temporary, path).await?; + set_file_permissions(path)?; Ok(()) } @@ -80,10 +95,13 @@ impl PluginDataStore { let lock = self.lock(plugin_id); let _guard = lock.lock().await; let path = self.root.join(plugin_id); - match tokio::fs::remove_dir_all(path).await { + match tokio::fs::remove_dir_all(&path).await { Ok(()) => Ok(()), Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error.into()), + Err(error) => Err(Error::Config(format!( + "plugin data cleanup failed at {}: {error}", + path.display() + ))), } } @@ -117,16 +135,24 @@ async fn replace_file(temporary: &Path, path: &Path) -> Result<()> { match tokio::fs::rename(temporary, path).await { Ok(()) => return Ok(()), Err(error) - if attempts < 10 + if attempts < 20 && matches!( error.raw_os_error(), Some(ACCESS_DENIED | SHARING_VIOLATION) ) => { attempts += 1; - tokio::time::sleep(std::time::Duration::from_millis(50)).await; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + Err(error) => { + tracing::warn!( + path = %path.display(), + attempts, + %error, + "plugin data file replacement failed" + ); + return Err(error.into()); } - Err(error) => return Err(error.into()), } } } diff --git a/server/src/plugin/worker.rs b/server/src/plugin/worker.rs index 2361244..7a91a2e 100644 --- a/server/src/plugin/worker.rs +++ b/server/src/plugin/worker.rs @@ -250,7 +250,12 @@ impl PluginWorker { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); - let mut child = command.spawn()?; + let mut child = command.spawn().map_err(|error| { + Error::Config(format!( + "cannot start plugin worker {}: {error}", + self.inner.executable.display() + )) + })?; let stdin = Arc::new(Mutex::new(child.stdin.take().ok_or_else(|| { Error::Config("cannot open plugin worker stdin".into()) diff --git a/server/src/provider/mod.rs b/server/src/provider/mod.rs index d997f3f..4fb9848 100644 --- a/server/src/provider/mod.rs +++ b/server/src/provider/mod.rs @@ -78,6 +78,60 @@ fn provider_event_error(label: &str, value: &serde_json::Value) -> Option Result<()> { + let extra = extra + .as_object() + .ok_or_else(|| crate::Error::Config("model extra params must be an object".into()))?; + let body = body + .as_object_mut() + .ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))?; + for (name, value) in extra { + if matches!( + name.as_str(), + "model" + | "stream" + | "messages" + | "input" + | "tools" + | "system" + | "instructions" + | "prompt_cache_key" + ) { + return Err(crate::Error::Config(format!( + "model extra params cannot replace {name}" + ))); + } + body.insert(name.clone(), value.clone()); + } + Ok(()) +} + +fn apply_body_allowlist( + body: &mut serde_json::Value, + allowed: Option<&std::collections::HashSet>, +) -> Result<()> { + let Some(allowed) = allowed else { + return Ok(()); + }; + body.as_object_mut() + .ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))? + .retain(|name, _| allowed.contains(name)); + Ok(()) +} + +fn apply_openai_prompt_cache_key(body: &mut serde_json::Value, model_id: &str) -> Result<()> { + if !model_id.to_ascii_lowercase().contains("gpt") { + return Ok(()); + } + body.as_object_mut() + .ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))? + .insert( + "prompt_cache_key".into(), + serde_json::Value::String("cursor-byok".into()), + ); + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -148,57 +202,3 @@ mod tests { assert_eq!(message, expected); } } - -fn merge_extra_params(body: &mut serde_json::Value, extra: &serde_json::Value) -> Result<()> { - let extra = extra - .as_object() - .ok_or_else(|| crate::Error::Config("model extra params must be an object".into()))?; - let body = body - .as_object_mut() - .ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))?; - for (name, value) in extra { - if matches!( - name.as_str(), - "model" - | "stream" - | "messages" - | "input" - | "tools" - | "system" - | "instructions" - | "prompt_cache_key" - ) { - return Err(crate::Error::Config(format!( - "model extra params cannot replace {name}" - ))); - } - body.insert(name.clone(), value.clone()); - } - Ok(()) -} - -fn apply_body_allowlist( - body: &mut serde_json::Value, - allowed: Option<&std::collections::HashSet>, -) -> Result<()> { - let Some(allowed) = allowed else { - return Ok(()); - }; - body.as_object_mut() - .ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))? - .retain(|name, _| allowed.contains(name)); - Ok(()) -} - -fn apply_openai_prompt_cache_key(body: &mut serde_json::Value, model_id: &str) -> Result<()> { - if !model_id.to_ascii_lowercase().contains("gpt") { - return Ok(()); - } - body.as_object_mut() - .ok_or_else(|| crate::Error::Provider("provider request body must be an object".into()))? - .insert( - "prompt_cache_key".into(), - serde_json::Value::String("cursor-byok".into()), - ); - Ok(()) -}