ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
"""Real Playwright Computer Use surface for registration forms."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
|
||||
from models import FieldSpec
|
||||
|
||||
|
||||
class RecoverableFillError(RuntimeError):
|
||||
"""A page-specific field failure that may be reported without aborting the call."""
|
||||
|
||||
|
||||
class RegistrationBrowser:
|
||||
"""Owns a real Chromium browser/context/page and exposes form operations.
|
||||
|
||||
The selector assigned during discovery is generated from the element itself and
|
||||
remains internal. Values are never included in screenshots or trace files.
|
||||
"""
|
||||
|
||||
def __init__(self, url: str, *, headless: bool = False, submit: bool = False):
|
||||
self.url = url
|
||||
self.headless = headless
|
||||
self.submit_enabled = submit
|
||||
self._playwright = None
|
||||
self.browser = None
|
||||
self.context = None
|
||||
self.page = None
|
||||
self.closed = False
|
||||
|
||||
async def open(self) -> None:
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
self._playwright = await async_playwright().start()
|
||||
self.browser = await self._playwright.chromium.launch(headless=self.headless)
|
||||
self.context = await self.browser.new_context()
|
||||
self.page = await self.context.new_page()
|
||||
await self.page.goto(self.url, wait_until="domcontentloaded", timeout=60_000)
|
||||
|
||||
async def discover_fields(self) -> List[FieldSpec]:
|
||||
if self.page is None:
|
||||
raise RuntimeError("browser is not open")
|
||||
raw = await self.page.locator(
|
||||
"input:not([type=hidden]):not([disabled]), select:not([disabled]), textarea:not([disabled])"
|
||||
).evaluate_all(
|
||||
"""els => els.map((el, i) => {
|
||||
const id = el.id || '';
|
||||
const explicit = id ? document.querySelector(`label[for="${CSS.escape(id)}"]`) : null;
|
||||
const wrapping = el.closest('label');
|
||||
const aria = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || '';
|
||||
const label = (explicit?.innerText || wrapping?.innerText || aria || el.placeholder || el.name || id || `field_${i}`).trim();
|
||||
const selector = ((el.type === 'radio' || el.type === 'checkbox') && el.name) ?
|
||||
`input[name="${CSS.escape(el.name)}"]` : id ? `#${CSS.escape(id)}` :
|
||||
(el.name ? `${el.tagName.toLowerCase()}[name="${CSS.escape(el.name)}"]` :
|
||||
`${el.tagName.toLowerCase()}:nth-of-type(${i + 1})`);
|
||||
return {
|
||||
name: el.name || id || `field_${i}`,
|
||||
label,
|
||||
input_type: el.tagName === 'SELECT' ? 'select' : (el.type || el.tagName.toLowerCase()),
|
||||
required: !!el.required || el.getAttribute('aria-required') === 'true',
|
||||
selector,
|
||||
format_hint: el.title || el.placeholder || '',
|
||||
pattern: el.pattern || '',
|
||||
options: el.tagName === 'SELECT' ? [...el.options].map(o => o.text.trim()).filter(Boolean) :
|
||||
((el.type === 'radio' || el.type === 'checkbox') ? [el.value, label].filter(Boolean) : [])
|
||||
};
|
||||
})"""
|
||||
)
|
||||
# Radio buttons with the same name are one logical field.
|
||||
fields: List[FieldSpec] = []
|
||||
seen: set[str] = set()
|
||||
for item in raw:
|
||||
spec = FieldSpec.from_dict(item)
|
||||
if spec.name in seen:
|
||||
existing = next(f for f in fields if f.name == spec.name)
|
||||
existing.options = list(dict.fromkeys(existing.options + spec.options))
|
||||
continue
|
||||
seen.add(spec.name)
|
||||
fields.append(spec)
|
||||
return fields
|
||||
|
||||
@property
|
||||
async def title(self) -> str:
|
||||
return await self.page.title() if self.page else ""
|
||||
|
||||
async def fill(self, field: FieldSpec, value: str) -> None:
|
||||
if self.page is None:
|
||||
raise RuntimeError("browser is not open")
|
||||
from playwright.async_api import Error as PlaywrightError
|
||||
|
||||
try:
|
||||
locator = self.page.locator(field.selector).first
|
||||
await locator.scroll_into_view_if_needed()
|
||||
kind = field.input_type.lower()
|
||||
if kind == "select":
|
||||
try:
|
||||
await locator.select_option(label=value)
|
||||
except PlaywrightError:
|
||||
await locator.select_option(value=value)
|
||||
elif kind in {"checkbox", "radio"}:
|
||||
group = self.page.locator(field.selector)
|
||||
wanted = value.strip().casefold()
|
||||
chosen = None
|
||||
for i in range(await group.count()):
|
||||
item = group.nth(i)
|
||||
raw_value = (await item.get_attribute("value") or "").strip()
|
||||
item_id = await item.get_attribute("id")
|
||||
label = ""
|
||||
if item_id:
|
||||
label_node = self.page.locator(f'label[for="{item_id}"]').first
|
||||
if await label_node.count():
|
||||
label = (await label_node.inner_text()).strip()
|
||||
if wanted in {raw_value.casefold(), label.casefold()}:
|
||||
chosen = item
|
||||
break
|
||||
if chosen is None:
|
||||
raise RecoverableFillError(
|
||||
f"{field.name} has no matching page option"
|
||||
)
|
||||
await chosen.check()
|
||||
else:
|
||||
await locator.fill(value)
|
||||
except RecoverableFillError:
|
||||
raise
|
||||
except PlaywrightError as exc:
|
||||
# Do not include the value or raw Playwright text: either may contain
|
||||
# user-supplied form data that must not enter logs or traces.
|
||||
raise RecoverableFillError(
|
||||
f"browser could not fill {field.name}: {type(exc).__name__}"
|
||||
) from exc
|
||||
|
||||
async def submit(self) -> bool:
|
||||
if not self.submit_enabled:
|
||||
return False
|
||||
if self.page is None:
|
||||
raise RuntimeError("browser is not open")
|
||||
button = self.page.locator(
|
||||
'button[type="submit"], input[type="submit"], button:has-text("注册"), button:has-text("Register")'
|
||||
).first
|
||||
if await button.count() == 0:
|
||||
raise RuntimeError("页面没有可识别的提交按钮")
|
||||
await button.click()
|
||||
await asyncio.sleep(1)
|
||||
return True
|
||||
|
||||
async def close(self) -> None:
|
||||
if self.context:
|
||||
await self.context.close()
|
||||
if self.browser:
|
||||
await self.browser.close()
|
||||
if self._playwright:
|
||||
await self._playwright.stop()
|
||||
self.closed = True
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.open()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await self.close()
|
||||
Reference in New Issue
Block a user