Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions desktop-tauri/src-tauri/server/src/server/core/plugin_host/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,24 +417,35 @@ impl ProviderAdapter for RpcAdapter {
})
}

/// 上游响应是否「其实是错误」(HTTP 200 但正文是业务失败)。
///
/// ── 为什么这一位必须问插件 ──────────────────────────────────
/// 这条判据是 provider 知识:ZCode 的活动套餐通道用 HTTP 200 +
/// `application/json` 返回 `{"code":1005,"msg":"exceed quota limit"}`,
/// 智谱清言用 200 + `-1001 剩余积分不足`。网关自己只看得到状态码与头,
/// 不知道哪家的 2xx 其实是失败。默认实现(`!(200..300).contains`)在
/// 这里会把它们当成功,直接交给 SSE 翻译层 —— 结果是 `output` 为空的
/// 一次「成功」响应,账号也不会进冷却。
///
/// ── 插件问不到时为什么回落到默认 ────────────────────────────
/// RPC 失败说明插件挂了;此时按 HTTP 状态码判成功至少与内置家同一口径,
/// 不会因为一次控制面超时把所有 2xx 响应都当成错误而误罚账号。
fn is_error_response_async<'a>(
&'a self,
status: u16,
headers: &'a HeaderMap,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool, GatewayError>> + Send + 'a>> {
Box::pin(async move {
if !(200..300).contains(&status) {
return Ok(true);
}
let default = !(200..300).contains(&status);
let mut params = self.base();
params.insert("status".into(), Value::from(status));
params.insert("headers".into(), headers_json(headers));
// 控制面问不到时回落到 HTTP 状态码:一次 RPC 超时不该把所有 2xx
// 响应都判成错误,从而误罚账号。
let value = self.control(
proto::methods::gateway::IS_ERROR_RESPONSE, Value::Object(params),
).await?;
value.as_bool().ok_or_else(|| GatewayError::with_status(
502, "插件返回的响应类型判定不是布尔值",
))
).await;
Ok(value.ok().and_then(|value| value.as_bool()).unwrap_or(default))
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,83 @@ async fn classify_error_goes_through_rpc() {
host.shutdown().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn is_error_response_goes_through_rpc() {
let Some((host, providers)) = boot().await else {
eprintln!("跳过:插件二进制不存在");
return;
};
let demo = providers.iter().find(|p| p.id.0 == "demo").expect("demo 存在");
let adapter = RpcAdapter::new(
"demo".into(),
crate::server::core::providers::ProviderKind::WorkBuddy,
host.clone(),
demo.clone(),
);

// 普通 200 + SSE 是成功流,不能因为走了 RPC 就被误判成错误。
let mut sse = axum::http::HeaderMap::new();
sse.insert(
axum::http::header::CONTENT_TYPE,
"text/event-stream".parse().expect("合法头值"),
);
assert!(
!adapter.is_error_response_async(200, &sse).await.expect("RPC 判定失败"),
"200 + text/event-stream 应当判成功"
);

// 非 2xx 无论插件怎么答都应当是错误(默认口径不能被 RPC 覆盖掉)。
assert!(
adapter.is_error_response_async(429, &sse).await.expect("RPC 判定失败"),
"429 应当判错误"
);

host.shutdown().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn plugin_200_json_is_an_error_response() {
let Some((host, providers)) = boot().await else {
eprintln!("跳过:插件二进制不存在");
return;
};
// 用 chatglm:插件侧对「200 + application/json」有明确判据(与 zcode
// 同一口径)。这条测试钉的是网关有没有把这一位问出去 —— 没接 RPC 时
// 默认实现会把它当成功,正好复现 testwhy0out 的 0 输出。
let chatglm = providers
.iter()
.find(|p| p.id.0 == "chatglm")
.expect("chatglm 存在");
let adapter = RpcAdapter::new(
"chatglm".into(),
crate::server::core::providers::ProviderKind::WorkBuddy,
host.clone(),
chatglm.clone(),
);

let mut json = axum::http::HeaderMap::new();
json.insert(
axum::http::header::CONTENT_TYPE,
"application/json; charset=utf-8".parse().expect("合法头值"),
);
assert!(
adapter.is_error_response_async(200, &json).await.expect("RPC 判定失败"),
"200 + application/json 必须判成错误,否则业务错误会被当成空回答"
);

let mut sse = axum::http::HeaderMap::new();
sse.insert(
axum::http::header::CONTENT_TYPE,
"text/event-stream".parse().expect("合法头值"),
);
assert!(
!adapter.is_error_response_async(200, &sse).await.expect("RPC 判定失败"),
"200 + text/event-stream 仍然应当判成功"
);

host.shutdown().await;
}

#[tokio::test(flavor = "multi_thread")]
async fn reasoning_patch_goes_through_rpc() {
let Some((host, providers)) = boot().await else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,12 @@ pub trait ProviderAdapter: Send + Sync {
!(200..300).contains(&status)
}

/// [`Self::is_error_response`] 的异步形态。
///
/// ── 为什么这一位需要异步 ────────────────────────────────────
/// 插件家的判据在子进程里(协议方法 `is_error_response`),而 trait 的
/// 同步签名没法 await 一次 RPC;转发循环本身就在 async 上下文里,所以
/// 把这一位做成异步钩子,默认转调同步版。内置家逐字不变。
fn is_error_response_async<'a>(
&'a self,
status: u16,
Expand Down