support magic link login (#3702)

This commit is contained in:
LawyZheng
2025-10-14 16:24:14 +08:00
committed by GitHub
parent eda2dcffa4
commit dc832ea6db
19 changed files with 443 additions and 30 deletions

View File

@@ -27,7 +27,6 @@ from skyvern.constants import (
SPECIAL_FIELD_VERIFICATION_CODE,
ScrapeType,
)
from skyvern.core.totp import poll_verification_code
from skyvern.errors.errors import (
GetTOTPVerificationCodeError,
ReachMaxRetriesError,
@@ -82,6 +81,7 @@ from skyvern.forge.sdk.models import Step, StepStatus
from skyvern.forge.sdk.schemas.files import FileInfo
from skyvern.forge.sdk.schemas.organizations import Organization
from skyvern.forge.sdk.schemas.tasks import Task, TaskRequest, TaskResponse, TaskStatus
from skyvern.forge.sdk.schemas.totp_codes import OTPType
from skyvern.forge.sdk.trace import TraceManager
from skyvern.forge.sdk.trace.experiment_utils import collect_experiment_metadata_safely
from skyvern.forge.sdk.workflow.context_manager import WorkflowRunContext
@@ -91,6 +91,7 @@ from skyvern.schemas.runs import CUA_ENGINES, RunEngine
from skyvern.schemas.steps import AgentStepOutput
from skyvern.services import run_service, service_utils
from skyvern.services.action_service import get_action_history
from skyvern.services.otp_service import poll_otp_value
from skyvern.utils.image_resizer import Resolution
from skyvern.utils.prompt_engine import MaxStepsReasonResponse, load_prompt_with_elements
from skyvern.webeye.actions.action_types import ActionType
@@ -101,6 +102,7 @@ from skyvern.webeye.actions.actions import (
CompleteVerifyResult,
DecisiveAction,
ExtractAction,
GotoUrlAction,
ReloadPageAction,
TerminateAction,
WebAction,
@@ -1030,15 +1032,17 @@ class ForgeAgent:
screenshots=scraped_page.screenshots,
)
try:
json_response = await self.handle_potential_verification_code(
task,
step,
scraped_page,
browser_state,
json_response,
otp_json_response, otp_actions = await self.handle_potential_OTP_actions(
task, step, scraped_page, browser_state, json_response
)
detailed_agent_step_output.llm_response = json_response
actions = parse_actions(task, step.step_id, step.order, scraped_page, json_response["actions"])
if otp_actions:
detailed_agent_step_output.llm_response = otp_json_response
actions = otp_actions
else:
actions = parse_actions(
task, step.step_id, step.order, scraped_page, json_response["actions"]
)
if context:
context.pop_totp_code(task.task_id)
except NoTOTPVerificationCodeFound:
@@ -3215,6 +3219,83 @@ class ForgeAgent:
)
return None, None, next_step
async def handle_potential_OTP_actions(
self,
task: Task,
step: Step,
scraped_page: ScrapedPage,
browser_state: BrowserState,
json_response: dict[str, Any],
) -> tuple[dict[str, Any], list[Action]]:
if not task.organization_id:
return json_response, []
if not task.totp_verification_url and not task.totp_identifier:
return json_response, []
should_verify_by_magic_link = json_response.get("should_verify_by_magic_link")
place_to_enter_verification_code = json_response.get("place_to_enter_verification_code")
should_enter_verification_code = json_response.get("should_enter_verification_code")
if (
not should_verify_by_magic_link
and not place_to_enter_verification_code
and not should_enter_verification_code
):
return json_response, []
if place_to_enter_verification_code and should_enter_verification_code:
json_response = await self.handle_potential_verification_code(
task, step, scraped_page, browser_state, json_response
)
actions = parse_actions(task, step.step_id, step.order, scraped_page, json_response["actions"])
return json_response, actions
if should_verify_by_magic_link:
actions = await self.handle_potential_magic_link(task, step, scraped_page, browser_state, json_response)
return json_response, actions
return json_response, []
async def handle_potential_magic_link(
self,
task: Task,
step: Step,
scraped_page: ScrapedPage,
browser_state: BrowserState,
json_response: dict[str, Any],
) -> list[Action]:
should_verify_by_magic_link = json_response.get("should_verify_by_magic_link")
if not should_verify_by_magic_link:
return []
LOG.info("Handling magic link verification", task_id=task.task_id)
otp_value = await poll_otp_value(
organization_id=task.organization_id,
task_id=task.task_id,
workflow_run_id=task.workflow_run_id,
totp_verification_url=task.totp_verification_url,
totp_identifier=task.totp_identifier,
)
if not otp_value or otp_value.get_otp_type() != OTPType.MAGIC_LINK:
return []
# TODO: not sure whether all magic links can directly login + navigate to the homepage
return [
GotoUrlAction(
action_type=ActionType.GOTO_URL,
reasoning="Navigating to the magic link URL to verify the login",
intention="Navigating to the magic link URL to verify the login",
url=otp_value.value,
organization_id=task.organization_id,
workflow_run_id=task.workflow_run_id,
task_id=task.task_id,
step_id=step.step_id,
step_order=step.order,
action_order=0,
),
]
async def handle_potential_verification_code(
self,
task: Task,
@@ -3238,7 +3319,7 @@ class ForgeAgent:
if workflow_run:
workflow_id = workflow_run.workflow_id
workflow_permanent_id = workflow_run.workflow_permanent_id
verification_code = await poll_verification_code(
otp_value = await poll_otp_value(
organization_id=task.organization_id,
task_id=task.task_id,
workflow_id=workflow_id,
@@ -3247,8 +3328,11 @@ class ForgeAgent:
totp_verification_url=task.totp_verification_url,
totp_identifier=task.totp_identifier,
)
if not otp_value or otp_value.get_otp_type() != OTPType.TOTP:
return json_response
current_context = skyvern_context.ensure_context()
current_context.totp_codes[task.task_id] = verification_code
current_context.totp_codes[task.task_id] = otp_value.value
extract_action_prompt, use_caching = await self._build_extract_action_prompt(
task,