mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-19 10:22:04 +08:00
feat: support local plugin install
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ uv.lock
|
||||
# IDE and editors
|
||||
.vscode
|
||||
.idea
|
||||
.zed/
|
||||
|
||||
# Logs and temporary files
|
||||
botpy.log
|
||||
|
||||
@@ -10,6 +10,7 @@ from ..utils import (
|
||||
check_astrbot_root,
|
||||
get_astrbot_root,
|
||||
get_git_repo,
|
||||
install_local_plugin,
|
||||
manage_plugin,
|
||||
)
|
||||
|
||||
@@ -143,12 +144,32 @@ def list(all: bool) -> None:
|
||||
|
||||
|
||||
@plug.command()
|
||||
@click.argument("name")
|
||||
@click.argument("name", required=False)
|
||||
@click.option(
|
||||
"--editable",
|
||||
"-e",
|
||||
"local_path",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
help="Install a plugin from a local directory",
|
||||
)
|
||||
@click.option("--proxy", help="Proxy server address")
|
||||
def install(name: str, proxy: str | None) -> None:
|
||||
def install(name: str | None, local_path: Path | None, proxy: str | None) -> None:
|
||||
"""Install a plugin"""
|
||||
base_path = _get_data_path()
|
||||
plug_path = base_path / "plugins"
|
||||
|
||||
if local_path is not None:
|
||||
install_local_plugin(local_path, plug_path)
|
||||
return
|
||||
|
||||
if name is None:
|
||||
raise click.ClickException("Missing plugin name or local plugin path")
|
||||
|
||||
local_name_path = Path(name).expanduser()
|
||||
if local_name_path.exists() and local_name_path.is_dir():
|
||||
install_local_plugin(local_name_path, plug_path)
|
||||
return
|
||||
|
||||
plugins = build_plug_list(base_path / "plugins")
|
||||
|
||||
plugin = next(
|
||||
|
||||
@@ -3,7 +3,13 @@ from .basic import (
|
||||
check_dashboard,
|
||||
get_astrbot_root,
|
||||
)
|
||||
from .plugin import PluginStatus, build_plug_list, get_git_repo, manage_plugin
|
||||
from .plugin import (
|
||||
PluginStatus,
|
||||
build_plug_list,
|
||||
get_git_repo,
|
||||
install_local_plugin,
|
||||
manage_plugin,
|
||||
)
|
||||
from .version_comparator import VersionComparator
|
||||
|
||||
__all__ = [
|
||||
@@ -14,5 +20,6 @@ __all__ = [
|
||||
"check_dashboard",
|
||||
"get_astrbot_root",
|
||||
"get_git_repo",
|
||||
"install_local_plugin",
|
||||
"manage_plugin",
|
||||
]
|
||||
|
||||
@@ -190,6 +190,37 @@ def build_plug_list(plugins_dir: Path) -> list:
|
||||
return result
|
||||
|
||||
|
||||
def install_local_plugin(source_path: Path, plugins_dir: Path) -> None:
|
||||
"""Install a plugin from a local directory."""
|
||||
source_path = source_path.expanduser().resolve()
|
||||
plugins_dir = plugins_dir.resolve()
|
||||
|
||||
if not source_path.exists() or not source_path.is_dir():
|
||||
raise click.ClickException(f"Local plugin path does not exist: {source_path}")
|
||||
|
||||
metadata = load_yaml_metadata(source_path)
|
||||
plugin_name = metadata.get("name")
|
||||
if not isinstance(plugin_name, str) or not plugin_name.strip():
|
||||
raise click.ClickException(
|
||||
f"Local plugin {source_path} must contain metadata.yaml with a valid name"
|
||||
)
|
||||
|
||||
target_path = plugins_dir / plugin_name
|
||||
if target_path.exists():
|
||||
raise click.ClickException(f"Plugin {plugin_name} already exists")
|
||||
|
||||
try:
|
||||
plugins_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(source_path, target_path)
|
||||
click.echo(f"Plugin {plugin_name} installed successfully from {source_path}")
|
||||
except Exception as e:
|
||||
if target_path.exists():
|
||||
shutil.rmtree(target_path, ignore_errors=True)
|
||||
raise click.ClickException(
|
||||
f"Error installing local plugin {plugin_name}: {e}"
|
||||
) from e
|
||||
|
||||
|
||||
def manage_plugin(
|
||||
plugin: dict,
|
||||
plugins_dir: Path,
|
||||
|
||||
103
tests/test_cli_plugin.py
Normal file
103
tests/test_cli_plugin.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from astrbot.cli.commands.cmd_plug import plug
|
||||
|
||||
|
||||
def _write_plugin(path: Path, name: str = "astrbot_plugin_local_demo") -> None:
|
||||
path.mkdir(parents=True)
|
||||
(path / "metadata.yaml").write_text(
|
||||
"\n".join(
|
||||
[
|
||||
f"name: {name}",
|
||||
"desc: Local plugin",
|
||||
"version: 1.0.0",
|
||||
"author: AstrBot",
|
||||
"repo: https://example.com/local-plugin",
|
||||
],
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(path / "main.py").write_text("PLUGIN_LOADED = True\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _write_astrbot_root(path: Path) -> None:
|
||||
(path / ".astrbot").touch()
|
||||
(path / "data" / "plugins").mkdir(parents=True)
|
||||
|
||||
|
||||
def test_plugin_install_editable_copies_local_plugin(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root = tmp_path / "root"
|
||||
source = tmp_path / "source-plugin"
|
||||
root.mkdir()
|
||||
_write_astrbot_root(root)
|
||||
_write_plugin(source)
|
||||
monkeypatch.chdir(root)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
plug,
|
||||
["install", "-e", str(source)],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
|
||||
target = root / "data" / "plugins" / "astrbot_plugin_local_demo"
|
||||
assert result.exit_code == 0
|
||||
assert (target / "metadata.yaml").exists()
|
||||
assert (target / "main.py").read_text(encoding="utf-8") == "PLUGIN_LOADED = True\n"
|
||||
|
||||
|
||||
def test_plugin_install_accepts_local_path_without_editable_flag(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root = tmp_path / "root"
|
||||
source = tmp_path / "source-plugin"
|
||||
root.mkdir()
|
||||
_write_astrbot_root(root)
|
||||
_write_plugin(source)
|
||||
monkeypatch.chdir(root)
|
||||
|
||||
result = CliRunner().invoke(plug, ["install", str(source)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (
|
||||
root / "data" / "plugins" / "astrbot_plugin_local_demo" / "metadata.yaml"
|
||||
).exists()
|
||||
|
||||
|
||||
def test_plugin_install_editable_rejects_existing_plugin(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root = tmp_path / "root"
|
||||
source = tmp_path / "source-plugin"
|
||||
root.mkdir()
|
||||
_write_astrbot_root(root)
|
||||
_write_plugin(source)
|
||||
_write_plugin(root / "data" / "plugins" / "astrbot_plugin_local_demo")
|
||||
monkeypatch.chdir(root)
|
||||
|
||||
result = CliRunner().invoke(plug, ["install", "-e", str(source)])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "already exists" in result.output
|
||||
|
||||
|
||||
def test_plugin_install_requires_name_or_editable_path(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
_write_astrbot_root(root)
|
||||
monkeypatch.chdir(root)
|
||||
|
||||
result = CliRunner().invoke(plug, ["install"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "Missing plugin name or local plugin path" in result.output
|
||||
Reference in New Issue
Block a user