From 72d6f0e8d053c491e99bf10f3ee3e130fe0c628f Mon Sep 17 00:00:00 2001 From: KT Date: Tue, 7 Jul 2026 14:44:41 +0800 Subject: [PATCH] fix(*): install channel deps by default, correct missing-dep hint Channel SDKs are opt-in extras that neither the installer nor onboarding pulled in, so enabling a channel failed with a missing-dependency error, and the disabled-channel warning told every user to run `uv sync` -- a dev-only command that does not apply to wheel/tool installs. - install.sh / install.ps1: install `raven[channels]` by default, with a fallback to base raven so one broken channel SDK on a given platform cannot block the whole install. - channels/manager.py: tailor the missing-dependency hint to the install mode via PEP 610 direct_url.json -- `uv sync --extra` for editable checkouts, re-run the installer for wheel/tool installs (OS-aware). Closes #88. Co-authored-by: Claude (claude-opus-4-8) --- install.ps1 | 21 ++++++- install.sh | 13 ++++- raven/channels/manager.py | 32 ++++++++++- tests/test_channels_manager.py | 101 ++++++++++++++++++++++++++++++++- 4 files changed, 160 insertions(+), 7 deletions(-) diff --git a/install.ps1 b/install.ps1 index f0cf381..acd67b7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -225,11 +225,28 @@ function Install-Raven([string]$UvPath, [string]$NodePath) { Write-Warn "Found node but not npm; skipping TUI bundle build" } } - & $UvPath tool install --force -e $scriptDir + # Install all channel adapters by default; fall back to base raven if + # the umbrella extra fails to build on this platform, so one broken + # channel SDK cannot block the whole install. + try { + & $UvPath tool install --force -e "$scriptDir[channels]" + if ($LASTEXITCODE -ne 0) { throw "channel extras install failed" } + } catch { + Write-Warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)." + & $UvPath tool install --force -e "$scriptDir" + if ($LASTEXITCODE -ne 0) { Fail "Raven install failed." } + } } else { $wheelUrl = Resolve-RavenWheel Write-Info " installing $wheelUrl" - & $UvPath tool install --force $wheelUrl + try { + & $UvPath tool install --force "raven[channels] @ $wheelUrl" + if ($LASTEXITCODE -ne 0) { throw "channel extras install failed" } + } catch { + Write-Warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)." + & $UvPath tool install --force $wheelUrl + if ($LASTEXITCODE -ne 0) { Fail "Raven install failed." } + } } & $UvPath tool update-shell | Out-Null Write-Ok "Raven installed" diff --git a/install.sh b/install.sh index 085ef43..89ed6fd 100755 --- a/install.sh +++ b/install.sh @@ -170,7 +170,13 @@ install_raven() { warn "No usable node found; skipping TUI build; raven tui may not work" fi fi - uv tool install --force -e "$script_dir" + # Install all channel adapters by default. If the umbrella extra fails to + # resolve/build on this platform, fall back to base raven so one broken + # channel SDK cannot block the whole install. + if ! uv tool install --force -e "$script_dir[channels]"; then + warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)." + uv tool install --force -e "$script_dir" + fi else # Remote mode: install the latest published release wheel, which bundles # the prebuilt ui-tui/dist/entry.js (built by CI). We deliberately do NOT @@ -185,7 +191,10 @@ install_raven() { fi [ -n "$wheel_url" ] || die "Could not resolve the latest raven release wheel from GitHub (check network, or set RAVEN_WHEEL_URL to a wheel URL)." info " installing $wheel_url" - uv tool install --force "$wheel_url" + if ! uv tool install --force "raven[channels] @ $wheel_url"; then + warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)." + uv tool install --force "$wheel_url" + fi fi # Ensure ~/.local/bin (uv tool bin dir) is on PATH for future shells. uv tool update-shell || true diff --git a/raven/channels/manager.py b/raven/channels/manager.py index 672b692..63652cb 100644 --- a/raven/channels/manager.py +++ b/raven/channels/manager.py @@ -8,6 +8,9 @@ from __future__ import annotations import asyncio +import json +import sys +from importlib.metadata import PackageNotFoundError, distribution from typing import Any from loguru import logger @@ -16,6 +19,31 @@ from raven.config.schema import Config +def _missing_dep_hint(modname: str) -> str: + """How to install a channel's missing SDK, tailored to the install mode. + + An editable (dev) checkout uses ``uv sync``; a wheel/tool install has no + source tree, so it must re-run the installer instead. PEP 610 + ``direct_url.json`` distinguishes them -- a wheel install records + ``archive_info`` (no ``dir_info`` key), so ``.get`` chaining avoids a + KeyError when it is absent. This runs while a channel is already failing, + so a missing/corrupt file must degrade to the installer hint, never raise. + """ + editable = False + try: + raw = distribution("raven").read_text("direct_url.json") + if raw: + editable = bool(json.loads(raw).get("dir_info", {}).get("editable", False)) + except (PackageNotFoundError, ValueError): + pass + + if editable: + return f"Run: uv sync --extra channel-{modname}" + if sys.platform == "win32": + return "Re-run the installer to add channels: irm https://raven.evermind.ai/install.ps1 | iex" + return "Re-run the installer to add channels: curl -fsSL https://raven.evermind.ai/install.sh | bash" + + class ChannelManager: """Manages chat channels: construct enabled adapters, start/stop, status.""" @@ -47,10 +75,10 @@ def _init_channels(self) -> None: logger.info("{} channel enabled", spec.display_name) except ImportError as e: logger.warning( - "{} channel disabled: missing dependency ({}). Run: uv sync --extra channel-{}", + "{} channel disabled: missing dependency ({}). {}", modname, e, - modname, + _missing_dep_hint(modname), ) self._validate_allow_from() diff --git a/tests/test_channels_manager.py b/tests/test_channels_manager.py index b40f249..0de2d38 100644 --- a/tests/test_channels_manager.py +++ b/tests/test_channels_manager.py @@ -3,12 +3,13 @@ status accessors. Outbound delivery moved to the spine outlets (no longer the manager's job).""" +from importlib.metadata import PackageNotFoundError from types import SimpleNamespace import pytest from raven.channels.contract import Capabilities, ChannelSpec -from raven.channels.manager import ChannelManager +from raven.channels.manager import ChannelManager, _missing_dep_hint class _FakeChannel: @@ -99,6 +100,104 @@ def test_validate_allow_from_rejects_empty(monkeypatch): ) +# ── _missing_dep_hint (install-mode / OS split) ─────────────────────── + +_EDITABLE_JSON = '{"url": "file:///src", "dir_info": {"editable": true}}' +_WHEEL_JSON = '{"url": "https://x/raven-0.1.2.whl", "archive_info": {}}' + + +def _patch_direct_url(monkeypatch, read_text_result): + class _Dist: + def read_text(self, name): + return read_text_result + + monkeypatch.setattr("raven.channels.manager.distribution", lambda pkg: _Dist()) + + +@pytest.mark.parametrize("modname", ["feishu", "weixin", "qq", "telegram", "dingtalk"]) +def test_hint_editable_names_the_channel_extra(monkeypatch, modname): + """Editable checkout -> `uv sync --extra channel-`, name interpolated.""" + _patch_direct_url(monkeypatch, _EDITABLE_JSON) + assert _missing_dep_hint(modname) == f"Run: uv sync --extra channel-{modname}" + + +@pytest.mark.parametrize( + "raw", + [ + _WHEEL_JSON, # archive_info: no 'dir_info' key -> .get chain must not KeyError + None, # direct_url.json absent -> read_text returns None + '{"url": "file:///x", "dir_info": {}}', # dir_info present, 'editable' missing + "{}", # empty object + "{not valid json", # corrupt file -> JSONDecodeError must be swallowed + ], + ids=["wheel", "absent", "dir_info_no_editable", "empty", "malformed"], +) +def test_hint_non_editable_points_to_installer(monkeypatch, raw): + """Any non-editable / malformed direct_url.json -> installer hint, never raises.""" + _patch_direct_url(monkeypatch, raw) + monkeypatch.setattr("raven.channels.manager.sys.platform", "linux") + hint = _missing_dep_hint("weixin") + assert "uv sync" not in hint + assert "install.sh" in hint + + +def test_hint_package_not_found_points_to_installer(monkeypatch): + """raven distribution not found -> installer hint, no exception.""" + + def _raise(pkg): + raise PackageNotFoundError(pkg) + + monkeypatch.setattr("raven.channels.manager.distribution", _raise) + monkeypatch.setattr("raven.channels.manager.sys.platform", "darwin") + assert "install.sh" in _missing_dep_hint("qq") + + +@pytest.mark.parametrize( + "platform, marker", + [("win32", "install.ps1"), ("darwin", "install.sh"), ("linux", "install.sh")], +) +def test_hint_installer_matches_os(monkeypatch, platform, marker): + """Wheel install picks the installer for the running OS (irm vs curl).""" + _patch_direct_url(monkeypatch, _WHEEL_JSON) + monkeypatch.setattr("raven.channels.manager.sys.platform", platform) + assert marker in _missing_dep_hint("slack") + + +@pytest.mark.parametrize( + "direct_url, platform, expected", + [ + (_EDITABLE_JSON, "linux", "uv sync --extra channel-feishu"), + (_WHEEL_JSON, "linux", "install.sh"), + (_WHEEL_JSON, "win32", "install.ps1"), + ], + ids=["editable", "wheel-unix", "wheel-win"], +) +def test_init_warning_carries_install_hint(monkeypatch, direct_url, platform, expected): + """A channel disabled by ImportError logs the mode-correct install hint.""" + from loguru import logger + + _patch_direct_url(monkeypatch, direct_url) + monkeypatch.setattr("raven.channels.manager.sys.platform", platform) + + def boom(config): + raise ImportError("No module named 'lark_oapi'") + + lines: list[str] = [] + sink_id = logger.add(lambda m: lines.append(str(m)), level="WARNING") + try: + _manager( + monkeypatch, + {"feishu": _spec(boom)}, + _config({"feishu": SimpleNamespace(enabled=True, allow_from=["*"])}), + ) + finally: + logger.remove(sink_id) + + warning = "".join(lines) + assert "feishu channel disabled" in warning + assert expected in warning + + # ── status / accessors ────────────────────────────────────────────────