0e8dd3f412
- new agent_reach/backends/opencli.py: probes install + daemon/extension state via `opencli daemon status` (pure query — `opencli doctor` auto-starts the daemon, a side effect health checks must avoid) - `agent-reach install --channels opencli`: npm install + Chrome Web Store guide (extension install cannot be automated — Chrome security model — so we print the one-click path) - server env skips OpenCLI (rides a real desktop Chrome session) - channels will adopt it as a backend candidate in follow-up PRs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""OpenCLI backend probing.
|
||
|
||
OpenCLI (github.com/jackwener/opencli) drives the user's real Chrome via a
|
||
browser-bridge extension + local daemon, reusing existing login sessions —
|
||
zero per-platform configuration, desktop-only (no headless).
|
||
|
||
Probing notes (verified live):
|
||
- `opencli doctor` AUTO-STARTS the daemon — a side effect, so health
|
||
checks must use `opencli daemon status` (pure query) instead.
|
||
- Exit codes are always 0; status must be parsed from text output.
|
||
- Extension connectivity is volatile (drops when Chrome restarts and
|
||
reconnects on demand) — report it, but treat "installed + daemon
|
||
reachable" as the stable signal.
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
|
||
from agent_reach.probe import probe_command
|
||
|
||
OPENCLI_PACKAGE = "@jackwener/opencli"
|
||
OPENCLI_EXTENSION_URL = (
|
||
"https://chromewebstore.google.com/detail/opencli/ildkmabpimmkaediidaifkhjpohdnifk"
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class OpenCLIStatus:
|
||
installed: bool = False
|
||
broken: bool = False
|
||
daemon_running: bool = False
|
||
extension_connected: bool = False
|
||
version: str = ""
|
||
hint: str = ""
|
||
|
||
@property
|
||
def ready(self) -> bool:
|
||
"""Fully usable right now: extension connected to the daemon."""
|
||
return self.installed and not self.broken and self.extension_connected
|
||
|
||
|
||
def opencli_status(timeout: int = 10) -> OpenCLIStatus:
|
||
"""Probe OpenCLI install + daemon/extension state without side effects."""
|
||
version_probe = probe_command(
|
||
"opencli", ["--version"], timeout=timeout, package=OPENCLI_PACKAGE
|
||
)
|
||
if version_probe.status == "missing":
|
||
return OpenCLIStatus(installed=False)
|
||
if not version_probe.ok:
|
||
return OpenCLIStatus(
|
||
installed=True,
|
||
broken=True,
|
||
hint=(
|
||
"opencli 命令存在但无法执行(node 环境损坏),重装:\n"
|
||
f" npm install -g {OPENCLI_PACKAGE}"
|
||
),
|
||
)
|
||
|
||
st = OpenCLIStatus(installed=True, version=version_probe.output.strip())
|
||
|
||
daemon_probe = probe_command(
|
||
"opencli", ["daemon", "status"], timeout=timeout, package=OPENCLI_PACKAGE
|
||
)
|
||
output = daemon_probe.output if daemon_probe.ok else ""
|
||
# `opencli daemon status` prints lines like:
|
||
# Daemon: running (PID 37389) / Daemon: not running
|
||
# Extension: connected / Extension: disconnected
|
||
for line in output.splitlines():
|
||
line = line.strip().lower()
|
||
if line.startswith("daemon:"):
|
||
st.daemon_running = "not running" not in line and "running" in line
|
||
elif line.startswith("extension:"):
|
||
st.extension_connected = "disconnected" not in line and "connected" in line
|
||
|
||
if not st.extension_connected:
|
||
st.hint = (
|
||
"OpenCLI 已安装,但 Chrome 扩展未连接。\n"
|
||
f" 1. 安装扩展(需手动点一次):{OPENCLI_EXTENSION_URL}\n"
|
||
" 2. 保持 Chrome 打开,运行 `opencli doctor` 验证"
|
||
)
|
||
return st
|
||
|
||
|
||
def opencli_summary(st: OpenCLIStatus) -> str:
|
||
"""One-line state description for channel messages / install output."""
|
||
if not st.installed:
|
||
return "OpenCLI 未安装"
|
||
if st.broken:
|
||
return "OpenCLI 无法执行(node 环境损坏)"
|
||
if st.ready:
|
||
return f"OpenCLI 可用(浏览器登录态,v{st.version})"
|
||
if st.daemon_running:
|
||
return "OpenCLI 已安装,等待 Chrome 扩展连接"
|
||
return "OpenCLI 已安装(daemon 未运行,使用时自动启动;需 Chrome 扩展)"
|