2e8daad802
移除关闭入口;最小化为可拖动浮动条并持久化 minimized_rect;重启保留浏览历史。 Co-authored-by: Cursor <cursoragent@cursor.com>
165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
"""Embedded browser UI state — history, tabs, panel layout."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from urllib.parse import urlparse
|
|
|
|
BROWSER_UI_CONTEXT = "embedded_browser"
|
|
MAX_URL_HISTORY = 128
|
|
MAX_VISIT_LOG = 512
|
|
MAX_TABS = 12
|
|
|
|
|
|
def _utcnow_iso() -> str:
|
|
return datetime.now(timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds")
|
|
|
|
|
|
def _is_safe_url(url: str) -> bool:
|
|
try:
|
|
parsed = urlparse(url.strip())
|
|
except Exception:
|
|
return False
|
|
if parsed.scheme not in ("http", "https"):
|
|
return False
|
|
if parsed.username or parsed.password:
|
|
return False
|
|
return bool(parsed.netloc)
|
|
|
|
|
|
def _hostname(url: str) -> str:
|
|
try:
|
|
return urlparse(url).hostname or url
|
|
except Exception:
|
|
return url
|
|
|
|
|
|
def empty_browser_state() -> dict[str, Any]:
|
|
return {
|
|
"url_history": [],
|
|
"visits": [],
|
|
"tabs": [],
|
|
"active_tab_id": None,
|
|
"minimized": False,
|
|
"rect": None,
|
|
"minimized_rect": None,
|
|
}
|
|
|
|
|
|
def _parse_rect(raw: dict | None, *, default_w: float, default_h: float) -> dict[str, float] | None:
|
|
if raw is None or not isinstance(raw, dict):
|
|
return None
|
|
try:
|
|
return {
|
|
"x": float(raw.get("x", 0)),
|
|
"y": float(raw.get("y", 0)),
|
|
"w": float(raw.get("w", default_w)),
|
|
"h": float(raw.get("h", default_h)),
|
|
}
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def parse_browser_state(raw: dict | None) -> dict[str, Any]:
|
|
base = empty_browser_state()
|
|
if not raw or not isinstance(raw, dict):
|
|
return base
|
|
|
|
url_history = [
|
|
u for u in raw.get("url_history") or []
|
|
if isinstance(u, str) and _is_safe_url(u)
|
|
][:MAX_URL_HISTORY]
|
|
|
|
visits: list[dict[str, str]] = []
|
|
for item in raw.get("visits") or []:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
url = item.get("url")
|
|
if not isinstance(url, str) or not _is_safe_url(url):
|
|
continue
|
|
visits.append({
|
|
"url": url,
|
|
"title": str(item.get("title") or _hostname(url))[:200],
|
|
"at": str(item.get("at") or _utcnow_iso()),
|
|
})
|
|
visits = visits[:MAX_VISIT_LOG]
|
|
|
|
tabs: list[dict[str, Any]] = []
|
|
for item in raw.get("tabs") or []:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
tab_id = item.get("id")
|
|
url = item.get("url")
|
|
if not isinstance(tab_id, str) or not tab_id.strip():
|
|
continue
|
|
if not isinstance(url, str):
|
|
continue
|
|
if url.strip() and not _is_safe_url(url):
|
|
continue
|
|
if url.strip():
|
|
stack = [
|
|
u for u in item.get("stack") or [url]
|
|
if isinstance(u, str) and _is_safe_url(u)
|
|
] or [url]
|
|
stack_index = item.get("stack_index", len(stack) - 1)
|
|
if not isinstance(stack_index, int):
|
|
stack_index = len(stack) - 1
|
|
stack_index = max(0, min(stack_index, len(stack) - 1))
|
|
else:
|
|
stack = []
|
|
stack_index = -1
|
|
tabs.append({
|
|
"id": tab_id.strip()[:64],
|
|
"url": url.strip(),
|
|
"title": str(item.get("title") or (_hostname(url) if url.strip() else "新标签"))[:200],
|
|
"stack": stack[-64:],
|
|
"stack_index": stack_index,
|
|
})
|
|
tabs = tabs[:MAX_TABS]
|
|
|
|
active_tab_id = raw.get("active_tab_id")
|
|
if not isinstance(active_tab_id, str) or not any(t["id"] == active_tab_id for t in tabs):
|
|
active_tab_id = tabs[0]["id"] if tabs else None
|
|
|
|
rect = _parse_rect(raw.get("rect"), default_w=960, default_h=640)
|
|
minimized_rect = _parse_rect(raw.get("minimized_rect"), default_w=320, default_h=52)
|
|
|
|
minimized = bool(raw.get("minimized"))
|
|
|
|
return {
|
|
"url_history": url_history,
|
|
"visits": visits,
|
|
"tabs": tabs,
|
|
"active_tab_id": active_tab_id,
|
|
"minimized": minimized,
|
|
"rect": rect,
|
|
"minimized_rect": minimized_rect,
|
|
}
|
|
|
|
|
|
def merge_browser_state(current: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
|
|
"""Merge client state; incoming wins for tabs/active/minimized/rect."""
|
|
parsed = parse_browser_state({**current, **incoming})
|
|
if incoming.get("url_history") is not None:
|
|
parsed["url_history"] = parse_browser_state({"url_history": incoming["url_history"]})["url_history"]
|
|
if incoming.get("visits") is not None:
|
|
parsed["visits"] = parse_browser_state({"visits": incoming["visits"]})["visits"]
|
|
return parsed
|
|
|
|
|
|
def record_visit(state: dict[str, Any], url: str, title: str | None = None) -> dict[str, Any]:
|
|
if not _is_safe_url(url):
|
|
return state
|
|
next_state = parse_browser_state(state)
|
|
hist = [url] + [u for u in next_state["url_history"] if u != url]
|
|
next_state["url_history"] = hist[:MAX_URL_HISTORY]
|
|
visit = {
|
|
"url": url,
|
|
"title": (title or _hostname(url))[:200],
|
|
"at": _utcnow_iso(),
|
|
}
|
|
visits = [visit] + next_state["visits"]
|
|
next_state["visits"] = visits[:MAX_VISIT_LOG]
|
|
return next_state
|