feat(mcp): add WorldQuant authentication recovery tools
Deploy production / deploy (push) Successful in 52s

This commit is contained in:
yuxuanhui
2026-09-10 10:26:44 +08:00
parent d4c33d0124
commit f67b4d99b7
6 changed files with 142 additions and 4 deletions
+46
View File
@@ -17,6 +17,7 @@ from ..catalog.platform import platform_options, validate_platform_scope
from ..catalog.research_metadata import ResearchMetadata, availability_key
from ..catalog.service import Catalog
from ..correlation import MIN_SAMPLES, THRESHOLD, WINDOW_YEARS
from ..jobs import AUTH_KINDS
from ..models import Account, Alpha, BacktestItem, Job, JobItem, ResearchRequest, SimulationAttempt, now
from ..research.serialization import encode_snapshot
from ..research.workspace_contracts import FieldAvailabilityInput
@@ -59,6 +60,51 @@ class ResearchAccess:
"platform_check": False,
}}
async def connection(self, args):
"""Read bounded connection evidence; never return credentials or session cookies."""
account = await self.db.get(Account, self.principal.account_id)
if not account or account.wq_user_id != self.principal.wq_user_id:
raise ResearchError("ACCOUNT_MISMATCH", "平台账户绑定已变化")
job = None
if args.job_id:
job = await self.db.get(Job, args.job_id)
if not job or job.kind not in AUTH_KINDS:
raise ResearchError("NOT_FOUND", "认证任务不存在")
return {"connection_status": account.connection_status,
"session_authenticated": self.client.authenticated,
"credentials_configured": bool(account.email and account.password_encrypted),
"requires_human_verification": account.connection_status == "verification_required",
"web_url": f"{self.public_origin}/",
"job": {"job_id": job.id, "kind": job.kind, "status": job.status} if job else None,
"next_step": "需要人工验证时在网页账户连接面板完成,再调用 authenticate_worldquant(action=verify)"}
async def authenticate(self, args):
"""Queue authentication using saved credentials, sharing the HTTP account lock.
The caller commits the job and audit together before waking the runner.
Human challenges remain in the browser; no password or challenge URL is exposed.
"""
account = await self.db.scalar(select(Account).where(
Account.id == self.principal.account_id).with_for_update())
if not account or account.wq_user_id != self.principal.wq_user_id:
raise ResearchError("ACCOUNT_MISMATCH", "平台账户绑定已变化")
if not account.email or not account.password_encrypted:
raise ResearchError("CREDENTIALS_NOT_CONFIGURED", "请先在网页保存 WorldQuant 账户配置")
job = await self.db.scalar(select(Job).where(
Job.kind.in_(AUTH_KINDS), Job.status.in_(("running", "queued"))))
if not job:
# Preserve a pending challenge instead of replacing its server-side session.
if args.action == "connect" and account.connection_status == "verification_required":
raise ResearchError("VERIFICATION_REQUIRED", "请在网页完成人工验证,再调用 action=verify")
job = Job(id=str(uuid4()), kind=args.action, payload={}, checkpoint={})
self.db.add(job)
if args.action == "connect":
account.connection_status, account.connection_error = "connecting", None
await self.db.flush()
self.wake = "jobs"
return {"job_id": job.id, "status": job.status, "action": job.kind,
"read_with": "get_worldquant_connection", "web_url": f"{self.public_origin}/"}
async def catalog(self, args):
data = await Catalog(self.db).search(args.filters, args.dataset_id)
return {**data, "scope": args.filters.model_dump(include={"region", "universe", "delay", "instrument_type"}),