重开仓库内容:改推五个技能(browser-harness / humanizer / humanizer-zh / product-planning / session-mechanism)
按授权清空原有内容后重新提交(原 oil-ui-pro 一并移出,可从历史恢复)。 browser-harness 剔除 .venv 等运行环境;根 .gitignore 补记 .venv/ 与 node_modules/。
This commit is contained in:
1 parent
9a152a1952
commit
777f7fe5d0
240 files changed
+52518
-4409
No files matched your search
@@ -0,0 +1,391 @@
|
||||
import os, sys, time, urllib.request
|
||||
from io import StringIO
|
||||
|
||||
# Windows default stdout/stderr encoding is cp1252
|
||||
# which can't encode the 🐴 marker helpers prepend to tab titles (or anything
|
||||
# else outside the locale charset). Force UTF-8 so `print(page_info())` and
|
||||
# tracebacks carrying page titles don't UnicodeEncodeError on Windows. #124(4).
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try: _stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception: pass
|
||||
|
||||
from .admin import (
|
||||
_version,
|
||||
NAME,
|
||||
daemon_alive,
|
||||
daemon_browser_kind,
|
||||
ensure_daemon,
|
||||
list_cloud_profiles,
|
||||
list_local_profiles,
|
||||
print_update_banner,
|
||||
restart_daemon,
|
||||
run_doctor,
|
||||
run_doctor_fix_snap,
|
||||
run_update,
|
||||
start_remote_daemon,
|
||||
stop_remote_daemon,
|
||||
sync_local_profile,
|
||||
)
|
||||
from . import auth, recorder, telemetry
|
||||
from .helpers import *
|
||||
|
||||
HELP = """Browser Harness
|
||||
|
||||
Read SKILL.md for the default workflow and examples.
|
||||
|
||||
Typical usage:
|
||||
browser-harness <<'PY'
|
||||
ensure_real_tab()
|
||||
print(page_info())
|
||||
PY
|
||||
|
||||
Helpers are pre-imported. The daemon auto-starts and connects to the running browser.
|
||||
|
||||
Commands:
|
||||
browser-harness --version print the installed version
|
||||
browser-harness --doctor diagnose install, daemon, and browser state
|
||||
browser-harness doctor same as --doctor
|
||||
browser-harness doctor --fix-snap print how to fix Snap Chromium blocking CDP (Linux)
|
||||
browser-harness auth login sign in to Browser Use Cloud for cloud browsers
|
||||
browser-harness auth login --device-code sign in from SSH/headless environments
|
||||
browser-harness auth status show Browser Use Cloud auth state
|
||||
browser-harness auth logout remove stored Browser Use Cloud auth
|
||||
browser-harness skill print the browser-harness skill text
|
||||
browser-harness recordings show recording status and recent sessions
|
||||
browser-harness recordings --latest print the newest recording directory
|
||||
browser-harness recordings enable save browser actions locally by default
|
||||
browser-harness recordings disable stop saving browser actions by default
|
||||
browser-harness video init <recording> prepare a recording for editing
|
||||
browser-harness video review <recording> compile and review the video
|
||||
browser-harness video export <recording> --reviewed export a verified MP4
|
||||
browser-harness telemetry status show anonymous telemetry opt-out state
|
||||
browser-harness --update [-y] pull the latest version (agents: pass -y)
|
||||
browser-harness --reload stop the daemon so next call picks up code changes
|
||||
"""
|
||||
|
||||
USAGE = """Usage:
|
||||
browser-harness <<'PY'
|
||||
print(page_info())
|
||||
PY
|
||||
"""
|
||||
|
||||
|
||||
# Probe /json/version (not a bare TCP connect) so a non-Chrome process bound to
|
||||
# 9222/9223 doesn't masquerade as Chrome and skip the cloud bootstrap. Mirrors
|
||||
# daemon.py's fallback probe.
|
||||
def _local_chrome_listening():
|
||||
for port in (9222, 9223):
|
||||
try:
|
||||
urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=0.3).close()
|
||||
return True
|
||||
except OSError: pass
|
||||
return False
|
||||
|
||||
|
||||
# BU_CDP_URL / BU_CDP_WS are documented to override local Chrome discovery
|
||||
# (install.md:58-59), so they must also block cloud auto-bootstrap. Without this
|
||||
# guard, start_remote_daemon() in admin.py overwrites BU_CDP_WS in the daemon
|
||||
# env with a cloud WebSocket URL, silently replacing the user's explicit endpoint
|
||||
# *and* billing them for a cloud browser they never asked for.
|
||||
def _explicit_cdp_configured():
|
||||
return bool(os.environ.get("BU_CDP_URL") or os.environ.get("BU_CDP_WS"))
|
||||
|
||||
|
||||
def _cloud_auth_configured():
|
||||
try:
|
||||
auth.get_browser_use_api_key()
|
||||
return True
|
||||
except (auth.CloudAuthRequired, auth.AuthError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def _print_skill():
|
||||
from importlib import resources
|
||||
# SKILL.md is UTF-8 (contains emoji); locale-codec read crashes on gbk Windows
|
||||
print(resources.files("browser_harness").joinpath("SKILL.md").read_text(encoding="utf-8"), end="")
|
||||
|
||||
|
||||
def _telemetry_command(args):
|
||||
if not args:
|
||||
return "script"
|
||||
first = args[0]
|
||||
if first in {"-h", "--help"}:
|
||||
return "help"
|
||||
if first == "--version":
|
||||
return "version"
|
||||
if first in {"--doctor", "doctor"}:
|
||||
return "doctor"
|
||||
if first == "--update":
|
||||
return "update"
|
||||
if first == "--reload":
|
||||
return "reload"
|
||||
if first == "--debug-clicks":
|
||||
return "debug-clicks"
|
||||
if first in {"auth", "skill", "recordings", "telemetry", "video"}:
|
||||
return first
|
||||
return "usage"
|
||||
|
||||
|
||||
def _exit_code(result) -> int:
|
||||
if result is None:
|
||||
return 0
|
||||
if isinstance(result, int):
|
||||
return result
|
||||
return 1
|
||||
|
||||
_MAX_TRACED_STEPS = 500
|
||||
_MAX_STEP_ARGS_LENGTH = 300
|
||||
_helper_trace = []
|
||||
_helper_call_count = 0
|
||||
|
||||
|
||||
def _step_args(args, kwargs):
|
||||
parts = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
|
||||
return ", ".join(parts)[:_MAX_STEP_ARGS_LENGTH]
|
||||
|
||||
|
||||
def _traced(name, fn):
|
||||
import functools
|
||||
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
global _helper_call_count
|
||||
_helper_call_count += 1
|
||||
entry = {"helper": name, "args": _step_args(args, kwargs)}
|
||||
if len(_helper_trace) < _MAX_TRACED_STEPS:
|
||||
_helper_trace.append(entry)
|
||||
step_start = time.monotonic()
|
||||
try:
|
||||
result = fn(*args, **kwargs)
|
||||
except BaseException as exc:
|
||||
entry["duration_seconds"] = round(time.monotonic() - step_start, 3)
|
||||
entry["error"] = str(exc)[:300]
|
||||
raise
|
||||
entry["duration_seconds"] = round(time.monotonic() - step_start, 3)
|
||||
recorder.observe(name, args, kwargs, entry["duration_seconds"])
|
||||
return result
|
||||
|
||||
wrapper.__bh_traced__ = True
|
||||
return wrapper
|
||||
|
||||
|
||||
def _install_helper_trace():
|
||||
from . import helpers
|
||||
|
||||
g = globals()
|
||||
for name in dir(helpers):
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
fn = g.get(name)
|
||||
if callable(fn) and not isinstance(fn, type) and not getattr(fn, "__bh_traced__", False):
|
||||
g[name] = _traced(name, fn)
|
||||
|
||||
|
||||
_MAX_OUTPUT_LENGTH = 20_000
|
||||
|
||||
|
||||
class _StreamTail:
|
||||
"""Pass-through stream wrapper that remembers the tail and total length."""
|
||||
|
||||
def __init__(self, wrapped, limit=500):
|
||||
self._wrapped = wrapped
|
||||
self._limit = limit
|
||||
self.tail = ""
|
||||
self.length = 0
|
||||
|
||||
def write(self, text):
|
||||
text = str(text)
|
||||
self.length += len(text)
|
||||
self.tail = (self.tail + text)[-self._limit :]
|
||||
return self._wrapped.write(text)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._wrapped, name)
|
||||
|
||||
|
||||
def _read_task(args):
|
||||
if args and args[0] == "--debug-clicks":
|
||||
args = args[1:]
|
||||
if args or sys.stdin.isatty():
|
||||
return None
|
||||
code = sys.stdin.read()
|
||||
sys.stdin = StringIO(code)
|
||||
return code
|
||||
|
||||
|
||||
def _traced_steps():
|
||||
return _helper_trace or None
|
||||
|
||||
|
||||
def _telemetry_browser(task):
|
||||
"""'cloud' | 'cdp' | 'local', self-reported by the daemon the task ran on.
|
||||
None when no browser was involved (non-script commands, daemon never up)."""
|
||||
if not task or not telemetry.is_enabled():
|
||||
return None
|
||||
try:
|
||||
return daemon_browser_kind()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
global _helper_call_count
|
||||
args = sys.argv[1:]
|
||||
if args and args[0] == "telemetry":
|
||||
sys.exit(telemetry.run_telemetry_cli(args[1:]))
|
||||
_helper_trace.clear()
|
||||
_helper_call_count = 0
|
||||
start_time = time.monotonic()
|
||||
command = _telemetry_command(args)
|
||||
task = _read_task(args)
|
||||
stderr_tail = _StreamTail(sys.stderr)
|
||||
stdout_tail = _StreamTail(sys.stdout, limit=_MAX_OUTPUT_LENGTH)
|
||||
sys.stderr = stderr_tail
|
||||
sys.stdout = stdout_tail
|
||||
try:
|
||||
_run(args)
|
||||
except SystemExit as exc:
|
||||
code = _exit_code(exc.code)
|
||||
telemetry.capture_cli_event(
|
||||
action="error" if code else "completed",
|
||||
command=command,
|
||||
task=task,
|
||||
browser=_telemetry_browser(task),
|
||||
output=stdout_tail.tail or None,
|
||||
output_length=stdout_tail.length or None,
|
||||
steps=_traced_steps(),
|
||||
step_count=_helper_call_count or None,
|
||||
duration_seconds=time.monotonic() - start_time,
|
||||
exit_code=code,
|
||||
error_message=str(exc.code) if isinstance(exc.code, str) else (stderr_tail.tail.strip() or None) if code else None,
|
||||
)
|
||||
raise
|
||||
except Exception as exc:
|
||||
telemetry.capture_cli_event(
|
||||
action="error",
|
||||
command=command,
|
||||
task=task,
|
||||
browser=_telemetry_browser(task),
|
||||
output=stdout_tail.tail or None,
|
||||
output_length=stdout_tail.length or None,
|
||||
steps=_traced_steps(),
|
||||
step_count=_helper_call_count or None,
|
||||
duration_seconds=time.monotonic() - start_time,
|
||||
exit_code=1,
|
||||
error_message=str(exc),
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
sys.stderr = stderr_tail._wrapped
|
||||
sys.stdout = stdout_tail._wrapped
|
||||
telemetry.capture_cli_event(
|
||||
action="completed",
|
||||
command=command,
|
||||
task=task,
|
||||
browser=_telemetry_browser(task),
|
||||
output=stdout_tail.tail or None,
|
||||
output_length=stdout_tail.length or None,
|
||||
steps=_traced_steps(),
|
||||
step_count=_helper_call_count or None,
|
||||
duration_seconds=time.monotonic() - start_time,
|
||||
exit_code=0,
|
||||
)
|
||||
|
||||
|
||||
def _run(args):
|
||||
if args and args[0] in {"-h", "--help"}:
|
||||
print(HELP)
|
||||
return
|
||||
if args and args[0] == "--version":
|
||||
print(_version() or "unknown")
|
||||
return
|
||||
if args and args[0] == "--doctor":
|
||||
sys.exit(run_doctor())
|
||||
if args and args[0] == "doctor":
|
||||
rest = args[1:]
|
||||
if rest == ["--fix-snap"]:
|
||||
sys.exit(run_doctor_fix_snap())
|
||||
if rest:
|
||||
print("usage: browser-harness doctor [--fix-snap]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
sys.exit(run_doctor())
|
||||
if args and args[0] == "auth":
|
||||
sys.exit(auth.run_auth_cli(args[1:]))
|
||||
if args and args[0] == "skill":
|
||||
if len(args) != 1:
|
||||
print("usage: browser-harness skill", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
_print_skill()
|
||||
return
|
||||
if args and args[0] == "recordings":
|
||||
rest = args[1:]
|
||||
if rest == ["--latest"]:
|
||||
latest = recorder.latest_recording()
|
||||
if latest is None:
|
||||
print("no recordings found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(latest)
|
||||
return
|
||||
if rest in (["enable"], ["disable"]):
|
||||
enabled = rest == ["enable"]
|
||||
recorder.set_auto_recording(enabled)
|
||||
print(f"auto-recording preference {'enabled' if enabled else 'disabled'}")
|
||||
return
|
||||
if rest:
|
||||
print("usage: browser-harness recordings [--latest|enable|disable]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
enabled, source = recorder.auto_recording_setting()
|
||||
print(f"auto-recording: {'on' if enabled else 'off'} ({source})")
|
||||
active = recorder.recording_dir()
|
||||
print(f"active: {active or 'none'}")
|
||||
recent = recorder.recordings()
|
||||
print(f"latest: {recent[0] if recent else 'none'}")
|
||||
return
|
||||
if args and args[0] == "video":
|
||||
from . import video
|
||||
|
||||
sys.exit(video.run_cli(args[1:]))
|
||||
if args and args[0] == "--update":
|
||||
yes = any(a in {"-y", "--yes"} for a in args[1:])
|
||||
sys.exit(run_update(yes=yes))
|
||||
if args and args[0] == "--reload":
|
||||
restart_daemon()
|
||||
print("daemon stopped — will restart fresh on next call")
|
||||
return
|
||||
if args and args[0] == "--debug-clicks":
|
||||
os.environ["BH_DEBUG_CLICKS"] = "1"
|
||||
args = args[1:]
|
||||
if not args and not sys.stdin.isatty():
|
||||
code = sys.stdin.read()
|
||||
if not code.strip():
|
||||
sys.exit(USAGE)
|
||||
else:
|
||||
sys.exit(USAGE)
|
||||
print_update_banner()
|
||||
# Auto-bootstrap a cloud browser is opt-in via BU_AUTOSPAWN — BROWSER_USE_API_KEY alone
|
||||
# is not enough, since the key is commonly set for unrelated reasons (profile sync,
|
||||
# cloud API calls, parent agents managing their own session). An explicit BU_CDP_URL
|
||||
# or BU_CDP_WS also blocks the spawn so we honour the precedence install.md promises.
|
||||
cloud_admin = code.lstrip().startswith(("start_remote_daemon(", "stop_remote_daemon("))
|
||||
if not cloud_admin:
|
||||
if (
|
||||
not daemon_alive()
|
||||
and not _local_chrome_listening()
|
||||
and not _explicit_cdp_configured()
|
||||
and _cloud_auth_configured()
|
||||
and os.environ.get("BU_AUTOSPAWN")
|
||||
):
|
||||
start_remote_daemon(NAME)
|
||||
try:
|
||||
ensure_daemon()
|
||||
except RuntimeError as e:
|
||||
# Setup/permission errors are instructions for calling agent
|
||||
print(f"browser-harness: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
_install_helper_trace()
|
||||
exec(code, globals())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user