mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-21 14:51:49 +02:00
fix: explain stale plan token
This commit is contained in:
1 parent
7ed16725cf
commit
06486ff380
2 files changed
+106
-24
No files matched your search
+46
-24
@@ -5490,6 +5490,29 @@ fn resolve_error_response(error: crate::cdp_target::ResolveError) -> (StatusCode
|
||||
/// The body is the same `{"code": ..., ...}` envelope the MCP transport puts
|
||||
/// in its error `data`, plus `message`, so a locator that matched three
|
||||
/// buttons hands a REST client the same candidate list an agent gets.
|
||||
/// An agent call's answer. A browser that refuses automation as unpaid on an
|
||||
/// account whose plan includes it gets the real reason: another desktop is the
|
||||
/// primary one, or the browser's own plan token is stale.
|
||||
async fn agent_reply<T>(
|
||||
id: &str,
|
||||
result: Result<T, crate::mcp_server::AgentError>,
|
||||
) -> Result<Json<T>, (StatusCode, String)> {
|
||||
use crate::mcp_server::AgentError;
|
||||
match result {
|
||||
Ok(value) => Ok(Json(value)),
|
||||
Err(AgentError::PaymentRequired(message)) => {
|
||||
let reason = match crate::cloud_auth::CLOUD_AUTH.wayfern_device_refusal() {
|
||||
Some(device) => Some(device),
|
||||
None => crate::mcp_server::stale_plan_token_refusal(Some(id)).await,
|
||||
};
|
||||
Err(agent_error_response(AgentError::PaymentRequired(
|
||||
reason.unwrap_or(message),
|
||||
)))
|
||||
}
|
||||
Err(error) => Err(agent_error_response(error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_error_response(error: crate::mcp_server::AgentError) -> (StatusCode, String) {
|
||||
use crate::cdp_target::CdpError;
|
||||
use crate::mcp_server::AgentError;
|
||||
@@ -5548,10 +5571,7 @@ async fn agent_perceive_api(
|
||||
Json(request): Json<crate::wayfern_cdp::PerceptionRequest>,
|
||||
) -> Result<Json<crate::wayfern_cdp::PerceptionPage>, (StatusCode, String)> {
|
||||
let ctx = agent_context_for(&id).await?;
|
||||
crate::mcp_server::agent_perceive(&ctx, &request)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(agent_error_response)
|
||||
agent_reply(&id, crate::mcp_server::agent_perceive(&ctx, &request).await).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -5582,10 +5602,11 @@ async fn agent_resolve_locator_api(
|
||||
Json(request): Json<crate::mcp_server::AgentResolveRequest>,
|
||||
) -> Result<Json<crate::wayfern_cdp::LocatorResolution>, (StatusCode, String)> {
|
||||
let ctx = agent_context_for(&id).await?;
|
||||
crate::mcp_server::agent_resolve_locator(&ctx, &request)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(agent_error_response)
|
||||
agent_reply(
|
||||
&id,
|
||||
crate::mcp_server::agent_resolve_locator(&ctx, &request).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -5616,10 +5637,11 @@ async fn agent_click_api(
|
||||
Json(request): Json<crate::mcp_server::AgentClickRequest>,
|
||||
) -> Result<Json<crate::mcp_server::AgentClick>, (StatusCode, String)> {
|
||||
let ctx = agent_context_for(&id).await?;
|
||||
crate::mcp_server::agent_click_locator(&ctx, &request)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(agent_error_response)
|
||||
agent_reply(
|
||||
&id,
|
||||
crate::mcp_server::agent_click_locator(&ctx, &request).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -5650,10 +5672,12 @@ async fn agent_type_api(
|
||||
Json(request): Json<crate::mcp_server::AgentTypeRequest>,
|
||||
) -> Result<Json<crate::mcp_server::AgentTyping>, (StatusCode, String)> {
|
||||
let ctx = agent_context_for(&id).await?;
|
||||
crate::mcp_server::agent_type_locator(&ctx, &request, crate::mcp_server::MAX_TYPING_SECONDS)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(agent_error_response)
|
||||
agent_reply(
|
||||
&id,
|
||||
crate::mcp_server::agent_type_locator(&ctx, &request, crate::mcp_server::MAX_TYPING_SECONDS)
|
||||
.await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -5684,10 +5708,7 @@ async fn agent_extract_api(
|
||||
Json(request): Json<crate::wayfern_cdp::ExtractionRequest>,
|
||||
) -> Result<Json<crate::wayfern_cdp::Extraction>, (StatusCode, String)> {
|
||||
let ctx = agent_context_for(&id).await?;
|
||||
crate::mcp_server::agent_extract(&ctx, &request)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(agent_error_response)
|
||||
agent_reply(&id, crate::mcp_server::agent_extract(&ctx, &request).await).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -5723,10 +5744,11 @@ async fn agent_pick_api(
|
||||
.timeout_ms
|
||||
.unwrap_or(crate::mcp_server::DEFAULT_PICK_TIMEOUT_MS)
|
||||
.clamp(1_000, crate::mcp_server::MAX_PICK_TIMEOUT_MS);
|
||||
crate::mcp_server::agent_pick_element(&ctx, timeout_ms)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(agent_error_response)
|
||||
agent_reply(
|
||||
&id,
|
||||
crate::mcp_server::agent_pick_element(&ctx, timeout_ms).await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -234,6 +234,47 @@ fn explain_engine_refusal(mut error: McpError, device_refusal: Option<String>) -
|
||||
error
|
||||
}
|
||||
|
||||
/// Why a running browser refuses automation on an account whose plan includes
|
||||
/// it, when no device refusal explains it.
|
||||
///
|
||||
/// A browser keeps the plan token it was launched with for its whole life.
|
||||
/// That token expires, and a launch while the token service did not answer
|
||||
/// gets none, so only a new launch gives the browser a token that works.
|
||||
pub(crate) async fn stale_plan_token_refusal(profile_id: Option<&str>) -> Option<String> {
|
||||
if CLOUD_AUTH.wayfern_device_refusal().is_some() || !CLOUD_AUTH.can_use_browser_automation().await
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let running_for = profile_id
|
||||
.and_then(|id| {
|
||||
crate::profile::ProfileManager::instance()
|
||||
.find_profile(id)
|
||||
.ok()
|
||||
})
|
||||
.and_then(|profile| profile.process_id)
|
||||
.and_then(crate::proxy_storage::process_start_time)
|
||||
.and_then(|start| {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.ok()?
|
||||
.as_secs();
|
||||
Some(now.saturating_sub(start))
|
||||
});
|
||||
Some(stale_plan_token_message(running_for))
|
||||
}
|
||||
|
||||
fn stale_plan_token_message(running_for: Option<u64>) -> String {
|
||||
let age = running_for
|
||||
.map(|secs| format!(" This browser has run for {} h.", secs / 3600))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"This browser cannot confirm the plan, although the account includes browser automation: \
|
||||
a browser keeps the plan token it was launched with, and that token has expired, or the \
|
||||
token service did not answer when the browser started.{age} Restart the profile to \
|
||||
automate it: call kill_profile, then run_profile."
|
||||
)
|
||||
}
|
||||
|
||||
/// The create_profile result. A setting that failed after the profile was
|
||||
/// created is named as not applied, so the agent never takes it as set.
|
||||
fn created_profile_result(
|
||||
@@ -6623,6 +6664,14 @@ impl McpServer {
|
||||
.dispatch_tool_call(caller, tool_name, &arguments)
|
||||
.await
|
||||
.map_err(|error| explain_engine_refusal(error, CLOUD_AUTH.wayfern_device_refusal()));
|
||||
if let Err(error) = &mut result {
|
||||
if error.message.contains("requires a paid Donut Browser plan") {
|
||||
let profile = arguments.get("profile_id").and_then(|v| v.as_str());
|
||||
if let Some(reason) = stale_plan_token_refusal(profile).await {
|
||||
error.message = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
let elapsed_ms = started.elapsed().as_millis();
|
||||
if human_tool {
|
||||
crate::agent_console::touch_session(caller.session);
|
||||
@@ -12862,6 +12911,17 @@ mod tests {
|
||||
"Profile not found"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn a_stale_plan_token_says_to_restart_and_how_long_the_browser_ran() {
|
||||
let message = stale_plan_token_message(Some(204_189));
|
||||
assert!(message.contains("56 h"), "{message}");
|
||||
assert!(
|
||||
message.contains("kill_profile, then run_profile"),
|
||||
"{message}"
|
||||
);
|
||||
assert!(!message.contains("requires a paid"), "{message}");
|
||||
assert!(!stale_plan_token_message(None).contains(" h."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bot_check_is_named_but_a_login_form_is_not() {
|
||||
|
||||
Reference in new issue
Block a user