diff --git a/gateway/platforms/qqbot/onboard.py b/gateway/platforms/qqbot/onboard.py index bcc6d24db0..39a702a7ab 100644 --- a/gateway/platforms/qqbot/onboard.py +++ b/gateway/platforms/qqbot/onboard.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging -import sys import time from enum import IntEnum from typing import Optional, Tuple @@ -98,7 +97,8 @@ def qr_register(timeout_seconds: int = 600) -> Optional[dict]: print(f" Scan the QR code above, or open this URL directly:\n {url}") else: print(f" Open this URL in QQ on your phone:\n {url}") - print(f" Tip: uv pip install --python {sys.executable} qrcode to display a scannable QR code here") + from hermes_cli.managed_uv import pip_install_hint + print(f" Tip: {pip_install_hint('qrcode')} to display a scannable QR code here") print() while time.monotonic() < deadline: try: diff --git a/hermes_cli/managed_uv.py b/hermes_cli/managed_uv.py index 77a89b0933..d86cc8778a 100644 --- a/hermes_cli/managed_uv.py +++ b/hermes_cli/managed_uv.py @@ -52,6 +52,15 @@ def resolve_uv() -> Optional[str]: return str(p) if p.is_file() and os.access(p, os.X_OK) else None +def pip_install_hint(package: str) -> str: + """Copy-pasteable command that installs *package* into the running interpreter. + + Names Hermes' own uv when it exists: the installer drops it in ``$HERMES_HOME/bin`` + without putting that on PATH, so a bare ``uv`` would fail for installer-only users. + """ + return f"{resolve_uv() or 'uv'} pip install --python {sys.executable} {package}" + + def managed_python_install_dir(project_root: Path | None = None) -> Path: """Return the checkout-scoped Python store shared by all profiles.""" root = Path(project_root) if project_root is not None else _PROJECT_ROOT diff --git a/hermes_cli/telegram_managed_bot.py b/hermes_cli/telegram_managed_bot.py index eac29fe549..f49c7c1347 100644 --- a/hermes_cli/telegram_managed_bot.py +++ b/hermes_cli/telegram_managed_bot.py @@ -76,7 +76,8 @@ def render_qr_terminal(url: str) -> str: def print_qr_code(url: str, *, include_link: bool = True) -> None: """Print a QR code to stdout, with URL fallback if qrcode is missing.""" - print(render_qr_terminal(url) or f" (Install 'qrcode' for a scannable QR code: uv pip install --python {sys.executable} qrcode)") + from hermes_cli.managed_uv import pip_install_hint + print(render_qr_terminal(url) or f" (Install 'qrcode' for a scannable QR code: {pip_install_hint('qrcode')})") if include_link: print(f" Link: {url}") diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index af44066dea..c27eadb143 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -30,7 +30,6 @@ import logging import mimetypes import os import re -import sys import threading import time import uuid @@ -4104,7 +4103,8 @@ def _qr_register_inner(*, initial_domain: str, timeout_seconds: int) -> Optional print(f"\n Scan the QR code above, or open this URL directly:\n {qr_url}") else: print(f" Open this URL in Feishu / Lark on your phone:\n\n {qr_url}\n") - print(f" Tip: uv pip install --python {sys.executable} qrcode to display a scannable QR code here next time") + from hermes_cli.managed_uv import pip_install_hint + print(f" Tip: {pip_install_hint('qrcode')} to display a scannable QR code here next time") print() result = _poll_registration( device_code=begin["device_code"], interval=begin["interval"], diff --git a/plugins/platforms/wecom/adapter.py b/plugins/platforms/wecom/adapter.py index e48bc89a2e..98a1a753f6 100644 --- a/plugins/platforms/wecom/adapter.py +++ b/plugins/platforms/wecom/adapter.py @@ -10,7 +10,6 @@ import asyncio import json import logging import re -import sys import time import uuid from datetime import datetime, timezone @@ -675,7 +674,8 @@ def qr_scan_for_bot_info(*, timeout_seconds: int = _QR_POLL_TIMEOUT) -> Optional print(f"\n Scan the QR code above, or open this URL directly:\n {page_url}") except Exception: print(f" Open this URL in WeCom on your phone:\n\n {page_url}\n") - print(f" Tip: uv pip install --python {sys.executable} qrcode to display a scannable QR code here next time") + from hermes_cli.managed_uv import pip_install_hint + print(f" Tip: {pip_install_hint('qrcode')} to display a scannable QR code here next time") print("\n Fetching configuration results...", end="", flush=True) deadline = time.monotonic() + timeout_seconds query_url = f"{_QR_QUERY_URL}?scode={urllib.parse.quote(scode)}" diff --git a/tests/hermes_cli/test_managed_uv.py b/tests/hermes_cli/test_managed_uv.py index e00bd84746..3ca8fc9f4d 100644 --- a/tests/hermes_cli/test_managed_uv.py +++ b/tests/hermes_cli/test_managed_uv.py @@ -184,6 +184,21 @@ class TestResolveUv: assert resolve_uv() is None +class TestPipInstallHint: + + def test_names_the_managed_uv_when_present(self, tmp_path): + uv = tmp_path / "bin" / _UV_BINARY_NAME + _make_executable(uv) + with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path): + from hermes_cli.managed_uv import pip_install_hint + assert pip_install_hint("qrcode") == f"{uv} pip install --python {sys.executable} qrcode" + + def test_falls_back_to_bare_uv_when_absent(self, tmp_path): + with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path): + from hermes_cli.managed_uv import pip_install_hint + assert pip_install_hint("qrcode") == f"uv pip install --python {sys.executable} qrcode" + + # --------------------------------------------------------------------------- # ensure_uv # ---------------------------------------------------------------------------