项目文件夹

文件
wehub-resource-sync 4e0f4422d0
build-docs / deploy (push) Has been cancelled
Check Markdown links / markdown-link-check (push) Has been cancelled
Pytest / test (3.11) (push) Has been cancelled
Pytest / test (3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:42 +08:00

395 行
14 KiB
Python
可执行文件

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/root/python3.11/bin/python3
"""Web Browser server Flask + Playwright backend."""
from __future__ import annotations
import atexit
import functools
import signal
import sys
import time
from pathlib import Path
from typing import Any
lib_path = str(Path(__file__).resolve().parent.parent / "lib")
sys.path.insert(0, lib_path)
from browser_manager import BrowserManager
from flask import Flask, Response, jsonify, request
from web_browser_config import ServerConfig
from web_browser_utils import catch_error, normalize_url, validate_request
config = ServerConfig()
browser_manager = BrowserManager()
def cleanup_on_exit():
"""Cleanup function for atexit and signal handlers."""
browser_manager.cleanup()
def signal_handler(signum, frame):
"""Handle shutdown signals."""
print(f"\nReceived signal {signum}, shutting down...")
cleanup_on_exit()
sys.exit(0)
# register cleanup handlers
atexit.register(cleanup_on_exit)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
app = Flask(__name__)
def require_website_open(func):
"""Decorator to ensure a website is open before executing the route."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not browser_manager.is_website_open():
return create_response({"status": "error", "message": "Please open a website first."}, False)
return func(*args, **kwargs)
return wrapper
def _get_response_metadata() -> dict[str, Any]:
"""Gather comprehensive metadata for API responses."""
metadata = {}
if browser_manager.is_website_open():
metadata["Browser Type"] = browser_manager.browser_name
metadata["Mouse Position"] = f"({browser_manager.mouse_x},{browser_manager.mouse_y})"
metadata["Viewport Size"] = f"{browser_manager.window_width}×{browser_manager.window_height}"
with browser_manager._browser_lock() as page:
metadata["Current URL"] = page.url
metadata["Page Title"] = page.title()
scroll_info = page.evaluate("""() => ({
scroll_position: { x: window.pageXOffset || document.documentElement.scrollLeft, y: window.pageYOffset || document.documentElement.scrollTop },
page_dimensions: { width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight },
visible_dimensions: { width: window.innerWidth, height: window.innerHeight }
})""")
scroll_pos = scroll_info["scroll_position"]
page_dims = scroll_info["page_dimensions"]
visible_dims = scroll_info["visible_dimensions"]
metadata["Scroll Position"] = f"({scroll_pos['x']},{scroll_pos['y']})"
metadata["Page Dimensions"] = f"{page_dims['width']}×{page_dims['height']}"
metadata["Visible Dimensions"] = f"{visible_dims['width']}×{visible_dims['height']}"
return metadata
def create_response(data: dict[str, Any], return_screenshot: bool) -> Response:
"""Create a JSON response with comprehensive metadata and optional screenshot."""
response_data = {**data}
if return_screenshot:
response_data.update(browser_manager.take_screenshot())
if "metadata" not in response_data:
response_data["metadata"] = {}
response_data["metadata"].update(_get_response_metadata())
return jsonify(response_data)
@app.route("/info", methods=["GET"])
@catch_error
def info():
if not browser_manager.is_website_open():
return create_response({"status": "success", "message": "No page open"}, False)
data = {
"status": "success",
"message": "Loaded info for current page",
}
return create_response(data, False)
@app.route("/close", methods=["POST"])
@catch_error
def close_browser():
browser_manager.cleanup()
browser_manager._init_browser()
return create_response({"status": "success", "message": "Closed browser"}, False)
@app.route("/set_window_size", methods=["POST"])
@validate_request("width", "height", "return_screenshot")
@require_website_open
@catch_error
def set_window_size():
width, height = request.json["width"], request.json["height"]
return_screenshot = request.json["return_screenshot"]
if width <= 0 or height <= 0:
return create_response(
{"status": "error", "message": f"Invalid dimensions ({width},{height}). Must be positive"}, False
)
with browser_manager._browser_lock() as page:
page.set_viewport_size({"width": width, "height": height})
browser_manager.window_width = width
browser_manager.window_height = height
browser_manager.constrain_mouse_position(page)
data = {"status": "success", "message": f"Set viewport to {width}×{height}"}
return create_response(data, return_screenshot)
@app.route("/screenshot", methods=["GET"])
@require_website_open
def screenshot():
data = {"status": "success", "message": "Screenshot"}
return create_response(data, True)
@app.route("/click", methods=["POST"])
@validate_request("x", "y", "button", "return_screenshot")
@catch_error
@require_website_open
def click():
x, y = round(request.json["x"]), round(request.json["y"])
button = request.json["button"]
return_screenshot = request.json["return_screenshot"]
x_valid, y_valid = browser_manager.validate_coordinates(x, y)
if not x_valid or not y_valid:
return create_response(
{
"status": "error",
"message": f"Invalid coordinates ({x},{y}). Must be within {browser_manager.window_width}x{browser_manager.window_height}",
},
False,
)
with browser_manager._browser_lock() as page:
page.mouse.click(x, y, button=button)
browser_manager.mouse_x, browser_manager.mouse_y = x, y
data = {"status": "success", "message": f"Clicked '{button}' at ({x},{y})"}
return create_response(data, return_screenshot)
@app.route("/double_click", methods=["POST"])
@validate_request("x", "y", "return_screenshot")
@catch_error
@require_website_open
def double_click():
x, y = round(request.json["x"]), round(request.json["y"])
return_screenshot = request.json["return_screenshot"]
x_valid, y_valid = browser_manager.validate_coordinates(x, y)
if not x_valid or not y_valid:
return create_response(
{
"status": "error",
"message": f"Invalid coordinates ({x},{y}). Must be within {browser_manager.window_width}x{browser_manager.window_height}",
},
False,
)
with browser_manager._browser_lock() as page:
page.mouse.dblclick(x, y)
browser_manager.mouse_x, browser_manager.mouse_y = x, y
data = {"status": "success", "message": f"Doubleclicked at ({x},{y})"}
return create_response(data, return_screenshot)
@app.route("/move", methods=["POST"])
@validate_request("x", "y", "return_screenshot")
@catch_error
@require_website_open
def move():
x, y = request.json["x"], request.json["y"]
return_screenshot = request.json["return_screenshot"]
x_valid, y_valid = browser_manager.validate_coordinates(x, y)
if not x_valid or not y_valid:
return create_response(
{
"status": "error",
"message": f"Invalid coordinates ({x},{y}). Must be within {browser_manager.window_width}x{browser_manager.window_height}",
},
False,
)
with browser_manager._browser_lock() as page:
page.mouse.move(x, y)
browser_manager.mouse_x, browser_manager.mouse_y = x, y
data = {"status": "success", "message": f"Moved mouse to ({x},{y})"}
return create_response(data, return_screenshot)
@app.route("/drag", methods=["POST"])
@validate_request("path", "return_screenshot")
@catch_error
@require_website_open
def drag():
path: list[list[int]] = request.json["path"]
return_screenshot = request.json["return_screenshot"]
if not path or len(path) < 2:
return create_response({"status": "error", "message": "Path needs at least two points"}, False)
for ix, point in enumerate(path):
if len(point) != 2:
return create_response(
{"status": "error", "message": f"Path point {ix} must have exactly 2 coordinates"}, False
)
x, y = point
x_valid, y_valid = browser_manager.validate_coordinates(x, y)
if not x_valid or not y_valid:
return create_response(
{
"status": "error",
"message": f"Invalid coordinates ({x},{y}) at path point {ix}. Must be within {browser_manager.window_width}x{browser_manager.window_height}",
},
False,
)
with browser_manager._browser_lock() as page:
page.mouse.move(*path[0])
page.mouse.down()
for x, y in path[1:]:
page.mouse.move(x, y)
page.mouse.up()
browser_manager.mouse_x, browser_manager.mouse_y = path[-1]
data = {"status": "success", "message": "Dragged the mouse along the path"}
return create_response(data, return_screenshot)
@app.route("/type", methods=["POST"])
@validate_request("text", "return_screenshot")
@catch_error
@require_website_open
def type_():
text = request.json["text"]
return_screenshot = request.json["return_screenshot"]
with browser_manager._browser_lock() as page:
page.keyboard.type(text)
data = {"status": "success", "message": f"Typed '{text}'"}
return create_response(data, return_screenshot)
@app.route("/scroll", methods=["POST"])
@validate_request("scroll_x", "scroll_y", "return_screenshot")
@catch_error
@require_website_open
def scroll():
delta_x, delta_y = request.json["scroll_x"], request.json["scroll_y"]
return_screenshot = request.json["return_screenshot"]
with browser_manager._browser_lock() as page:
page.mouse.wheel(delta_x, delta_y)
data = {"status": "success", "message": f"Scrolled by ({delta_x},{delta_y})"}
return create_response(data, return_screenshot)
@app.route("/execute_script", methods=["POST"])
@validate_request("script", "return_screenshot")
@catch_error
@require_website_open
def exec_script():
script = request.json["script"]
return_screenshot = request.json["return_screenshot"]
with browser_manager._browser_lock() as page:
result = page.evaluate(script)
data = {"status": "success", "message": (f"Script executed.\n<script_result>\n{result}\n</script_result>")}
return create_response(data, return_screenshot)
@app.route("/back", methods=["POST"])
@validate_request("return_screenshot")
@catch_error
@require_website_open
def back():
return_screenshot = request.json["return_screenshot"]
with browser_manager._browser_lock() as page:
page.go_back()
data = {"status": "success", "message": "Navigated back"}
return create_response(data, return_screenshot)
@app.route("/forward", methods=["POST"])
@validate_request("return_screenshot")
@catch_error
@require_website_open
def forward():
return_screenshot = request.json["return_screenshot"]
with browser_manager._browser_lock() as page:
page.go_forward()
data = {"status": "success", "message": "Navigated forward"}
return create_response(data, return_screenshot)
@app.route("/reload", methods=["POST"])
@validate_request("return_screenshot")
@catch_error
@require_website_open
def reload():
return_screenshot = request.json["return_screenshot"]
with browser_manager._browser_lock() as page:
page.reload()
data = {"status": "success", "message": "Reloaded the page"}
return create_response(data, return_screenshot)
@app.route("/wait", methods=["POST"])
@validate_request("ms", "return_screenshot")
@catch_error
@require_website_open
def wait():
milliseconds = request.json["ms"]
return_screenshot = request.json["return_screenshot"]
time.sleep(milliseconds / 1000.0)
data = {"status": "success", "message": f"Waited {milliseconds} ms"}
return create_response(data, return_screenshot)
@app.route("/keypress", methods=["POST"])
@validate_request("keys", "return_screenshot")
@catch_error
@require_website_open
def keypress():
keys: list[str] = request.json["keys"]
return_screenshot = request.json["return_screenshot"]
if not isinstance(keys, list):
return create_response({"status": "error", "message": "Keys must be a list"}, False)
if not keys:
return create_response({"status": "error", "message": "Keys list empty"}, False)
with browser_manager._browser_lock() as _:
for key in keys[:-1]:
browser_manager.key_down(key)
browser_manager.key_press(keys[-1])
for key in reversed(keys[:-1]):
browser_manager.key_up(key)
data = {"status": "success", "message": f"Pressed keys {keys}"}
return create_response(data, return_screenshot)
@app.route("/goto", methods=["POST"])
@validate_request("url", "return_screenshot")
@catch_error
def goto():
url = normalize_url(request.json["url"])
return_screenshot = request.json["return_screenshot"]
with browser_manager._browser_lock() as page:
page.goto(url, wait_until="load")
data = {"status": "success", "message": f"Navigated to {url}"}
return create_response(data, return_screenshot)
@app.route("/console", methods=["GET"])
@catch_error
@require_website_open
def get_console():
console_messages = browser_manager.get_console_output()
data = {
"status": "success",
"message": f"Retrieved {len(console_messages)} console messages",
"console_messages": console_messages,
}
return create_response(data, False)
def main():
"""Run the Flask server with proper cleanup handling."""
try:
app.run(host="0.0.0.0", port=config.port, threaded=False, use_reloader=False)
except KeyboardInterrupt:
print("\nShutting down gracefully...")
finally:
cleanup_on_exit()
if __name__ == "__main__":
main()