Files
AstrBot/astrbot/cli/commands/cmd_plug.py
LIghtJUNction 5535f0c932 Merge branch 'master' into dev
# Conflicts:
#	AGENTS.md
#	astrbot/__init__.py
#	astrbot/cli/utils/__init__.py
#	astrbot/core/astr_agent_tool_exec.py
#	astrbot/core/computer/booters/local.py
#	astrbot/core/cron/manager.py
#	astrbot/core/knowledge_base/kb_mgr.py
#	astrbot/core/knowledge_base/retrieval/manager.py
#	astrbot/core/platform/sources/discord/discord_platform_adapter.py
#	astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py
#	astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py
#	astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py
#	astrbot/core/platform/sources/wecom/wecom_adapter.py
#	astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py
#	astrbot/core/provider/func_tool_manager.py
#	astrbot/core/provider/sources/mimo_api_common.py
#	astrbot/core/star/star_manager.py
#	astrbot/core/tools/computer_tools/fs.py
#	astrbot/core/tools/computer_tools/python.py
#	astrbot/core/tools/computer_tools/shell.py
#	astrbot/core/tools/message_tools.py
#	astrbot/core/tools/web_search_tools.py
#	astrbot/core/utils/io.py
#	astrbot/core/utils/tencent_record_helper.py
#	dashboard/package.json
#	dashboard/src/assets/mdi-subset/materialdesignicons-subset.css
#	dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff
#	dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2
#	dashboard/src/components/ConfirmDialog.vue
#	dashboard/src/components/chat/Chat.vue
#	dashboard/src/components/chat/ChatInput.vue
#	dashboard/src/components/chat/ChatMessageList.vue
#	dashboard/src/components/chat/ConfigSelector.vue
#	dashboard/src/components/chat/MessageList.vue
#	dashboard/src/components/chat/ProjectDialog.vue
#	dashboard/src/components/chat/ProjectList.vue
#	dashboard/src/components/chat/ProjectView.vue
#	dashboard/src/components/chat/ProviderModelMenu.vue
#	dashboard/src/components/chat/RegenerateMenu.vue
#	dashboard/src/components/chat/StandaloneChat.vue
#	dashboard/src/components/chat/ThreadPanel.vue
#	dashboard/src/components/chat/ThreadedMarkdownMessagePart.vue
#	dashboard/src/components/chat/message_list_comps/MarkdownMessagePart.vue
#	dashboard/src/components/chat/message_list_comps/ReasoningTimeline.vue
#	dashboard/src/components/chat/message_list_comps/RefsSidebar.vue
#	dashboard/src/components/config/UnsavedChangesConfirmDialog.vue
#	dashboard/src/components/extension/MarketPluginCard.vue
#	dashboard/src/components/extension/McpServersSection.vue
#	dashboard/src/components/extension/SkillsSection.vue
#	dashboard/src/components/extension/componentPanel/components/DetailsDialog.vue
#	dashboard/src/components/extension/componentPanel/components/RenameDialog.vue
#	dashboard/src/components/folder/BaseCreateFolderDialog.vue
#	dashboard/src/components/folder/BaseFolderItemSelector.vue
#	dashboard/src/components/folder/BaseMoveToFolderDialog.vue
#	dashboard/src/components/platform/AddNewPlatform.vue
#	dashboard/src/components/provider/AddNewProvider.vue
#	dashboard/src/components/provider/ProviderModelsPanel.vue
#	dashboard/src/components/shared/BackupDialog.vue
#	dashboard/src/components/shared/ChangelogDialog.vue
#	dashboard/src/components/shared/ExtensionCard.vue
#	dashboard/src/components/shared/FileConfigItem.vue
#	dashboard/src/components/shared/KnowledgeBaseSelector.vue
#	dashboard/src/components/shared/ListConfigItem.vue
#	dashboard/src/components/shared/ObjectEditor.vue
#	dashboard/src/components/shared/PersonaForm.vue
#	dashboard/src/components/shared/PluginSetSelector.vue
#	dashboard/src/components/shared/ProviderSelector.vue
#	dashboard/src/components/shared/ReadmeDialog.vue
#	dashboard/src/components/shared/SidebarCustomizer.vue
#	dashboard/src/components/shared/T2ITemplateEditor.vue
#	dashboard/src/components/shared/UninstallConfirmDialog.vue
#	dashboard/src/components/shared/WaitingForRestart.vue
#	dashboard/src/composables/useMessages.ts
#	dashboard/src/composables/useProjects.ts
#	dashboard/src/composables/useProviderModelConfigDialog.ts
#	dashboard/src/composables/useProviderSources.ts
#	dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
#	dashboard/src/i18n/locales/ru-RU/features/extension.json
#	dashboard/src/i18n/locales/ru-RU/features/provider.json
#	dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
#	dashboard/src/scss/_override.scss
#	dashboard/src/stores/common.js
#	dashboard/src/stores/customizer.ts
#	dashboard/src/views/ConfigPage.vue
#	dashboard/src/views/ConversationPage.vue
#	dashboard/src/views/PlatformPage.vue
#	dashboard/src/views/SessionManagementPage.vue
#	dashboard/src/views/WelcomePage.vue
#	dashboard/src/views/alkaid/KnowledgeBase.vue
#	dashboard/src/views/alkaid/LongTermMemory.vue
#	dashboard/src/views/authentication/auth/LoginPage.vue
#	dashboard/src/views/extension/InstalledPluginsTab.vue
#	dashboard/src/views/extension/useExtensionPage.js
#	dashboard/src/views/knowledge-base/DocumentDetail.vue
#	dashboard/src/views/knowledge-base/KBList.vue
#	dashboard/src/views/knowledge-base/components/DocumentsTab.vue
#	dashboard/src/views/knowledge-base/components/SettingsTab.vue
#	dashboard/src/views/knowledge-base/components/TavilyKeyDialog.vue
#	dashboard/src/views/persona/FolderTree.vue
#	dashboard/src/views/persona/MoveToFolderDialog.vue
#	dashboard/src/views/persona/PersonaManager.vue
#	requirements.txt
#	tests/test_local_shell_component.py
#	tests/unit/test_func_tool_manager.py
#	tests/unit/test_web_search_tools.py
2026-07-16 17:39:40 +08:00

277 lines
8.3 KiB
Python

import re
import shutil
from pathlib import Path
import click
from astrbot.cli.i18n import t
from astrbot.cli.utils import (
PluginStatus,
build_plug_list,
get_git_repo,
install_local_plugin,
manage_plugin,
)
@click.group(name="plugin")
def plug() -> None:
"""Plugin management"""
def display_plugins(plugins, title=None, color=None) -> None:
if title:
click.echo(click.style(title, fg=color, bold=True))
click.echo(
f"{'Name':<20} {'Version':<10} {'Status':<10} {'Author':<15} {'Description':<30}",
)
click.echo("-" * 85)
for p in plugins:
desc = p["desc"][:30] + ("..." if len(p["desc"]) > 30 else "")
click.echo(
f"{p['name']:<20} {p['version']:<10} {p['status']:<10} "
f"{p['author']:<15} {desc:<30}",
)
@plug.command()
@click.argument("name")
def new(name: str) -> None:
"""Create a new plugin"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
plug_path = base_path / "plugins" / name
if plug_path.exists():
raise click.ClickException(t("plugin_already_exists", name=name))
author = click.prompt("Enter plugin author", type=str)
desc = click.prompt("Enter plugin description", type=str)
version = click.prompt("Enter plugin version", type=str)
if not re.match(r"^\d+\.\d+(\.\d+)?$", version.lower().lstrip("v")):
raise click.ClickException("Version must be in x.y or x.y.z format")
repo = click.prompt("Enter plugin repository URL:", type=str)
if not repo.startswith("http"):
raise click.ClickException("Repository URL must start with http")
click.echo("Downloading plugin template...")
get_git_repo(
"https://github.com/Soulter/helloworld",
plug_path,
)
click.echo("Rewriting plugin metadata...")
# Rewrite metadata.yaml
with open(plug_path / "metadata.yaml", "w", encoding="utf-8") as f:
f.write(
f"name: {name}\n"
f"desc: {desc}\n"
f"version: {version}\n"
f"author: {author}\n"
f"repo: {repo}\n",
)
# Rewrite README.md
with open(plug_path / "README.md", "w", encoding="utf-8") as f:
f.write(
f"# {name}\n\n{desc}\n\n# Support\n\n[Documentation](https://docs.astrbot.app)\n",
)
# Rewrite main.py
with open(plug_path / "main.py", encoding="utf-8") as f:
content = f.read()
new_content = content.replace(
'@register("helloworld", "YourName", "一个简单的 Hello World 插件", "1.0.0")',
f'@register("{name}", "{author}", "{desc}", "{version}")',
)
with open(plug_path / "main.py", "w", encoding="utf-8") as f:
f.write(new_content)
click.echo(f"Plugin {name} created successfully")
@plug.command()
@click.option("--all", "-a", is_flag=True, help="List uninstalled plugins")
def list(all: bool) -> None:
"""List plugins"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
plugins = build_plug_list(base_path / "plugins")
# Unpublished plugins
not_published_plugins = [
p for p in plugins if p["status"] == PluginStatus.NOT_PUBLISHED
]
if not_published_plugins:
display_plugins(not_published_plugins, "Unpublished Plugins", "red")
# Plugins needing update
need_update_plugins = [
p for p in plugins if p["status"] == PluginStatus.NEED_UPDATE
]
if need_update_plugins:
display_plugins(need_update_plugins, "Plugins Needing Update", "yellow")
# Installed plugins
installed_plugins = [p for p in plugins if p["status"] == PluginStatus.INSTALLED]
if installed_plugins:
display_plugins(installed_plugins, "Installed Plugins", "green")
# Uninstalled plugins
not_installed_plugins = [
p for p in plugins if p["status"] == PluginStatus.NOT_INSTALLED
]
if not_installed_plugins and all:
display_plugins(not_installed_plugins, "Uninstalled Plugins", "blue")
if (
not any([not_published_plugins, need_update_plugins, installed_plugins])
and not all
):
click.echo("No plugins installed")
@plug.command()
@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 as a symlink",
)
@click.option("--proxy", help="Proxy server address")
def install(name: str | None, local_path: Path | None, proxy: str | None) -> None:
"""Install a plugin"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
plug_path = base_path / "plugins"
if local_path is not None:
install_local_plugin(local_path, plug_path, editable=True)
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, editable=False)
return
plugins = build_plug_list(base_path / "plugins")
plugin = next(
(
p
for p in plugins
if p["name"] == name and p["status"] == PluginStatus.NOT_INSTALLED
),
None,
)
if not plugin:
raise click.ClickException(t("plugin_not_found_or_installed", name=name))
manage_plugin(plugin, plug_path, is_update=False, proxy=proxy)
@plug.command()
@click.argument("name")
def remove(name: str) -> None:
"""Uninstall a plugin"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
plugins = build_plug_list(base_path / "plugins")
plugin = next((p for p in plugins if p["name"] == name), None)
if not plugin or not plugin.get("local_path"):
raise click.ClickException(t("plugin_not_found_or_installed", name=name))
plugin_path = plugin["local_path"]
click.confirm(t("plugin_uninstall_confirm", name=name), default=False, abort=True)
try:
shutil.rmtree(plugin_path)
click.echo(t("plugin_uninstall_success", name=name))
except Exception as e:
raise click.ClickException(
t("plugin_uninstall_failed_ex", name=name, error=str(e)),
) from e
@plug.command()
@click.argument("name", required=False)
@click.option("--proxy", help="GitHub proxy address")
def update(name: str, proxy: str | None) -> None:
"""Update plugins"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
plug_path = base_path / "plugins"
plugins = build_plug_list(base_path / "plugins")
if name:
plugin = next(
(
p
for p in plugins
if p["name"] == name and p["status"] == PluginStatus.NEED_UPDATE
),
None,
)
if not plugin:
raise click.ClickException(
f"Plugin {name} does not need updating or cannot be updated",
)
manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
else:
need_update_plugins = [
p for p in plugins if p["status"] == PluginStatus.NEED_UPDATE
]
if not need_update_plugins:
click.echo(t("plugin_no_update_needed"))
return
click.echo(t("plugin_found_update", count=str(len(need_update_plugins))))
for plugin in need_update_plugins:
plugin_name = plugin["name"]
click.echo(t("plugin_updating", name=plugin_name))
manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
@plug.command()
@click.argument("query")
def search(query: str) -> None:
"""Search for plugins"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
plugins = build_plug_list(base_path / "plugins")
matched_plugins = [
p
for p in plugins
if query.lower() in p["name"].lower()
or query.lower() in p["desc"].lower()
or query.lower() in p["author"].lower()
]
if not matched_plugins:
click.echo(t("plugin_search_no_result", query=query))
return
display_plugins(matched_plugins, t("plugin_search_results", query=query), "cyan")