diff --git a/gateway/platforms/qqbot/onboard.py b/gateway/platforms/qqbot/onboard.py index c804cf7fc3..bcc6d24db0 100644 --- a/gateway/platforms/qqbot/onboard.py +++ b/gateway/platforms/qqbot/onboard.py @@ -98,7 +98,7 @@ 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: {sys.executable} -m pip install qrcode to display a scannable QR code here") + print(f" Tip: uv pip install --python {sys.executable} qrcode to display a scannable QR code here") print() while time.monotonic() < deadline: try: diff --git a/hermes_cli/telegram_managed_bot.py b/hermes_cli/telegram_managed_bot.py index 8b6d08bb65..eac29fe549 100644 --- a/hermes_cli/telegram_managed_bot.py +++ b/hermes_cli/telegram_managed_bot.py @@ -76,7 +76,7 @@ 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: {sys.executable} -m pip install qrcode)") + print(render_qr_terminal(url) or f" (Install 'qrcode' for a scannable QR code: uv pip install --python {sys.executable} qrcode)") if include_link: print(f" Link: {url}") diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index 69c62c5a54..af44066dea 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -4104,7 +4104,7 @@ 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: {sys.executable} -m pip install qrcode to display a scannable QR code here next time") + print(f" Tip: uv pip install --python {sys.executable} 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 9213740fc7..e48bc89a2e 100644 --- a/plugins/platforms/wecom/adapter.py +++ b/plugins/platforms/wecom/adapter.py @@ -675,7 +675,7 @@ 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: {sys.executable} -m pip install qrcode to display a scannable QR code here next time") + print(f" Tip: uv pip install --python {sys.executable} 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/gateway/test_feishu_onboard.py b/tests/gateway/test_feishu_onboard.py index 21e1d1b6d9..c7faf7e6a6 100644 --- a/tests/gateway/test_feishu_onboard.py +++ b/tests/gateway/test_feishu_onboard.py @@ -1,6 +1,7 @@ """Tests for plugins.platforms.feishu.adapter — Feishu scan-to-create registration.""" import json +import sys from unittest.mock import patch, MagicMock import pytest @@ -221,6 +222,31 @@ class TestQrRegister: result = qr_register() assert result is None + @patch("plugins.platforms.feishu.adapter._render_qr", return_value=False) + @patch("plugins.platforms.feishu.adapter._poll_registration", return_value=None) + @patch("plugins.platforms.feishu.adapter._begin_registration") + @patch("plugins.platforms.feishu.adapter._init_registration") + def test_qr_fallback_tip_targets_active_interpreter( + self, mock_init, mock_begin, mock_poll, mock_render, capsys + ): + """#111695: the install tip must target the running venv (uv, no pip module).""" + from plugins.platforms.feishu.adapter import _qr_register_inner + + mock_begin.return_value = { + "device_code": "dc_123", + "qr_url": "https://example.com/qr", + "user_code": "ABCD", + "interval": 1, + "expire_in": 60, + } + + assert _qr_register_inner(initial_domain="feishu", timeout_seconds=60) is None + + output = capsys.readouterr().out + assert "https://example.com/qr" in output + assert f"uv pip install --python {sys.executable} qrcode" in output + assert "Tip: pip install qrcode" not in output + # -- Contract: expected errors → None, unexpected errors → propagate -- diff --git a/tests/hermes_cli/test_telegram_managed_bot.py b/tests/hermes_cli/test_telegram_managed_bot.py index 7768515e9c..2352a6dcce 100644 --- a/tests/hermes_cli/test_telegram_managed_bot.py +++ b/tests/hermes_cli/test_telegram_managed_bot.py @@ -38,14 +38,16 @@ class TestQRCode: def test_print_qr_code_tip_targets_active_interpreter(self, capsys): # Regression for #111695: a bare `pip install` targets the wrong - # environment when Hermes runs in an isolated venv. The fallback tip - # must name the interpreter that is actually running. + # environment when Hermes runs in an isolated venv (which has no pip + # module at all). The fallback tip must name the interpreter that is + # actually running, via uv. import sys with patch.dict("sys.modules", {"qrcode": None}): print_qr_code("https://t.me/newbot/Bot/test_bot") captured = capsys.readouterr() - assert sys.executable in captured.out + assert f"uv pip install --python {sys.executable} qrcode" in captured.out + assert " pip install qrcode)" not in captured.out class TestCreatePairing: