mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-12 07:50:15 +08:00
Compare commits
2 Commits
feat/plugi
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa9897cacb | ||
|
|
1835467544 |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"display_name": "AstrBot",
|
||||
"desc": "AstrBot's internal plugin, providing some basic capabilities."
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"display_name": "AstrBot",
|
||||
"desc": "AstrBot 的内部插件,提供一些基础能力。"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"display_name": "Built-in Commands",
|
||||
"desc": "AstrBot's internal plugin, providing built-in commands such as /reset, /help, and /sid."
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"display_name": "内置指令",
|
||||
"desc": "AstrBot 自带插件,提供 /reset、/help、/sid 等内置指令。"
|
||||
}
|
||||
}
|
||||
@@ -399,9 +399,6 @@ async def _ensure_persona_and_skills(
|
||||
event, extract_persona_custom_error_message_from_persona(persona)
|
||||
)
|
||||
|
||||
if req.system_prompt is None:
|
||||
req.system_prompt = ""
|
||||
|
||||
if persona:
|
||||
# Inject persona system prompt
|
||||
if prompt := persona["prompt"]:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import abc
|
||||
import asyncio
|
||||
import uuid
|
||||
from asyncio import Queue
|
||||
from collections.abc import Coroutine
|
||||
@@ -139,9 +138,7 @@ class Platform(abc.ABC):
|
||||
|
||||
异步方法。
|
||||
"""
|
||||
asyncio.create_task(
|
||||
Metric.upload(msg_event_tick=1, adapter_name=self.meta().name)
|
||||
)
|
||||
await Metric.upload(msg_event_tick=1, adapter_name=self.meta().name)
|
||||
|
||||
def commit_event(self, event: AstrMessageEvent) -> None:
|
||||
"""提交一个事件到事件队列。"""
|
||||
|
||||
@@ -949,9 +949,7 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
buffer.squash_plain()
|
||||
await self.send(buffer)
|
||||
|
||||
asyncio.create_task(
|
||||
Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
|
||||
)
|
||||
await Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
|
||||
self._has_send_oper = True
|
||||
|
||||
async def send_streaming(self, generator, use_fallback: bool = False):
|
||||
@@ -1002,9 +1000,7 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
if buffer:
|
||||
buffer.squash_plain()
|
||||
await self.send(buffer)
|
||||
asyncio.create_task(
|
||||
Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
|
||||
)
|
||||
await Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
|
||||
self._has_send_oper = True
|
||||
|
||||
async def _flush_and_close_card() -> None:
|
||||
@@ -1079,10 +1075,8 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
# If no text was produced at all, no card was created
|
||||
if card_id is None:
|
||||
if not fallback_used:
|
||||
asyncio.create_task(
|
||||
Metric.upload(
|
||||
msg_event_tick=1, adapter_name=self.platform_meta.name
|
||||
)
|
||||
await Metric.upload(
|
||||
msg_event_tick=1, adapter_name=self.platform_meta.name
|
||||
)
|
||||
self._has_send_oper = True
|
||||
return
|
||||
@@ -1090,7 +1084,5 @@ class LarkMessageEvent(AstrMessageEvent):
|
||||
await _flush_and_close_card()
|
||||
|
||||
# 内联父类 send_streaming 的副作用
|
||||
asyncio.create_task(
|
||||
Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
|
||||
)
|
||||
await Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
|
||||
self._has_send_oper = True
|
||||
|
||||
@@ -222,9 +222,8 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
):
|
||||
return
|
||||
|
||||
# 私聊主动推送不需要 msg_id,见 https://github.com/AstrBotDevs/AstrBot/issues/7904
|
||||
msg_id = self._session_last_message_id.get(session.session_id)
|
||||
if not msg_id and session.message_type != MessageType.FRIEND_MESSAGE:
|
||||
if not msg_id:
|
||||
logger.warning(
|
||||
"[QQOfficial] No cached msg_id for session: %s, skip send_by_session",
|
||||
session.session_id,
|
||||
|
||||
@@ -27,8 +27,6 @@ class StarMetadata:
|
||||
"""插件作者"""
|
||||
desc: str | None = None
|
||||
"""插件简介"""
|
||||
short_desc: str | None = None
|
||||
"""插件短简介"""
|
||||
version: str | None = None
|
||||
"""插件版本"""
|
||||
repo: str | None = None
|
||||
@@ -69,9 +67,6 @@ class StarMetadata:
|
||||
astrbot_version: str | None = None
|
||||
"""插件要求的 AstrBot 版本范围(PEP 440 specifier,如 >=4.13.0,<4.17.0)"""
|
||||
|
||||
i18n: dict[str, dict] = field(default_factory=dict)
|
||||
"""插件自带的国际化文案,按 locale 分组。"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import tempfile
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import yaml
|
||||
@@ -497,11 +496,6 @@ class PluginManager:
|
||||
name=metadata["name"],
|
||||
author=metadata["author"],
|
||||
desc=metadata["desc"],
|
||||
short_desc=(
|
||||
metadata["short_desc"]
|
||||
if isinstance(metadata.get("short_desc"), str)
|
||||
else None
|
||||
),
|
||||
version=metadata["version"],
|
||||
repo=metadata["repo"] if "repo" in metadata else None,
|
||||
display_name=metadata.get("display_name", None),
|
||||
@@ -519,49 +513,10 @@ class PluginManager:
|
||||
if isinstance(metadata.get("astrbot_version"), str)
|
||||
else None
|
||||
),
|
||||
i18n=PluginManager._load_plugin_i18n(plugin_path),
|
||||
)
|
||||
|
||||
return metadata
|
||||
|
||||
@staticmethod
|
||||
def _load_plugin_i18n(plugin_path: str) -> dict[str, dict]:
|
||||
plugin_root = Path(plugin_path)
|
||||
i18n_dir = plugin_root / ".astrbot-plugin" / "i18n"
|
||||
if not i18n_dir.is_dir():
|
||||
return {}
|
||||
|
||||
translations: dict[str, dict] = {}
|
||||
try:
|
||||
for file_path in i18n_dir.iterdir():
|
||||
if file_path.suffix.lower() != ".json":
|
||||
continue
|
||||
locale = file_path.stem
|
||||
if not locale or len(locale) > 32:
|
||||
continue
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
if file_path.stat().st_size > 1024 * 1024:
|
||||
logger.warning("插件 i18n 文件超过 1MB,已跳过: %s", file_path)
|
||||
continue
|
||||
|
||||
try:
|
||||
with file_path.open(encoding="utf-8") as f:
|
||||
locale_data = json.load(f)
|
||||
if isinstance(locale_data, dict):
|
||||
translations[locale] = locale_data
|
||||
else:
|
||||
logger.warning(
|
||||
"插件 i18n 文件内容不是 JSON object,已跳过: %s",
|
||||
file_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("加载插件 i18n 文件失败 %s: %s", file_path, exc)
|
||||
except OSError as exc:
|
||||
logger.warning("读取插件 i18n 目录失败 %s: %s", i18n_dir, exc)
|
||||
|
||||
return translations
|
||||
|
||||
@staticmethod
|
||||
def _normalize_plugin_dir_name(plugin_name: str) -> str:
|
||||
return plugin_name.strip()
|
||||
@@ -741,7 +696,6 @@ class PluginManager:
|
||||
"name": metadata.name,
|
||||
"author": metadata.author,
|
||||
"desc": metadata.desc,
|
||||
"short_desc": metadata.short_desc,
|
||||
"version": metadata.version,
|
||||
"repo": metadata.repo,
|
||||
"display_name": metadata.display_name,
|
||||
@@ -983,13 +937,11 @@ class PluginManager:
|
||||
metadata.name = metadata_yaml.name
|
||||
metadata.author = metadata_yaml.author
|
||||
metadata.desc = metadata_yaml.desc
|
||||
metadata.short_desc = metadata_yaml.short_desc
|
||||
metadata.version = metadata_yaml.version
|
||||
metadata.repo = metadata_yaml.repo
|
||||
metadata.display_name = metadata_yaml.display_name
|
||||
metadata.support_platforms = metadata_yaml.support_platforms
|
||||
metadata.astrbot_version = metadata_yaml.astrbot_version
|
||||
metadata.i18n = metadata_yaml.i18n
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。",
|
||||
@@ -1354,11 +1306,7 @@ class PluginManager:
|
||||
self._rebuild_failed_plugin_info()
|
||||
|
||||
async def install_plugin(
|
||||
self,
|
||||
repo_url: str,
|
||||
proxy: str = "",
|
||||
ignore_version_check: bool = False,
|
||||
download_url: str = "",
|
||||
self, repo_url: str, proxy: str = "", ignore_version_check: bool = False
|
||||
):
|
||||
"""从仓库 URL 安装插件
|
||||
|
||||
@@ -1367,7 +1315,6 @@ class PluginManager:
|
||||
Args:
|
||||
repo_url (str): 要安装的插件仓库 URL
|
||||
proxy (str, optional): 用于下载的代理服务器。默认为空字符串。
|
||||
download_url (str, optional): 插件压缩包下载地址。提供时优先从此地址下载安装包。
|
||||
|
||||
Returns:
|
||||
dict | None: 安装成功时返回包含插件信息的字典:
|
||||
@@ -1395,14 +1342,7 @@ class PluginManager:
|
||||
raise Exception(
|
||||
f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。"
|
||||
)
|
||||
if download_url:
|
||||
plugin_path = await self.updator.install(
|
||||
repo_url,
|
||||
proxy,
|
||||
download_url=download_url,
|
||||
)
|
||||
else:
|
||||
plugin_path = await self.updator.install(repo_url, proxy)
|
||||
plugin_path = await self.updator.install(repo_url, proxy)
|
||||
|
||||
# reload the plugin
|
||||
dir_name = os.path.basename(plugin_path)
|
||||
|
||||
@@ -18,15 +18,11 @@ class PluginUpdator(RepoZipUpdator):
|
||||
def get_plugin_store_path(self) -> str:
|
||||
return self.plugin_store_path
|
||||
|
||||
async def install(self, repo_url: str, proxy="", download_url: str = "") -> str:
|
||||
async def install(self, repo_url: str, proxy="") -> str:
|
||||
_, repo_name, _ = self.parse_github_url(repo_url)
|
||||
repo_name = self.format_name(repo_name)
|
||||
plugin_path = os.path.join(self.plugin_store_path, repo_name)
|
||||
if download_url:
|
||||
logger.info(f"Downloading plugin archive for {repo_name}: {download_url}")
|
||||
await self._download_file(download_url, plugin_path + ".zip")
|
||||
else:
|
||||
await self.download_from_repo_url(plugin_path, repo_url, proxy)
|
||||
await self.download_from_repo_url(plugin_path, repo_url, proxy)
|
||||
self.unzip_file(plugin_path + ".zip", plugin_path)
|
||||
|
||||
return plugin_path
|
||||
@@ -35,25 +31,21 @@ class PluginUpdator(RepoZipUpdator):
|
||||
repo_url = plugin.repo
|
||||
|
||||
if not repo_url:
|
||||
raise Exception(f"Plugin {plugin.name} does not specify a repository URL.")
|
||||
raise Exception(f"插件 {plugin.name} 没有指定仓库地址。")
|
||||
|
||||
if not plugin.root_dir_name:
|
||||
raise Exception(
|
||||
f"Plugin {plugin.name} does not specify a root directory name."
|
||||
)
|
||||
raise Exception(f"插件 {plugin.name} 的根目录名未指定。")
|
||||
|
||||
plugin_path = os.path.join(self.plugin_store_path, plugin.root_dir_name)
|
||||
|
||||
logger.info(
|
||||
f"Updating plugin at path: {plugin_path}, repository URL: {repo_url}",
|
||||
)
|
||||
logger.info(f"正在更新插件,路径: {plugin_path},仓库地址: {repo_url}")
|
||||
await self.download_from_repo_url(plugin_path, repo_url, proxy=proxy)
|
||||
|
||||
try:
|
||||
remove_dir(plugin_path)
|
||||
except BaseException as e:
|
||||
logger.error(
|
||||
f"Failed to remove old plugin directory {plugin_path}: {e!s}; using overwrite installation.",
|
||||
f"删除旧版本插件 {plugin_path} 文件夹失败: {e!s},使用覆盖安装。",
|
||||
)
|
||||
|
||||
self.unzip_file(plugin_path + ".zip", plugin_path)
|
||||
@@ -63,7 +55,7 @@ class PluginUpdator(RepoZipUpdator):
|
||||
def unzip_file(self, zip_path: str, target_dir: str) -> None:
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
update_dir = ""
|
||||
logger.info(f"Extracting archive: {zip_path}")
|
||||
logger.info(f"正在解压压缩包: {zip_path}")
|
||||
with zipfile.ZipFile(zip_path, "r") as z:
|
||||
update_dir = z.namelist()[0]
|
||||
z.extractall(target_dir)
|
||||
@@ -79,11 +71,11 @@ class PluginUpdator(RepoZipUpdator):
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Removing temporary files: {zip_path} and {os.path.join(target_dir, update_dir)}",
|
||||
f"删除临时文件: {zip_path} 和 {os.path.join(target_dir, update_dir)}",
|
||||
)
|
||||
shutil.rmtree(os.path.join(target_dir, update_dir), onerror=on_error)
|
||||
os.remove(zip_path)
|
||||
except BaseException:
|
||||
logger.warning(
|
||||
f"Failed to remove update files; you can manually delete {zip_path} and {os.path.join(target_dir, update_dir)}",
|
||||
f"删除更新文件失败,可以手动删除 {zip_path} 和 {os.path.join(target_dir, update_dir)}",
|
||||
)
|
||||
|
||||
@@ -220,7 +220,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
||||
else session
|
||||
)
|
||||
except Exception as exc:
|
||||
return f"error: invalid session: {exc} - session should be a string in the format of 'platform_id:platform_type:session_id'."
|
||||
return f"error: invalid session: {exc}"
|
||||
|
||||
await context.context.context.send_message(
|
||||
target_session,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import asyncio
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import uuid
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
@@ -14,14 +11,6 @@ from astrbot.core.config import VERSION
|
||||
|
||||
class Metric:
|
||||
_iid_cache = None
|
||||
_has_uploaded_once = False
|
||||
_upload_interval_seconds = 10 * 60
|
||||
_max_pending_metric_groups = 64
|
||||
_counter_fields = {"llm_tick", "msg_event_tick"}
|
||||
_pending_metrics: dict[tuple[tuple[str, str], ...], dict[str, Any]] = {}
|
||||
_flush_task: asyncio.Task | None = None
|
||||
_lock: asyncio.Lock | None = None
|
||||
_lock_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
@staticmethod
|
||||
def get_installation_id():
|
||||
@@ -51,49 +40,25 @@ class Metric:
|
||||
return "null"
|
||||
|
||||
@staticmethod
|
||||
def _get_lock() -> asyncio.Lock:
|
||||
loop = asyncio.get_running_loop()
|
||||
if Metric._lock is None or Metric._lock_loop is not loop:
|
||||
Metric._lock = asyncio.Lock()
|
||||
Metric._lock_loop = loop
|
||||
return Metric._lock
|
||||
async def upload(**kwargs) -> None:
|
||||
"""上传相关非敏感的指标以更好地了解 AstrBot 的使用情况。上传的指标不会包含任何有关消息文本、用户信息等敏感信息。
|
||||
|
||||
@staticmethod
|
||||
def _format_group_value(value: Any) -> str:
|
||||
return repr(value)
|
||||
|
||||
@staticmethod
|
||||
def _get_metric_group_key(kwargs: dict[str, Any]) -> tuple[tuple[str, str], ...]:
|
||||
return tuple(
|
||||
sorted(
|
||||
(key, Metric._format_group_value(value))
|
||||
for key, value in kwargs.items()
|
||||
if key not in Metric._counter_fields
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_metric_group_fields(kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key not in Metric._counter_fields
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _coerce_counter(value: Any) -> int:
|
||||
Powered by TickStats.
|
||||
"""
|
||||
if os.environ.get("ASTRBOT_DISABLE_METRICS", "0") == "1":
|
||||
return
|
||||
base_url = "https://tickstats.soulter.top/api/metric/90a6c2a1"
|
||||
kwargs["v"] = VERSION
|
||||
kwargs["os"] = sys.platform
|
||||
payload = {"metrics_data": kwargs}
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _ensure_flush_task_locked() -> None:
|
||||
if Metric._flush_task is None or Metric._flush_task.done():
|
||||
Metric._flush_task = asyncio.create_task(Metric._flush_periodically())
|
||||
|
||||
@staticmethod
|
||||
async def _save_platform_stats(kwargs: dict[str, Any]) -> None:
|
||||
kwargs["hn"] = socket.gethostname()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
kwargs["iid"] = Metric.get_installation_id()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if "adapter_name" in kwargs:
|
||||
await db_helper.insert_platform_stats(
|
||||
@@ -103,93 +68,6 @@ class Metric:
|
||||
except Exception as e:
|
||||
logger.error(f"保存指标到数据库失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def _add_pending_metrics(kwargs: dict[str, Any]) -> None:
|
||||
key = Metric._get_metric_group_key(kwargs)
|
||||
immediate_metrics = None
|
||||
should_flush = False
|
||||
lock = Metric._get_lock()
|
||||
async with lock:
|
||||
if not Metric._has_uploaded_once:
|
||||
Metric._has_uploaded_once = True
|
||||
immediate_metrics = dict(kwargs)
|
||||
else:
|
||||
pending = Metric._pending_metrics.setdefault(
|
||||
key,
|
||||
Metric._get_metric_group_fields(kwargs),
|
||||
)
|
||||
for counter_field in Metric._counter_fields:
|
||||
if counter_field in kwargs:
|
||||
pending[counter_field] = pending.get(
|
||||
counter_field,
|
||||
0,
|
||||
) + Metric._coerce_counter(kwargs[counter_field])
|
||||
Metric._ensure_flush_task_locked()
|
||||
should_flush = (
|
||||
len(Metric._pending_metrics) > Metric._max_pending_metric_groups
|
||||
)
|
||||
|
||||
if immediate_metrics is not None:
|
||||
await Metric._post_metrics(immediate_metrics)
|
||||
return
|
||||
if should_flush:
|
||||
await Metric.flush()
|
||||
|
||||
@staticmethod
|
||||
async def _flush_periodically() -> None:
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(Metric._upload_interval_seconds)
|
||||
await Metric.flush()
|
||||
|
||||
lock = Metric._get_lock()
|
||||
async with lock:
|
||||
if not Metric._pending_metrics:
|
||||
Metric._flush_task = None
|
||||
return
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
current_task = asyncio.current_task()
|
||||
with suppress(RuntimeError):
|
||||
lock = Metric._get_lock()
|
||||
async with lock:
|
||||
if Metric._flush_task is current_task:
|
||||
Metric._flush_task = None
|
||||
|
||||
@staticmethod
|
||||
async def flush() -> None:
|
||||
"""Flush pending metrics immediately."""
|
||||
lock = Metric._get_lock()
|
||||
async with lock:
|
||||
pending_metrics = list(Metric._pending_metrics.values())
|
||||
Metric._pending_metrics = {}
|
||||
|
||||
for metrics_data in pending_metrics:
|
||||
await Metric._post_metrics(metrics_data)
|
||||
await asyncio.sleep(0.25)
|
||||
|
||||
@staticmethod
|
||||
async def _post_metrics(metrics_data: dict[str, Any]) -> None:
|
||||
if os.environ.get("ASTRBOT_DISABLE_METRICS", "0") == "1":
|
||||
return
|
||||
|
||||
base_url = "https://tickstats.soulter.top/api/metric/90a6c2a1"
|
||||
payload_metrics = dict(metrics_data)
|
||||
payload_metrics["v"] = VERSION
|
||||
payload_metrics["os"] = sys.platform
|
||||
try:
|
||||
payload_metrics["hn"] = socket.gethostname()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
payload_metrics["iid"] = Metric.get_installation_id()
|
||||
except Exception:
|
||||
pass
|
||||
payload = {"metrics_data": payload_metrics}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.post(base_url, json=payload, timeout=3) as response:
|
||||
@@ -197,15 +75,3 @@ class Metric:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def upload(**kwargs) -> None:
|
||||
"""上传相关非敏感的指标以更好地了解 AstrBot 的使用情况。上传的指标不会包含任何有关消息文本、用户信息等敏感信息。
|
||||
|
||||
Powered by TickStats.
|
||||
"""
|
||||
if os.environ.get("ASTRBOT_DISABLE_METRICS", "0") == "1":
|
||||
return
|
||||
|
||||
await Metric._save_platform_stats(kwargs)
|
||||
await Metric._add_pending_metrics(dict(kwargs))
|
||||
|
||||
@@ -1498,7 +1498,7 @@ class ConfigRoute(Route):
|
||||
}
|
||||
|
||||
async def _get_plugin_config(self, plugin_name: str):
|
||||
ret: dict = {"metadata": None, "config": None, "i18n": {}}
|
||||
ret: dict = {"metadata": None, "config": None}
|
||||
|
||||
for plugin_md in star_registry:
|
||||
if plugin_md.name == plugin_name:
|
||||
@@ -1514,7 +1514,6 @@ class ConfigRoute(Route):
|
||||
"items": plugin_md.config.schema, # 初始化时通过 __setattr__ 存入了 schema
|
||||
},
|
||||
}
|
||||
ret["i18n"] = plugin_md.i18n
|
||||
break
|
||||
|
||||
return ret
|
||||
|
||||
@@ -409,7 +409,6 @@ class PluginRoute(Route):
|
||||
"support_platforms": plugin.support_platforms,
|
||||
"astrbot_version": plugin.astrbot_version,
|
||||
"installed_at": self._get_plugin_installed_at(plugin),
|
||||
"i18n": plugin.i18n,
|
||||
}
|
||||
# 检查是否为全空的幽灵插件
|
||||
if not any(
|
||||
@@ -504,7 +503,6 @@ class PluginRoute(Route):
|
||||
|
||||
post_data = await request.get_json()
|
||||
repo_url = post_data["url"]
|
||||
download_url = str(post_data.get("download_url") or "").strip()
|
||||
ignore_version_check = bool(post_data.get("ignore_version_check", False))
|
||||
|
||||
proxy: str = post_data.get("proxy", None)
|
||||
@@ -517,7 +515,6 @@ class PluginRoute(Route):
|
||||
repo_url,
|
||||
proxy,
|
||||
ignore_version_check=ignore_version_check,
|
||||
download_url=download_url,
|
||||
)
|
||||
# self.core_lifecycle.restart()
|
||||
logger.info(f"安装插件 {repo_url} 成功。")
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import PluginPlatformChip from "@/components/shared/PluginPlatformChip.vue";
|
||||
import { usePluginI18n } from "@/utils/pluginI18n";
|
||||
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
const { pluginShortDesc } = usePluginI18n();
|
||||
|
||||
const props = defineProps({
|
||||
plugin: {
|
||||
@@ -22,7 +20,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["install", "open"]);
|
||||
const emit = defineEmits(["install"]);
|
||||
|
||||
const normalizePlatformList = (platforms) => {
|
||||
if (!Array.isArray(platforms)) return [];
|
||||
@@ -33,27 +31,16 @@ const platformDisplayList = computed(() =>
|
||||
normalizePlatformList(props.plugin?.support_platforms),
|
||||
);
|
||||
|
||||
const cardDescription = computed(() =>
|
||||
pluginShortDesc(props.plugin, props.plugin?.short_desc || props.plugin?.desc || ""),
|
||||
);
|
||||
|
||||
const handleInstall = (plugin) => {
|
||||
emit("install", plugin);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
emit("open", props.plugin);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
class="rounded-lg d-flex flex-column plugin-card"
|
||||
variant="outlined"
|
||||
elevation="0"
|
||||
:ripple="false"
|
||||
@click="handleOpen"
|
||||
>
|
||||
|
||||
<v-card-text
|
||||
@@ -87,15 +74,6 @@ const handleOpen = () => {
|
||||
>
|
||||
{{ tm("market.recommended") }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="plugin?.astrbot_compatible === false"
|
||||
color="error"
|
||||
size="x-small"
|
||||
label
|
||||
class="market-incompatible-chip"
|
||||
>
|
||||
{{ tm("status.incompatible") }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center plugin-meta">
|
||||
@@ -132,6 +110,17 @@ const handleOpen = () => {
|
||||
>
|
||||
{{ plugin.author }}
|
||||
</span>
|
||||
<div
|
||||
class="d-flex align-center text-subtitle-2 ml-2"
|
||||
style="color: rgba(var(--v-theme-on-surface), 0.7)"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-source-branch"
|
||||
size="x-small"
|
||||
style="margin-right: 2px"
|
||||
></v-icon>
|
||||
<span>{{ plugin.version }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="plugin.stars !== undefined"
|
||||
class="d-flex align-center text-subtitle-2 ml-2"
|
||||
@@ -147,7 +136,27 @@ const handleOpen = () => {
|
||||
</div>
|
||||
|
||||
<div class="text-caption plugin-description">
|
||||
{{ cardDescription }}
|
||||
{{ plugin.desc }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="plugin.astrbot_version || platformDisplayList.length"
|
||||
class="plugin-badges"
|
||||
>
|
||||
<v-chip
|
||||
v-if="plugin.astrbot_version"
|
||||
size="x-small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
style="height: 20px"
|
||||
>
|
||||
AstrBot: {{ plugin.astrbot_version }}
|
||||
</v-chip>
|
||||
<PluginPlatformChip
|
||||
:platforms="plugin.support_platforms"
|
||||
size="x-small"
|
||||
:chip-style="{ height: '20px' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="plugin-stats"></div>
|
||||
@@ -158,16 +167,36 @@ const handleOpen = () => {
|
||||
style="gap: 6px; padding: 8px 12px; padding-top: 0"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
v-if="platformDisplayList.length"
|
||||
class="plugin-badges"
|
||||
<v-chip
|
||||
v-for="tag in plugin.tags?.slice(0, 2)"
|
||||
:key="tag"
|
||||
:color="tag === 'danger' ? 'error' : 'primary'"
|
||||
label
|
||||
size="x-small"
|
||||
style="height: 20px"
|
||||
>
|
||||
<PluginPlatformChip
|
||||
:platforms="plugin.support_platforms"
|
||||
size="x-small"
|
||||
:chip-style="{ height: '20px' }"
|
||||
/>
|
||||
</div>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
<v-menu v-if="plugin.tags && plugin.tags.length > 2" open-on-hover offset-y>
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-chip
|
||||
v-bind="menuProps"
|
||||
color="grey"
|
||||
label
|
||||
size="x-small"
|
||||
style="height: 20px; cursor: pointer"
|
||||
>
|
||||
+{{ plugin.tags.length - 2 }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item v-for="tag in plugin.tags.slice(2)" :key="tag">
|
||||
<v-chip :color="tag === 'danger' ? 'error' : 'primary'" label size="small">
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-if="plugin?.repo"
|
||||
@@ -209,22 +238,6 @@ const handleOpen = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plugin-card {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
cursor: pointer;
|
||||
transition: background-color 0.16s ease;
|
||||
}
|
||||
|
||||
.plugin-card:hover,
|
||||
.plugin-card:focus-within {
|
||||
background: rgba(var(--v-theme-on-surface), 0.04);
|
||||
}
|
||||
|
||||
.plugin-card :deep(.v-card__overlay),
|
||||
.plugin-card :deep(.v-ripple__container) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plugin-card-content {
|
||||
padding: 12px;
|
||||
padding-bottom: 8px;
|
||||
@@ -273,12 +286,6 @@ const handleOpen = () => {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.market-incompatible-chip {
|
||||
flex-shrink: 0;
|
||||
font-weight: 700;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.plugin-title {
|
||||
line-height: 1.3;
|
||||
font-size: 1rem;
|
||||
@@ -300,11 +307,11 @@ const handleOpen = () => {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
min-height: calc(1.3em * 2);
|
||||
max-height: calc(1.3em * 2);
|
||||
min-height: calc(1.3em * 3);
|
||||
max-height: calc(1.3em * 3);
|
||||
}
|
||||
|
||||
.plugin-badges {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ref, computed } from 'vue'
|
||||
import ConfigItemRenderer from './ConfigItemRenderer.vue'
|
||||
import TemplateListEditor from './TemplateListEditor.vue'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||
import { useConfigTextResolver } from '@/composables/useConfigTextResolver'
|
||||
import axios from 'axios'
|
||||
import { useToast } from '@/utils/toast'
|
||||
|
||||
@@ -25,10 +24,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
pluginI18n: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
pathPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -40,9 +35,12 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const { getRaw } = useModuleI18n('features/config-metadata')
|
||||
const { translateIfKey, resolveConfigText } = useConfigTextResolver(props)
|
||||
const currentConfigPath = computed(() => props.pathPrefix || props.metadataKey)
|
||||
const { tm, getRaw } = useModuleI18n('features/config-metadata')
|
||||
|
||||
const translateIfKey = (value) => {
|
||||
if (!value || typeof value !== 'string') return value
|
||||
return getRaw(value) ? tm(value) : value
|
||||
}
|
||||
|
||||
const filteredIterable = computed(() => {
|
||||
if (!props.iterable) return {}
|
||||
@@ -176,11 +174,11 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<template>
|
||||
<div class="config-section" v-if="iterable && metadata[metadataKey]?.type === 'object'">
|
||||
<v-list-item-title class="config-title">
|
||||
{{ resolveConfigText(currentConfigPath, 'description', metadata[metadataKey]?.description) }} <span class="metadata-key">({{ metadataKey }})</span>
|
||||
{{ translateIfKey(metadata[metadataKey]?.description) }} <span class="metadata-key">({{ metadataKey }})</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
{{ resolveConfigText(currentConfigPath, 'hint', metadata[metadataKey]?.hint) }}
|
||||
{{ translateIfKey(metadata[metadataKey]?.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
|
||||
@@ -209,7 +207,6 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
:iterable="iterable[key]"
|
||||
:metadataKey="key"
|
||||
:pluginName="pluginName"
|
||||
:pluginI18n="pluginI18n"
|
||||
:pathPrefix="getItemPath(key)"
|
||||
>
|
||||
</AstrBotConfig>
|
||||
@@ -223,22 +220,19 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<div class="config-section mb-2">
|
||||
<v-list-item-title class="config-title">
|
||||
<span v-if="metadata[metadataKey].items[key]?.description">
|
||||
{{ resolveConfigText(getItemPath(key), 'description', metadata[metadataKey].items[key]?.description) }}
|
||||
{{ translateIfKey(metadata[metadataKey].items[key]?.description) }}
|
||||
<span class="property-key">({{ key }})</span>
|
||||
</span>
|
||||
<span v-else>{{ key }}</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint" class="important-hint">‼️</span>
|
||||
{{ resolveConfigText(getItemPath(key), 'hint', metadata[metadataKey].items[key]?.hint) }}
|
||||
{{ translateIfKey(metadata[metadataKey].items[key]?.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
<TemplateListEditor
|
||||
v-model="iterable[key]"
|
||||
:templates="metadata[metadataKey].items[key]?.templates || {}"
|
||||
:plugin-name="pluginName"
|
||||
:plugin-i18n="pluginI18n"
|
||||
:config-path="getItemPath(key)"
|
||||
class="config-field"
|
||||
/>
|
||||
</div>
|
||||
@@ -251,7 +245,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
<span v-if="metadata[metadataKey].items[key]?.description">
|
||||
{{ resolveConfigText(getItemPath(key), 'description', metadata[metadataKey].items[key]?.description) }}
|
||||
{{ translateIfKey(metadata[metadataKey].items[key]?.description) }}
|
||||
<span class="property-key">({{ key }})</span>
|
||||
</span>
|
||||
<span v-else>{{ key }}</span>
|
||||
@@ -260,7 +254,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && getItemHint(key, metadata[metadataKey].items[key])"
|
||||
class="important-hint">‼️</span>
|
||||
{{ resolveConfigText(getItemPath(key), 'hint', getItemHint(key, metadata[metadataKey].items[key])) }}
|
||||
{{ translateIfKey(getItemHint(key, metadata[metadataKey].items[key])) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
@@ -270,7 +264,6 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
v-model="iterable[key]"
|
||||
:item-meta="metadata[metadataKey].items[key] || null"
|
||||
:plugin-name="pluginName"
|
||||
:plugin-i18n="pluginI18n"
|
||||
:config-key="getItemPath(key)"
|
||||
:loading="loadingEmbeddingDim"
|
||||
:show-fullscreen-btn="!!metadata[metadataKey].items[key]?.editor_mode"
|
||||
@@ -294,13 +287,13 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<v-col cols="12" sm="7" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ resolveConfigText(getItemPath(metadataKey), 'description', metadata[metadataKey]?.description) }}
|
||||
{{ metadata[metadataKey]?.description }}
|
||||
<span class="property-key">({{ metadataKey }})</span>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
{{ resolveConfigText(getItemPath(metadataKey), 'hint', metadata[metadataKey]?.hint) }}
|
||||
{{ metadata[metadataKey]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
@@ -310,9 +303,6 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
v-if="metadata[metadataKey]?.type === 'template_list' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
:templates="metadata[metadataKey]?.templates || {}"
|
||||
:plugin-name="pluginName"
|
||||
:plugin-i18n="pluginI18n"
|
||||
:config-path="getItemPath(metadataKey)"
|
||||
class="config-field"
|
||||
/>
|
||||
<ConfigItemRenderer
|
||||
@@ -320,7 +310,6 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
v-model="iterable[metadataKey]"
|
||||
:item-meta="metadata[metadataKey]"
|
||||
:plugin-name="pluginName"
|
||||
:plugin-i18n="pluginI18n"
|
||||
:config-key="getItemPath(metadataKey)"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
@@ -6,7 +6,6 @@ import ConfigItemRenderer from './ConfigItemRenderer.vue'
|
||||
import TemplateListEditor from './TemplateListEditor.vue'
|
||||
import PersonaQuickPreview from './PersonaQuickPreview.vue'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||
import { useConfigTextResolver } from '@/composables/useConfigTextResolver'
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
@@ -29,15 +28,20 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const { getRaw } = useModuleI18n('features/config-metadata')
|
||||
const { tm, getRaw } = useModuleI18n('features/config-metadata')
|
||||
const { tm: tmConfig } = useModuleI18n('features/config')
|
||||
const { translateIfKey } = useConfigTextResolver()
|
||||
|
||||
const hintMarkdown = new MarkdownIt({
|
||||
linkify: true,
|
||||
breaks: true
|
||||
})
|
||||
|
||||
// 翻译器函数 - 如果是国际化键则翻译,否则原样返回
|
||||
const translateIfKey = (value) => {
|
||||
if (!value || typeof value !== 'string') return value
|
||||
return tm(value)
|
||||
}
|
||||
|
||||
const renderHint = (value) => {
|
||||
const text = translateIfKey(value)
|
||||
if (!text) return ''
|
||||
|
||||
@@ -213,9 +213,6 @@
|
||||
v-else-if="itemMeta?.type === 'dict'"
|
||||
:model-value="modelValue"
|
||||
:item-meta="itemMeta"
|
||||
:plugin-name="pluginName"
|
||||
:plugin-i18n="pluginI18n"
|
||||
:config-key="configKey"
|
||||
@update:model-value="emitUpdate"
|
||||
class="config-field"
|
||||
/>
|
||||
@@ -244,7 +241,6 @@ import PluginSetSelector from './PluginSetSelector.vue'
|
||||
import T2ITemplateEditor from './T2ITemplateEditor.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||
import { usePluginI18n } from '@/utils/pluginI18n'
|
||||
|
||||
const numericTemp = ref(null)
|
||||
const listSearchText = ref('')
|
||||
@@ -262,10 +258,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
pluginI18n: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
configKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -283,7 +275,6 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue', 'get-embedding-dim', 'open-fullscreen'])
|
||||
const { t } = useI18n()
|
||||
const { getRaw } = useModuleI18n('features/config-metadata')
|
||||
const { configText } = usePluginI18n()
|
||||
|
||||
function emitUpdate(val) {
|
||||
emit('update:modelValue', val)
|
||||
@@ -306,17 +297,6 @@ function getLabel(itemMeta, index, option) {
|
||||
}
|
||||
|
||||
function getTranslatedLabels(itemMeta) {
|
||||
if (
|
||||
props.pluginName
|
||||
&& props.configKey
|
||||
&& props.pluginI18n
|
||||
&& Object.keys(props.pluginI18n).length > 0
|
||||
) {
|
||||
const translatedLabels = configText(props.pluginI18n, props.configKey, 'labels', null)
|
||||
if (Array.isArray(translatedLabels)) {
|
||||
return translatedLabels
|
||||
}
|
||||
}
|
||||
if (!itemMeta?.labels) return null
|
||||
if (typeof itemMeta.labels === 'string') {
|
||||
const translatedLabels = getRaw(itemMeta.labels)
|
||||
|
||||
@@ -6,7 +6,6 @@ import UninstallConfirmDialog from "./UninstallConfirmDialog.vue";
|
||||
import PluginPlatformChip from "./PluginPlatformChip.vue";
|
||||
import StyledMenu from "./StyledMenu.vue";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
import { usePluginI18n } from "@/utils/pluginI18n";
|
||||
|
||||
const props = defineProps({
|
||||
extension: {
|
||||
@@ -46,7 +45,6 @@ const attrs = useAttrs();
|
||||
|
||||
// 国际化
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
const { pluginName, pluginDesc } = usePluginI18n();
|
||||
|
||||
const supportPlatforms = computed(() => {
|
||||
const platforms = props.extension?.support_platforms;
|
||||
@@ -75,10 +73,6 @@ const logoSrc = computed(() => {
|
||||
: defaultPluginIcon;
|
||||
});
|
||||
|
||||
const localizedName = computed(() => pluginName(props.extension));
|
||||
|
||||
const localizedDesc = computed(() => pluginDesc(props.extension));
|
||||
|
||||
watch(
|
||||
() => props.extension?.logo,
|
||||
() => {
|
||||
@@ -176,15 +170,17 @@ const togglePin = () => {
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="
|
||||
localizedName?.length &&
|
||||
localizedName !== extension.name
|
||||
? `${localizedName} (${extension.name})`
|
||||
extension.display_name?.length &&
|
||||
extension.display_name !== extension.name
|
||||
? `${extension.display_name} (${extension.name})`
|
||||
: extension.name
|
||||
"
|
||||
>
|
||||
<template v-slot:activator="{ props: titleTooltipProps }">
|
||||
<span v-bind="titleTooltipProps" class="extension-title__text">{{
|
||||
localizedName
|
||||
extension.display_name?.length
|
||||
? extension.display_name
|
||||
: extension.name
|
||||
}}</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
@@ -284,7 +280,7 @@ const togglePin = () => {
|
||||
class="extension-desc"
|
||||
:class="{ 'text-caption': $vuetify.display.xs }"
|
||||
>
|
||||
{{ localizedDesc }}
|
||||
{{ extension.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-card class="item-card hover-elevation" style="padding: 4px;" :variant="variant" elevation="0">
|
||||
<v-card class="item-card hover-elevation" style="padding: 4px;" elevation="0">
|
||||
<v-card-title class="d-flex justify-space-between align-center pb-1 pt-3">
|
||||
<span class="text-h2 text-truncate" :title="getItemTitle()">{{ getItemTitle() }}</span>
|
||||
<v-tooltip location="top">
|
||||
@@ -116,10 +116,6 @@ export default {
|
||||
disableDelete: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
emits: ['toggle-enabled', 'delete', 'edit', 'copy'],
|
||||
@@ -139,10 +135,9 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.item-card {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
position: relative;
|
||||
border-radius: 18px;
|
||||
transition: background-color 0.16s ease, transform 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
min-height: 220px;
|
||||
height: 100%;
|
||||
@@ -152,7 +147,6 @@ export default {
|
||||
}
|
||||
|
||||
.hover-elevation:hover {
|
||||
background: rgba(var(--v-theme-on-surface), 0.04);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<v-col cols="4">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-caption font-weight-medium">{{ getTemplateTitle(template, templateKey) }}</span>
|
||||
<span v-if="template.hint" class="text-caption text-grey" style="font-size: 0.7rem;">{{ resolveTemplateText(templateKey, 'hint', template.hint) }}</span>
|
||||
<span v-if="template.hint" class="text-caption text-grey" style="font-size: 0.7rem;">{{ translateIfKey(template.hint) }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
|
||||
@@ -221,11 +221,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||
import { useToast } from '@/utils/toast'
|
||||
import { useConfigTextResolver } from '@/composables/useConfigTextResolver'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { tm, getRaw } = useModuleI18n('features/config-metadata')
|
||||
const { warning: toastWarning } = useToast()
|
||||
|
||||
const props = defineProps({
|
||||
@@ -237,18 +237,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
pluginName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
pluginI18n: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
configKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -263,8 +251,6 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const { translateIfKey, resolveConfigText } = useConfigTextResolver(props)
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const resolveButtonText = computed(() => props.buttonText || t('core.common.list.modifyButton'))
|
||||
@@ -529,15 +515,13 @@ function cancelDialog() {
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function getTemplateTitle(template, templateKey) {
|
||||
return resolveTemplateText(templateKey, 'name', template?.name || template?.description || templateKey)
|
||||
function translateIfKey(value) {
|
||||
if (!value || typeof value !== 'string') return value
|
||||
return getRaw(value) ? tm(value) : value
|
||||
}
|
||||
|
||||
function resolveTemplateText(templateKey, attr, fallback) {
|
||||
if (!props.configKey) {
|
||||
return translateIfKey(fallback) || ''
|
||||
}
|
||||
return resolveConfigText(`${props.configKey}.template_schema.${templateKey}`, attr, fallback)
|
||||
function getTemplateTitle(template, templateKey) {
|
||||
return translateIfKey(template?.name || template?.description || templateKey)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -554,3 +538,4 @@ function resolveTemplateText(templateKey, attr, fallback) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@
|
||||
></v-checkbox>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>{{ pluginDisplayName(plugin) }}</v-list-item-title>
|
||||
<v-list-item-title>{{ plugin.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ pluginDescription(plugin) || tm('pluginSetSelector.noDescription') }}
|
||||
{{ plugin.desc || tm('pluginSetSelector.noDescription') }}
|
||||
<v-chip v-if="!plugin.activated" size="x-small" color="grey" class="ml-1">
|
||||
{{ tm('pluginSetSelector.notActivated') }}
|
||||
</v-chip>
|
||||
@@ -105,7 +105,6 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
import { usePluginI18n } from '@/utils/pluginI18n'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -124,7 +123,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { tm } = useModuleI18n('core.shared')
|
||||
const { pluginName, pluginDesc } = usePluginI18n()
|
||||
|
||||
const dialog = ref(false)
|
||||
const pluginList = ref([])
|
||||
@@ -132,9 +130,6 @@ const loading = ref(false)
|
||||
const selectionMode = ref('custom') // 'all', 'none', 'custom'
|
||||
const selectedPlugins = ref([])
|
||||
|
||||
const pluginDisplayName = (plugin) => pluginName(plugin) || plugin.name
|
||||
const pluginDescription = (plugin) => pluginDesc(plugin)
|
||||
|
||||
// 判断是否为"所有插件"模式
|
||||
const isAllPlugins = computed(() => {
|
||||
return props.modelValue && props.modelValue.length === 1 && props.modelValue[0] === '*'
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
:key="option.value"
|
||||
@click="addEntry(option.value)"
|
||||
>
|
||||
<v-list-item-title>{{ option.label }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="option.hint">{{ option.hint }}</v-list-item-subtitle>
|
||||
<v-list-item-title>{{ translateIfKey(option.label) }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="option.hint">{{ translateIfKey(option.hint) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="d-flex flex-column">
|
||||
<v-list-item-title class="property-name">{{ templateLabel(entry.__template_key) }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="property-hint" v-if="getTemplate(entry)?.hint || getTemplate(entry)?.description">
|
||||
{{ templateText(entry.__template_key, 'hint', getTemplate(entry)?.hint || getTemplate(entry)?.description) }}
|
||||
{{ translateIfKey(getTemplate(entry)?.hint || getTemplate(entry)?.description) }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,10 +82,10 @@
|
||||
>
|
||||
<div class="config-section mb-2">
|
||||
<v-list-item-title class="config-title">
|
||||
{{ templateItemText(entry.__template_key, itemKey, 'description', itemMeta?.description) || itemKey }}
|
||||
{{ translateIfKey(itemMeta?.description) || itemKey }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint" v-if="itemMeta?.hint">
|
||||
{{ templateItemText(entry.__template_key, itemKey, 'hint', itemMeta.hint) }}
|
||||
{{ translateIfKey(itemMeta.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
<div v-for="(childMeta, childKey, childIndex) in itemMeta.items" :key="childKey">
|
||||
@@ -94,10 +94,10 @@
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ templateItemText(entry.__template_key, `${itemKey}.${childKey}`, 'description', childMeta?.description) || childKey }}
|
||||
{{ translateIfKey(childMeta?.description) || childKey }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
{{ templateItemText(entry.__template_key, `${itemKey}.${childKey}`, 'hint', childMeta?.hint) }}
|
||||
{{ translateIfKey(childMeta?.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
@@ -105,9 +105,6 @@
|
||||
<ConfigItemRenderer
|
||||
v-model="entry[itemKey][childKey]"
|
||||
:item-meta="childMeta"
|
||||
:plugin-name="pluginName"
|
||||
:plugin-i18n="pluginI18n"
|
||||
:config-key="templateItemPath(entry.__template_key, `${itemKey}.${childKey}`)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -125,11 +122,11 @@
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
<span v-if="itemMeta?.description">{{ templateItemText(entry.__template_key, itemKey, 'description', itemMeta?.description) }} <span class="property-key">({{ itemKey }})</span></span>
|
||||
<span v-if="itemMeta?.description">{{ translateIfKey(itemMeta?.description) }} <span class="property-key">({{ itemKey }})</span></span>
|
||||
<span v-else>{{ itemKey }}</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
{{ templateItemText(entry.__template_key, itemKey, 'hint', itemMeta?.hint) }}
|
||||
{{ translateIfKey(itemMeta?.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
@@ -137,9 +134,6 @@
|
||||
<ConfigItemRenderer
|
||||
v-model="entry[itemKey]"
|
||||
:item-meta="itemMeta"
|
||||
:plugin-name="pluginName"
|
||||
:plugin-i18n="pluginI18n"
|
||||
:config-key="templateItemPath(entry.__template_key, itemKey)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -159,8 +153,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import ConfigItemRenderer from './ConfigItemRenderer.vue'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
import { useConfigTextResolver } from '@/composables/useConfigTextResolver'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -170,24 +163,12 @@ const props = defineProps({
|
||||
templates: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
pluginName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
pluginI18n: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
configPath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { t } = useI18n()
|
||||
const { resolveConfigText } = useConfigTextResolver(props)
|
||||
const { tm, getRaw } = useModuleI18n('features/config-metadata')
|
||||
|
||||
const expandedEntries = ref({})
|
||||
|
||||
@@ -207,31 +188,20 @@ const defaultValueMap = {
|
||||
|
||||
const templateOptions = computed(() => {
|
||||
return Object.entries(props.templates || {}).map(([value, meta]) => ({
|
||||
label: templateText(value, 'name', meta?.name || value),
|
||||
label: meta?.name || value,
|
||||
value,
|
||||
hint: templateText(value, 'hint', meta?.hint || meta?.description || '')
|
||||
hint: meta?.hint || meta?.description || ''
|
||||
}))
|
||||
})
|
||||
|
||||
function templateLabel(key) {
|
||||
if (!key) return t('core.common.templateList.unknownTemplate') || '未指定模板'
|
||||
return templateText(key, 'name', props.templates?.[key]?.name || key)
|
||||
return translateIfKey(props.templates?.[key]?.name || key)
|
||||
}
|
||||
|
||||
function templatePath(templateKey) {
|
||||
return props.configPath ? `${props.configPath}.templates.${templateKey}` : `templates.${templateKey}`
|
||||
}
|
||||
|
||||
function templateItemPath(templateKey, itemPath) {
|
||||
return `${templatePath(templateKey)}.${itemPath}`
|
||||
}
|
||||
|
||||
function templateText(templateKey, attr, fallback) {
|
||||
return resolveConfigText(templatePath(templateKey), attr, fallback)
|
||||
}
|
||||
|
||||
function templateItemText(templateKey, itemPath, attr, fallback) {
|
||||
return resolveConfigText(templateItemPath(templateKey, itemPath), attr, fallback)
|
||||
function translateIfKey(value) {
|
||||
if (!value || typeof value !== 'string') return value
|
||||
return getRaw(value) ? tm(value) : value
|
||||
}
|
||||
|
||||
function buildDefaults(itemsMeta = {}) {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
import { usePluginI18n } from '@/utils/pluginI18n'
|
||||
|
||||
export function useConfigTextResolver(props = {}) {
|
||||
const { tm, getRaw } = useModuleI18n('features/config-metadata')
|
||||
const { configText } = usePluginI18n()
|
||||
|
||||
const translateIfKey = (value) => {
|
||||
if (!value || typeof value !== 'string') return value
|
||||
return getRaw(value) ? tm(value) : value
|
||||
}
|
||||
|
||||
const hasPluginI18n = () => {
|
||||
return Boolean(
|
||||
props.pluginName
|
||||
&& props.pluginI18n
|
||||
&& Object.keys(props.pluginI18n).length > 0,
|
||||
)
|
||||
}
|
||||
|
||||
const resolveConfigText = (path, attr, fallback) => {
|
||||
const fallbackText = translateIfKey(fallback) || ''
|
||||
if (!hasPluginI18n()) {
|
||||
return fallbackText
|
||||
}
|
||||
return configText(props.pluginI18n, path, attr, fallbackText)
|
||||
}
|
||||
|
||||
return {
|
||||
translateIfKey,
|
||||
resolveConfigText,
|
||||
}
|
||||
}
|
||||
@@ -61,10 +61,9 @@
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"system": "System",
|
||||
"loading": "Loading...",
|
||||
"installed": "Installed",
|
||||
"unknown": "Unknown",
|
||||
"incompatible": "Incompatible"
|
||||
"loading": "Loading...",
|
||||
"installed": "Installed",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"tooltips": {
|
||||
"enable": "Click to Enable",
|
||||
@@ -110,14 +109,9 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Info",
|
||||
"version": "Version",
|
||||
"author": "Author",
|
||||
"category": "Category",
|
||||
"stars": "Stars",
|
||||
"tags": "Tags",
|
||||
"astrbotVersion": "AstrBot Version Requirement",
|
||||
"supportPlatforms": "Supported Platforms",
|
||||
"authorWebsite": "Author Website",
|
||||
"authorWebsite": "Author Website",
|
||||
"repository": "Repository"
|
||||
}
|
||||
},
|
||||
@@ -158,6 +152,8 @@
|
||||
"sourceExists": "This source already exists",
|
||||
"installPlugin": "Install Plugin",
|
||||
"randomPlugins": "🎲 Random Plugins",
|
||||
"showRandomPlugins": "Show Random Plugins",
|
||||
"hideRandomPlugins": "Hide Random Plugins",
|
||||
"sourceSafetyWarning": "Even with the default source, plugin stability and security cannot be fully guaranteed. Please verify carefully before use."
|
||||
},
|
||||
"sort": {
|
||||
@@ -200,10 +196,7 @@
|
||||
"title": "Install Extension",
|
||||
"fromFile": "Install from File",
|
||||
"fromUrl": "Install from URL",
|
||||
"supportPlatformsCount": "Supports {count} Platforms",
|
||||
"sectionTitle": "Install",
|
||||
"downloadSource": "The plugin package will be installed from:",
|
||||
"githubSecurityWarning": "AstrBot cannot guarantee the safety of plugins downloaded from GitHub. Install only if you trust the plugin source."
|
||||
"supportPlatformsCount": "Supports {count} Platforms"
|
||||
},
|
||||
"danger_warning": {
|
||||
"title": "Dangerous Plugin Warning",
|
||||
|
||||
@@ -63,8 +63,7 @@
|
||||
"system": "Системный",
|
||||
"loading": "Загрузка...",
|
||||
"installed": "Установлен",
|
||||
"unknown": "Неизвестно",
|
||||
"incompatible": "Несовместимо"
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
"tooltips": {
|
||||
"enable": "Включить",
|
||||
@@ -110,13 +109,8 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Информация",
|
||||
"version": "Версия",
|
||||
"author": "Автор",
|
||||
"category": "Категория",
|
||||
"stars": "Звезды",
|
||||
"tags": "Теги",
|
||||
"astrbotVersion": "Требуемая версия AstrBot",
|
||||
"supportPlatforms": "Поддерживаемые платформы",
|
||||
"authorWebsite": "Сайт автора",
|
||||
"repository": "Репозиторий"
|
||||
}
|
||||
@@ -157,6 +151,8 @@
|
||||
"sourceExists": "Этот источник уже есть в списке",
|
||||
"installPlugin": "Установить плагин",
|
||||
"randomPlugins": "🎲 Случайные плагины",
|
||||
"showRandomPlugins": "Показать случайные",
|
||||
"hideRandomPlugins": "Скрыть случайные",
|
||||
"sourceSafetyWarning": "Даже при использовании источников по умолчанию мы не можем гарантировать 100% безопасность и стабильность сторонних плагинов. Пожалуйста, будьте внимательны."
|
||||
},
|
||||
"sort": {
|
||||
@@ -199,10 +195,7 @@
|
||||
"title": "Установка плагина",
|
||||
"fromFile": "Из файла",
|
||||
"fromUrl": "По ссылке",
|
||||
"supportPlatformsCount": "Поддерживает платформ: {count}",
|
||||
"sectionTitle": "Установка",
|
||||
"downloadSource": "Пакет плагина будет установлен из:",
|
||||
"githubSecurityWarning": "AstrBot не может гарантировать безопасность плагинов, загруженных с GitHub. Устанавливайте только если доверяете источнику плагина."
|
||||
"supportPlatformsCount": "Поддерживает платформ: {count}"
|
||||
},
|
||||
"danger_warning": {
|
||||
"title": "Внимание!",
|
||||
|
||||
@@ -61,10 +61,9 @@
|
||||
"enabled": "启用",
|
||||
"disabled": "禁用",
|
||||
"system": "系统",
|
||||
"loading": "加载中...",
|
||||
"installed": "已安装",
|
||||
"unknown": "未知",
|
||||
"incompatible": "不兼容"
|
||||
"loading": "加载中...",
|
||||
"installed": "已安装",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"tooltips": {
|
||||
"enable": "点击启用",
|
||||
@@ -110,14 +109,9 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "信息",
|
||||
"version": "版本",
|
||||
"author": "作者",
|
||||
"category": "类别",
|
||||
"stars": "Star数",
|
||||
"tags": "标签",
|
||||
"astrbotVersion": "AstrBot 版本要求",
|
||||
"supportPlatforms": "支持平台",
|
||||
"authorWebsite": "作者网站",
|
||||
"authorWebsite": "作者网站",
|
||||
"repository": "仓库"
|
||||
}
|
||||
},
|
||||
@@ -158,6 +152,8 @@
|
||||
"sourceExists": "该插件源已存在",
|
||||
"installPlugin": "安装插件",
|
||||
"randomPlugins": "🎲 随机插件",
|
||||
"showRandomPlugins": "显示随机插件",
|
||||
"hideRandomPlugins": "隐藏随机插件",
|
||||
"sourceSafetyWarning": "即使是默认插件源,我们也不能完全保证插件的稳定性和安全性,使用前请谨慎核查。"
|
||||
},
|
||||
"sort": {
|
||||
@@ -200,10 +196,7 @@
|
||||
"title": "安装插件",
|
||||
"fromFile": "从文件安装",
|
||||
"fromUrl": "从链接安装",
|
||||
"supportPlatformsCount": "支持 {count} 个平台",
|
||||
"sectionTitle": "安装",
|
||||
"downloadSource": "将从以下位置安装插件包体:",
|
||||
"githubSecurityWarning": "AstrBot 不能保证从 GitHub 上下载的插件的安全性。请确认你信任该插件来源后再安装。"
|
||||
"supportPlatformsCount": "支持 {count} 个平台"
|
||||
},
|
||||
"danger_warning": {
|
||||
"title": "警告",
|
||||
|
||||
@@ -163,10 +163,8 @@ export const useCommonStore = defineStore("common", {
|
||||
const pluginData = res.data.data[key];
|
||||
|
||||
data.push({
|
||||
...pluginData,
|
||||
"name": pluginData.name || key, // 优先使用插件数据中的name字段,否则使用键名
|
||||
"desc": pluginData.desc,
|
||||
"short_desc": pluginData?.short_desc ? pluginData.short_desc : "",
|
||||
"author": pluginData.author,
|
||||
"repo": pluginData.repo,
|
||||
"installed": false,
|
||||
@@ -177,9 +175,7 @@ export const useCommonStore = defineStore("common", {
|
||||
"pinned": pluginData?.pinned ? pluginData.pinned : false,
|
||||
"stars": pluginData?.stars ? pluginData.stars : 0,
|
||||
"updated_at": pluginData?.updated_at ? pluginData.updated_at : "",
|
||||
"download_url": pluginData?.download_url ? pluginData.download_url : "",
|
||||
"display_name": pluginData?.display_name ? pluginData.display_name : "",
|
||||
"i18n": pluginData?.i18n && typeof pluginData.i18n === 'object' ? pluginData.i18n : {},
|
||||
"astrbot_version": pluginData?.astrbot_version ? pluginData.astrbot_version : "",
|
||||
"category": pluginData?.category ? pluginData.category : "",
|
||||
"support_platforms": Array.isArray(pluginData?.support_platforms)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
|
||||
function getLocaleData(i18n, locale) {
|
||||
if (!i18n || typeof i18n !== 'object' || !locale) return null
|
||||
return i18n[locale] || null
|
||||
}
|
||||
|
||||
function getByPath(source, key) {
|
||||
if (!source || typeof source !== 'object' || !key) return undefined
|
||||
|
||||
const parts = key.split('.')
|
||||
let current = source
|
||||
for (const part of parts) {
|
||||
if (!current || typeof current !== 'object' || !(part in current)) {
|
||||
return undefined
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
export function resolvePluginI18n(i18n, locale, key, fallback = '') {
|
||||
const localeData = getLocaleData(i18n, locale)
|
||||
const value = getByPath(localeData, key)
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
export function usePluginI18n() {
|
||||
const { locale } = useI18n()
|
||||
|
||||
const resolve = (i18n, key, fallback = '') => {
|
||||
return resolvePluginI18n(i18n, locale.value, key, fallback)
|
||||
}
|
||||
|
||||
const pluginName = (plugin) => {
|
||||
const fallback = plugin?.display_name?.length ? plugin.display_name : plugin?.name
|
||||
return resolve(plugin?.i18n, 'metadata.display_name', fallback || '')
|
||||
}
|
||||
|
||||
const pluginDesc = (plugin, fallback = '') => {
|
||||
return resolve(
|
||||
plugin?.i18n,
|
||||
'metadata.desc',
|
||||
fallback || plugin?.desc || plugin?.description || '',
|
||||
)
|
||||
}
|
||||
|
||||
const pluginShortDesc = (plugin, fallback = '') => {
|
||||
return resolve(
|
||||
plugin?.i18n,
|
||||
'metadata.short_desc',
|
||||
fallback || plugin?.short_desc || plugin?.desc || plugin?.description || '',
|
||||
)
|
||||
}
|
||||
|
||||
const configText = (i18n, path, attr, fallback = '') => {
|
||||
const key = path ? `config.${path}.${attr}` : `config.${attr}`
|
||||
return resolve(i18n, key, fallback)
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
resolve,
|
||||
pluginName,
|
||||
pluginDesc,
|
||||
pluginShortDesc,
|
||||
configText,
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,6 @@ export const getPluginSearchFields = (plugin) => {
|
||||
plugin?.name,
|
||||
plugin?.trimmedName,
|
||||
plugin?.display_name,
|
||||
plugin?.short_desc,
|
||||
plugin?.desc,
|
||||
plugin?.author,
|
||||
plugin?.repo,
|
||||
|
||||
@@ -12,11 +12,8 @@ import MarketPluginsTab from "./extension/MarketPluginsTab.vue";
|
||||
import PluginDetailPage from "./extension/PluginDetailPage.vue";
|
||||
import { useExtensionPage } from "./extension/useExtensionPage";
|
||||
import { computed } from "vue";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
import { usePluginI18n } from "@/utils/pluginI18n";
|
||||
|
||||
const pageState = useExtensionPage();
|
||||
const { pluginName, pluginDesc } = usePluginI18n();
|
||||
|
||||
const {
|
||||
commonStore,
|
||||
@@ -127,8 +124,6 @@ const {
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
openInstallDialog,
|
||||
closeInstallDialog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
@@ -152,9 +147,6 @@ const {
|
||||
getPlatformDisplayList,
|
||||
resolveSelectedInstallPlugin,
|
||||
selectedInstallPlugin,
|
||||
selectedInstallDownloadUrl,
|
||||
selectedInstallSourceUrl,
|
||||
installUsesGithubSource,
|
||||
checkInstallCompatibility,
|
||||
refreshPluginMarket,
|
||||
handleLocaleChange,
|
||||
@@ -166,8 +158,6 @@ const selectedPluginId = computed(() => {
|
||||
return Array.isArray(pluginId) ? pluginId[0] : pluginId || "";
|
||||
});
|
||||
|
||||
const selectedDetailTab = computed(() => extractTabFromHash(route.hash) || "installed");
|
||||
|
||||
const selectedInstalledPlugin = computed(() => {
|
||||
if (!selectedPluginId.value) return null;
|
||||
const data = Array.isArray(extension_data?.data) ? extension_data.data : [];
|
||||
@@ -175,59 +165,23 @@ const selectedInstalledPlugin = computed(() => {
|
||||
});
|
||||
|
||||
const selectedMarketPlugin = computed(() => {
|
||||
const plugin = selectedInstalledPlugin.value;
|
||||
if (!plugin) return null;
|
||||
const market = Array.isArray(pluginMarketData.value) ? pluginMarketData.value : [];
|
||||
const installedPlugin = selectedInstalledPlugin.value;
|
||||
const repo = installedPlugin?.repo?.toLowerCase();
|
||||
const repo = plugin.repo?.toLowerCase();
|
||||
return (
|
||||
market.find((item) => item.name === selectedPluginId.value) ||
|
||||
market.find((item) => repo && item.repo?.toLowerCase() === repo) ||
|
||||
market.find((item) => item.name === plugin.name) ||
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
const selectedDetailPlugin = computed(() => {
|
||||
if (selectedDetailTab.value === "market" && selectedMarketPlugin.value) {
|
||||
return selectedMarketPlugin.value;
|
||||
}
|
||||
return selectedInstalledPlugin.value || selectedMarketPlugin.value;
|
||||
});
|
||||
|
||||
const installDialogPluginName = computed(() =>
|
||||
selectedInstallPlugin.value ? pluginName(selectedInstallPlugin.value) : "",
|
||||
);
|
||||
|
||||
const installDialogPluginDesc = computed(() =>
|
||||
String(
|
||||
selectedInstallPlugin.value
|
||||
? pluginDesc(
|
||||
selectedInstallPlugin.value,
|
||||
selectedInstallPlugin.value.desc ||
|
||||
selectedInstallPlugin.value.description ||
|
||||
"",
|
||||
)
|
||||
: "",
|
||||
).trim(),
|
||||
);
|
||||
|
||||
const installDialogPluginAuthor = computed(() => {
|
||||
const author = selectedInstallPlugin.value?.author;
|
||||
if (Array.isArray(author)) return author.join(", ");
|
||||
if (author && typeof author === "object") return author.name || "";
|
||||
return typeof author === "string" ? author.trim() : "";
|
||||
});
|
||||
|
||||
const installDialogPluginLogo = computed(() => {
|
||||
const logo = selectedInstallPlugin.value?.logo;
|
||||
return typeof logo === "string" && logo.trim() ? logo : defaultPluginIcon;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PluginDetailPage
|
||||
v-if="selectedPluginId && selectedDetailPlugin"
|
||||
:plugin="selectedDetailPlugin"
|
||||
v-if="selectedPluginId && selectedInstalledPlugin"
|
||||
:plugin="selectedInstalledPlugin"
|
||||
:market-plugin="selectedMarketPlugin"
|
||||
:source-tab="selectedDetailTab"
|
||||
:state="pageState"
|
||||
/>
|
||||
|
||||
@@ -241,10 +195,10 @@ const installDialogPluginLogo = computed(() => {
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
@click="router.push({ name: 'Extensions', hash: `#${selectedDetailTab}` })"
|
||||
@click="router.push({ name: 'Extensions', hash: '#installed' })"
|
||||
/>
|
||||
<h2 class="text-h3 mb-0 ml-2">
|
||||
{{ selectedDetailTab === "market" ? tm("tabs.market") : tm("titles.installedAstrBotPlugins") }}
|
||||
{{ tm("titles.installedAstrBotPlugins") }}
|
||||
<v-icon icon="mdi-chevron-right" size="24" class="mx-1" />
|
||||
{{ selectedPluginId }}
|
||||
</h2>
|
||||
@@ -377,7 +331,6 @@ const installDialogPluginLogo = computed(() => {
|
||||
:iterable="extension_config.config"
|
||||
:metadataKey="curr_namespace"
|
||||
:pluginName="curr_namespace"
|
||||
:pluginI18n="extension_config.i18n"
|
||||
/>
|
||||
<p v-else>{{ tm("dialogs.config.noConfig") }}</p>
|
||||
</div>
|
||||
@@ -639,118 +592,26 @@ const installDialogPluginLogo = computed(() => {
|
||||
<div
|
||||
class="v-card v-card--density-default rounded-lg v-card--variant-elevated"
|
||||
>
|
||||
<div class="v-card__loader">
|
||||
<v-progress-linear
|
||||
:indeterminate="loading_"
|
||||
color="primary"
|
||||
height="2"
|
||||
:active="loading_"
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
|
||||
<v-card-title class="text-h3 pa-4 pb-0 pl-6">
|
||||
{{ tm("dialogs.install.title") }}
|
||||
</v-card-title>
|
||||
|
||||
<div class="v-card-text">
|
||||
<div v-if="selectedMarketInstallPlugin" class="market-install-confirm">
|
||||
<div class="market-install-confirm__header">
|
||||
<img
|
||||
:src="installDialogPluginLogo"
|
||||
:alt="installDialogPluginName"
|
||||
class="market-install-confirm__logo"
|
||||
/>
|
||||
<div class="market-install-confirm__meta">
|
||||
<div class="market-install-confirm__name">
|
||||
{{ installDialogPluginName }}
|
||||
</div>
|
||||
<div
|
||||
v-if="installDialogPluginAuthor"
|
||||
class="market-install-confirm__author"
|
||||
>
|
||||
{{ tm("detail.info.author") }}: {{ installDialogPluginAuthor }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div v-if="installDialogPluginDesc" class="market-install-confirm__section">
|
||||
<div class="market-install-confirm__section-title">
|
||||
{{ tm("table.headers.description") }}
|
||||
</div>
|
||||
<div class="market-install-confirm__desc">
|
||||
{{ installDialogPluginDesc }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedInstallPlugin" class="mt-4">
|
||||
<v-chip
|
||||
v-if="selectedInstallPlugin.astrbot_version"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mr-2 mb-2"
|
||||
>
|
||||
{{ tm("card.status.astrbotVersion") }}:
|
||||
{{ selectedInstallPlugin.astrbot_version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="normalizePlatformList(selectedInstallPlugin.support_platforms).length"
|
||||
size="small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ tm("card.status.supportPlatform") }}:
|
||||
{{
|
||||
getPlatformDisplayList(selectedInstallPlugin.support_platforms).join(
|
||||
", ",
|
||||
)
|
||||
}}
|
||||
</v-chip>
|
||||
<v-alert
|
||||
v-if="
|
||||
selectedInstallPlugin.astrbot_version &&
|
||||
installCompat.checked &&
|
||||
!installCompat.compatible
|
||||
"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="market-install-alert mt-2 mb-3"
|
||||
>
|
||||
{{ installCompat.message }}
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedInstallSourceUrl"
|
||||
class="market-install-confirm__section-title mt-4"
|
||||
>
|
||||
{{ tm("dialogs.install.sectionTitle") }}
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedInstallSourceUrl"
|
||||
class="market-install-source text-caption text-medium-emphasis mb-3"
|
||||
>
|
||||
<div>{{ tm("dialogs.install.downloadSource") }}</div>
|
||||
<div class="market-install-source__url">
|
||||
{{ selectedInstallSourceUrl }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="installUsesGithubSource"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="market-install-alert mt-4 mb-4"
|
||||
>
|
||||
{{ tm("dialogs.install.githubSecurityWarning") }}
|
||||
</v-alert>
|
||||
|
||||
<ProxySelector v-if="!selectedInstallDownloadUrl" class="mt-4" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<v-tabs v-model="uploadTab" color="primary">
|
||||
<v-tabs v-model="uploadTab" color="primary">
|
||||
<v-tab value="file">{{ tm("dialogs.install.fromFile") }}</v-tab>
|
||||
<v-tab value="url">{{ tm("dialogs.install.fromUrl") }}</v-tab>
|
||||
</v-tabs>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="uploadTab" class="mt-4">
|
||||
<v-window v-model="uploadTab" class="mt-4">
|
||||
<v-window-item value="file">
|
||||
<div class="d-flex flex-column align-center justify-center pa-4">
|
||||
<v-file-input
|
||||
@@ -841,57 +702,24 @@ const installDialogPluginLogo = computed(() => {
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="market-install-alert mt-2 mb-3"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ installCompat.message }}
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedInstallSourceUrl"
|
||||
class="market-install-confirm__section-title mt-4"
|
||||
>
|
||||
{{ tm("dialogs.install.sectionTitle") }}
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedInstallSourceUrl"
|
||||
class="market-install-source text-caption text-medium-emphasis mb-3"
|
||||
>
|
||||
<div>{{ tm("dialogs.install.downloadSource") }}</div>
|
||||
<div class="market-install-source__url">
|
||||
{{ selectedInstallSourceUrl }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="installUsesGithubSource"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="market-install-alert mb-4"
|
||||
>
|
||||
{{ tm("dialogs.install.githubSecurityWarning") }}
|
||||
</v-alert>
|
||||
|
||||
<ProxySelector v-if="!selectedInstallDownloadUrl"></ProxySelector>
|
||||
<ProxySelector></ProxySelector>
|
||||
</div>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</template>
|
||||
</v-window>
|
||||
</div>
|
||||
|
||||
<div class="v-card-actions">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="closeInstallDialog">{{
|
||||
<v-btn color="grey" variant="text" @click="dialog = false">{{
|
||||
tm("buttons.cancel")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
:loading="loading_"
|
||||
:disabled="loading_"
|
||||
@click="newExtension"
|
||||
>{{
|
||||
<v-btn color="primary" variant="text" @click="newExtension">{{
|
||||
tm("buttons.install")
|
||||
}}</v-btn>
|
||||
</div>
|
||||
@@ -1100,64 +928,6 @@ const installDialogPluginLogo = computed(() => {
|
||||
transform: translateY(-4px) scale(1.05);
|
||||
box-shadow: 0 12px 20px rgba(var(--v-theme-primary), 0.4);
|
||||
}
|
||||
|
||||
.market-install-confirm {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.market-install-confirm__header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.market-install-confirm__logo {
|
||||
border-radius: 14px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.market-install-confirm__meta {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-install-confirm__name {
|
||||
color: rgba(var(--v-theme-on-surface), 0.92);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.market-install-confirm__author,
|
||||
.market-install-confirm__desc {
|
||||
color: rgba(var(--v-theme-on-surface), 0.64);
|
||||
line-height: 1.55;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.market-install-confirm__section-title {
|
||||
color: rgba(var(--v-theme-on-surface), 0.92);
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.market-install-alert {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.market-install-source {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-install-source__url {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<v-row v-else>
|
||||
<v-col v-for="(platform, index) in config_data.platform || []" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card :item="platform" title-field="id" enabled-field="enable"
|
||||
variant="outlined"
|
||||
:bglogo="getPlatformIcon(platform.type || platform.id)" @toggle-enabled="platformStatusChange"
|
||||
@delete="deletePlatform" @edit="editPlatform">
|
||||
<template #item-details="{ item }">
|
||||
|
||||
@@ -116,7 +116,6 @@ const {
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
openInstallDialog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
@@ -366,7 +365,7 @@ const togglePinnedExtension = (extension) => {
|
||||
z-index: 10000;
|
||||
border-radius: 16px;
|
||||
"
|
||||
@click="openInstallDialog"
|
||||
@click="dialog = true"
|
||||
>
|
||||
<span class="v-btn__overlay"></span>
|
||||
<span class="v-btn__underlay"></span>
|
||||
|
||||
@@ -80,6 +80,7 @@ const {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
showRandomPlugins,
|
||||
marketCategoryFilter,
|
||||
marketCategoryItems,
|
||||
normalizeStr,
|
||||
@@ -95,6 +96,7 @@ const {
|
||||
randomPlugins,
|
||||
shufflePlugins,
|
||||
refreshRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
displayItemsPerPage,
|
||||
totalPages,
|
||||
paginatedPlugins,
|
||||
@@ -123,7 +125,6 @@ const {
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
openInstallDialog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
@@ -174,15 +175,6 @@ const marketCategorySelectItems = computed(() =>
|
||||
value: item.value,
|
||||
})),
|
||||
);
|
||||
|
||||
const openMarketPluginDetail = (plugin) => {
|
||||
if (!plugin?.name) return;
|
||||
router.push({
|
||||
name: "ExtensionDetails",
|
||||
params: { pluginId: plugin.name },
|
||||
hash: "#market",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -213,6 +205,20 @@ const openMarketPluginDetail = (plugin) => {
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
rounded="md"
|
||||
class="text-none px-2"
|
||||
:prepend-icon="showRandomPlugins ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click="toggleRandomPluginsVisibility"
|
||||
>
|
||||
{{
|
||||
showRandomPlugins
|
||||
? tm("market.hideRandomPlugins")
|
||||
: tm("market.showRandomPlugins")
|
||||
}}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
@@ -257,7 +263,7 @@ const openMarketPluginDetail = (plugin) => {
|
||||
z-index: 10000;
|
||||
border-radius: 16px;
|
||||
"
|
||||
@click="openInstallDialog"
|
||||
@click="dialog = true"
|
||||
>
|
||||
<span class="v-btn__overlay"></span>
|
||||
<span class="v-btn__underlay"></span>
|
||||
@@ -341,7 +347,6 @@ const openMarketPluginDetail = (plugin) => {
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
@open="openMarketPluginDetail"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -359,7 +364,7 @@ const openMarketPluginDetail = (plugin) => {
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<div v-if="randomPlugins.length > 0">
|
||||
<div v-if="showRandomPlugins">
|
||||
<div
|
||||
class="d-flex align-center mb-2 mt-4"
|
||||
style="justify-content: space-between; flex-wrap: wrap; gap: 8px"
|
||||
@@ -392,7 +397,6 @@ const openMarketPluginDetail = (plugin) => {
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
@open="openMarketPluginDetail"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -4,8 +4,6 @@ import axios from "axios";
|
||||
import DOMPurify from "dompurify";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
import { usePluginI18n } from "@/utils/pluginI18n";
|
||||
import PluginPlatformChip from "@/components/shared/PluginPlatformChip.vue";
|
||||
|
||||
const props = defineProps({
|
||||
plugin: {
|
||||
@@ -16,10 +14,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
sourceTab: {
|
||||
type: String,
|
||||
default: "installed",
|
||||
},
|
||||
state: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -27,7 +21,6 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const { tm, router } = props.state;
|
||||
const { pluginName, pluginDesc: resolvePluginDesc } = usePluginI18n();
|
||||
|
||||
const markdown = new MarkdownIt({
|
||||
html: true,
|
||||
@@ -82,13 +75,8 @@ const logoLoadFailed = ref(false);
|
||||
const detailPageRef = ref(null);
|
||||
const isHeaderStuck = ref(false);
|
||||
|
||||
const displayName = computed(() => pluginName(props.plugin));
|
||||
const detailSourceTab = computed(() =>
|
||||
props.sourceTab === "market" ? "market" : "installed",
|
||||
);
|
||||
const isMarketDetail = computed(() => detailSourceTab.value === "market");
|
||||
const detailParentTitle = computed(() =>
|
||||
isMarketDetail.value ? tm("tabs.market") : tm("titles.installedAstrBotPlugins"),
|
||||
const displayName = computed(() =>
|
||||
props.plugin.display_name?.length ? props.plugin.display_name : props.plugin.name,
|
||||
);
|
||||
|
||||
const pluginDesc = computed(() => {
|
||||
@@ -98,7 +86,7 @@ const pluginDesc = computed(() => {
|
||||
props.marketPlugin?.desc ||
|
||||
props.marketPlugin?.description ||
|
||||
"";
|
||||
return String(resolvePluginDesc(props.plugin, desc) || "").trim();
|
||||
return String(desc || "").trim();
|
||||
});
|
||||
|
||||
const logoSrc = computed(() => {
|
||||
@@ -155,68 +143,10 @@ const authorWebsite = computed(() => {
|
||||
|
||||
const repoUrl = computed(() => props.plugin.repo || props.marketPlugin?.repo || "");
|
||||
|
||||
const firstPresentValue = (...values) =>
|
||||
values.find(
|
||||
(value) =>
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
value !== "" &&
|
||||
(!Array.isArray(value) || value.length > 0),
|
||||
);
|
||||
|
||||
const versionDisplay = computed(() =>
|
||||
String(firstPresentValue(props.plugin.version, props.marketPlugin?.version) || "").trim(),
|
||||
);
|
||||
|
||||
const starsDisplay = computed(() => {
|
||||
const value = firstPresentValue(props.plugin.stars, props.marketPlugin?.stars);
|
||||
return value === undefined ? "" : String(value);
|
||||
});
|
||||
|
||||
const tagsDisplay = computed(() => {
|
||||
const tags = firstPresentValue(props.plugin.tags, props.marketPlugin?.tags);
|
||||
if (!Array.isArray(tags)) return [];
|
||||
return tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0);
|
||||
});
|
||||
|
||||
const astrbotVersionDisplay = computed(() =>
|
||||
String(
|
||||
firstPresentValue(props.plugin.astrbot_version, props.marketPlugin?.astrbot_version) || "",
|
||||
).trim(),
|
||||
);
|
||||
|
||||
const supportPlatformsDisplay = computed(() => {
|
||||
const platforms = firstPresentValue(
|
||||
props.plugin.support_platforms,
|
||||
props.marketPlugin?.support_platforms,
|
||||
);
|
||||
if (!Array.isArray(platforms)) return [];
|
||||
return platforms.filter((platform) => typeof platform === "string");
|
||||
});
|
||||
|
||||
const infoRows = computed(() => {
|
||||
const rows = [
|
||||
{ label: tm("detail.info.version"), value: versionDisplay.value, optional: true },
|
||||
{ label: tm("detail.info.author"), value: authorDisplay.value },
|
||||
{ label: tm("detail.info.category"), value: categoryDisplay.value, optional: true },
|
||||
{ label: tm("detail.info.stars"), value: starsDisplay.value, optional: true },
|
||||
{
|
||||
label: tm("detail.info.tags"),
|
||||
value: tagsDisplay.value,
|
||||
kind: "tags",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
label: tm("detail.info.astrbotVersion"),
|
||||
value: astrbotVersionDisplay.value,
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
label: tm("detail.info.supportPlatforms"),
|
||||
value: supportPlatformsDisplay.value,
|
||||
kind: "platforms",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
label: tm("detail.info.authorWebsite"),
|
||||
value: authorWebsite.value,
|
||||
@@ -231,38 +161,12 @@ const infoRows = computed(() => {
|
||||
},
|
||||
];
|
||||
|
||||
return rows.filter(
|
||||
(row) => !row.optional || (Array.isArray(row.value) ? row.value.length > 0 : row.value),
|
||||
);
|
||||
return rows.filter((row) => !row.optional || row.value);
|
||||
});
|
||||
|
||||
const normalizeHandlerList = (source) => {
|
||||
if (!source || typeof source !== "object") return [];
|
||||
if (Array.isArray(source.handlers)) {
|
||||
return source.handlers.filter((handler) => handler && typeof handler === "object");
|
||||
}
|
||||
if (Array.isArray(source.command_handlers)) {
|
||||
return source.command_handlers.filter(
|
||||
(handler) => handler && typeof handler === "object",
|
||||
);
|
||||
}
|
||||
if (Array.isArray(source.commands)) {
|
||||
return source.commands
|
||||
.filter((command) => command && (typeof command === "string" || typeof command === "object"))
|
||||
.map((command) =>
|
||||
typeof command === "string"
|
||||
? { cmd: command, type: "指令" }
|
||||
: { type: command.type || "指令", ...command },
|
||||
);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const handlers = computed(() => {
|
||||
const pluginHandlers = normalizeHandlerList(props.plugin);
|
||||
if (pluginHandlers.length > 0) return pluginHandlers;
|
||||
return normalizeHandlerList(props.marketPlugin);
|
||||
});
|
||||
const handlers = computed(() =>
|
||||
Array.isArray(props.plugin.handlers) ? props.plugin.handlers : [],
|
||||
);
|
||||
|
||||
const handlerGroupOrder = [
|
||||
"commands",
|
||||
@@ -414,7 +318,7 @@ const openExternal = (url) => {
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
router.push({ name: "Extensions", hash: `#${detailSourceTab.value}` });
|
||||
router.push({ name: "Extensions", hash: "#installed" });
|
||||
};
|
||||
|
||||
const renderMarkdown = (source) => {
|
||||
@@ -454,28 +358,6 @@ const fetchReadme = async () => {
|
||||
readmeEmpty.value = false;
|
||||
renderedReadme.value = "";
|
||||
|
||||
if (isMarketDetail.value) {
|
||||
readmeLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const inlineReadme =
|
||||
props.plugin.readme ||
|
||||
props.plugin.README ||
|
||||
props.plugin.readme_content ||
|
||||
props.plugin.docs ||
|
||||
props.marketPlugin?.readme ||
|
||||
props.marketPlugin?.README ||
|
||||
props.marketPlugin?.readme_content ||
|
||||
props.marketPlugin?.docs ||
|
||||
"";
|
||||
|
||||
if (inlineReadme) {
|
||||
renderedReadme.value = renderMarkdown(inlineReadme);
|
||||
readmeLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.get("/api/plugin/readme", {
|
||||
params: { name: props.plugin.name },
|
||||
@@ -500,8 +382,6 @@ const fetchReadme = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const showDocsSection = computed(() => !isMarketDetail.value);
|
||||
|
||||
watch(
|
||||
() => props.plugin?.name,
|
||||
() => {
|
||||
@@ -536,7 +416,7 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<h2 class="detail-title">
|
||||
<button class="detail-title__parent" type="button" @click="goBack">
|
||||
{{ detailParentTitle }}
|
||||
{{ tm("titles.installedAstrBotPlugins") }}
|
||||
</button>
|
||||
<v-icon icon="mdi-chevron-right" size="24" class="mx-1" />
|
||||
<span class="detail-title__current">{{ displayName }}</span>
|
||||
@@ -558,9 +438,9 @@ onBeforeUnmount(() => {
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<section v-if="groupedHandlerSections.length" class="detail-section">
|
||||
<section class="detail-section">
|
||||
<h3 class="detail-section__title">{{ tm("detail.contents") }}</h3>
|
||||
<div class="handler-groups">
|
||||
<div v-if="groupedHandlerSections.length" class="handler-groups">
|
||||
<div
|
||||
v-for="group in groupedHandlerSections"
|
||||
:key="group.key"
|
||||
@@ -659,6 +539,11 @@ onBeforeUnmount(() => {
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
<v-card v-else class="rounded-lg handler-card" variant="outlined">
|
||||
<v-card-text class="pa-4 text-medium-emphasis">
|
||||
{{ tm("detail.noContents") }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</section>
|
||||
|
||||
<section class="detail-section">
|
||||
@@ -679,21 +564,6 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ row.actionText }}
|
||||
</v-btn>
|
||||
<div v-else-if="row.kind === 'tags'" class="detail-tags">
|
||||
<v-chip
|
||||
v-for="tag in row.value"
|
||||
:key="tag"
|
||||
:color="tag === 'danger' ? 'error' : 'primary'"
|
||||
label
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-else-if="row.kind === 'platforms'" class="detail-tags">
|
||||
<PluginPlatformChip :platforms="row.value" />
|
||||
</div>
|
||||
<button
|
||||
v-else-if="row.href"
|
||||
class="detail-link"
|
||||
@@ -711,7 +581,7 @@ onBeforeUnmount(() => {
|
||||
</v-card>
|
||||
</section>
|
||||
|
||||
<section v-if="showDocsSection" class="detail-section">
|
||||
<section class="detail-section">
|
||||
<h3 class="detail-section__title">{{ tm("detail.docsTitle") }}</h3>
|
||||
<v-card class="rounded-lg docs-card" variant="outlined">
|
||||
<v-card-text>
|
||||
@@ -957,12 +827,6 @@ onBeforeUnmount(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.docs-card {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
color: rgba(var(--v-theme-on-surface), 0.9);
|
||||
|
||||
@@ -17,6 +17,36 @@ import {
|
||||
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const useRandomPluginsDisplay = ({ activeTab, marketSearch, currentPage }) => {
|
||||
const showRandomPlugins = ref(true);
|
||||
|
||||
const toggleRandomPluginsVisibility = () => {
|
||||
showRandomPlugins.value = !showRandomPlugins.value;
|
||||
};
|
||||
|
||||
const collapseRandomPlugins = () => {
|
||||
showRandomPlugins.value = false;
|
||||
};
|
||||
|
||||
watch(marketSearch, () => {
|
||||
if (activeTab.value === "market") {
|
||||
collapseRandomPlugins();
|
||||
}
|
||||
});
|
||||
|
||||
watch(currentPage, (newPage, oldPage) => {
|
||||
if (newPage === oldPage) return;
|
||||
if (activeTab.value !== "market") return;
|
||||
collapseRandomPlugins();
|
||||
});
|
||||
|
||||
return {
|
||||
showRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
};
|
||||
};
|
||||
|
||||
const buildFailedPluginItems = (raw) => {
|
||||
return Object.entries(raw || {}).map(([dirName, info]) => {
|
||||
const detail = info && typeof info === "object" ? info : {};
|
||||
@@ -40,7 +70,6 @@ export const useExtensionPage = () => {
|
||||
const { tm } = useModuleI18n("features/extension");
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const marketCompatibilityCache = new Map();
|
||||
|
||||
const getSelectedGitHubProxy = () => {
|
||||
if (typeof window === "undefined" || !window.localStorage) return "";
|
||||
@@ -98,7 +127,6 @@ export const useExtensionPage = () => {
|
||||
const extension_config = reactive({
|
||||
metadata: {},
|
||||
config: {},
|
||||
i18n: {},
|
||||
});
|
||||
const pluginMarketData = ref([]);
|
||||
const loadingDialog = reactive({
|
||||
@@ -110,7 +138,6 @@ export const useExtensionPage = () => {
|
||||
const showPluginInfoDialog = ref(false);
|
||||
const selectedPlugin = ref({});
|
||||
const curr_namespace = ref("");
|
||||
const currentConfigPlugin = ref("");
|
||||
const updatingAll = ref(false);
|
||||
|
||||
const readmeDialog = reactive({
|
||||
@@ -188,6 +215,15 @@ export const useExtensionPage = () => {
|
||||
const sortOrder = ref("desc"); // desc (降序) or asc (升序)
|
||||
const randomPluginNames = ref([]);
|
||||
const marketCategoryFilter = ref("all");
|
||||
const {
|
||||
showRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
} = useRandomPluginsDisplay({
|
||||
activeTab,
|
||||
marketSearch,
|
||||
currentPage,
|
||||
});
|
||||
|
||||
// 插件市场拼音搜索
|
||||
|
||||
@@ -818,7 +854,6 @@ export const useExtensionPage = () => {
|
||||
|
||||
const openExtensionConfig = async (extension_name) => {
|
||||
curr_namespace.value = extension_name;
|
||||
currentConfigPlugin.value = extension_name;
|
||||
configDialog.value = true;
|
||||
try {
|
||||
const res = await axios.get(
|
||||
@@ -826,7 +861,6 @@ export const useExtensionPage = () => {
|
||||
);
|
||||
extension_config.metadata = res.data.data.metadata;
|
||||
extension_config.config = res.data.data.config;
|
||||
extension_config.i18n = res.data.data.i18n || {};
|
||||
} catch (err) {
|
||||
toast(err, "error");
|
||||
}
|
||||
@@ -844,10 +878,8 @@ export const useExtensionPage = () => {
|
||||
toast(res.data.message, "error");
|
||||
}
|
||||
configDialog.value = false;
|
||||
currentConfigPlugin.value = "";
|
||||
extension_config.metadata = {};
|
||||
extension_config.config = {};
|
||||
extension_config.i18n = {};
|
||||
getExtensions();
|
||||
} catch (err) {
|
||||
toast(err, "error");
|
||||
@@ -885,52 +917,6 @@ export const useExtensionPage = () => {
|
||||
changelogDialog.repoUrl = plugin.repo;
|
||||
changelogDialog.show = true;
|
||||
};
|
||||
|
||||
const resetInstallDialogState = () => {
|
||||
selectedMarketInstallPlugin.value = null;
|
||||
extension_url.value = "";
|
||||
upload_file.value = null;
|
||||
uploadTab.value = "file";
|
||||
installCompat.checked = false;
|
||||
installCompat.compatible = true;
|
||||
installCompat.message = "";
|
||||
};
|
||||
|
||||
const openInstallDialog = () => {
|
||||
resetInstallDialogState();
|
||||
dialog.value = true;
|
||||
};
|
||||
|
||||
const closeInstallDialog = () => {
|
||||
dialog.value = false;
|
||||
resetInstallDialogState();
|
||||
};
|
||||
|
||||
const normalizeInstallUrl = (value) =>
|
||||
String(value || "").trim().replace(/\/+$/, "");
|
||||
|
||||
const isGithubRepoUrl = (value) =>
|
||||
/^https:\/\/github\.com\/[^/\s]+\/[^/\s]+(?:\.git)?(?:\/tree\/[^/\s]+)?$/i.test(
|
||||
normalizeInstallUrl(value),
|
||||
);
|
||||
|
||||
const selectedInstallDownloadUrl = computed(() => {
|
||||
const plugin = selectedInstallPlugin.value;
|
||||
const downloadUrl = String(plugin?.download_url || "").trim();
|
||||
if (!downloadUrl) return "";
|
||||
if (normalizeInstallUrl(plugin?.repo) !== normalizeInstallUrl(extension_url.value)) {
|
||||
return "";
|
||||
}
|
||||
return downloadUrl;
|
||||
});
|
||||
|
||||
const selectedInstallSourceUrl = computed(
|
||||
() => selectedInstallDownloadUrl.value || String(extension_url.value || "").trim(),
|
||||
);
|
||||
|
||||
const installUsesGithubSource = computed(
|
||||
() => !selectedInstallDownloadUrl.value && isGithubRepoUrl(extension_url.value),
|
||||
);
|
||||
|
||||
// 为表格视图创建一个处理安装插件的函数
|
||||
const handleInstallPlugin = async (plugin) => {
|
||||
@@ -940,7 +926,6 @@ export const useExtensionPage = () => {
|
||||
} else {
|
||||
selectedMarketInstallPlugin.value = plugin;
|
||||
extension_url.value = plugin.repo;
|
||||
upload_file.value = null;
|
||||
dialog.value = true;
|
||||
uploadTab.value = "url";
|
||||
}
|
||||
@@ -951,7 +936,6 @@ export const useExtensionPage = () => {
|
||||
if (selectedDangerPlugin.value) {
|
||||
selectedMarketInstallPlugin.value = selectedDangerPlugin.value;
|
||||
extension_url.value = selectedDangerPlugin.value.repo;
|
||||
upload_file.value = null;
|
||||
dialog.value = true;
|
||||
uploadTab.value = "url";
|
||||
}
|
||||
@@ -1193,58 +1177,6 @@ export const useExtensionPage = () => {
|
||||
}
|
||||
pluginMarketData.value = notInstalled.concat(installed);
|
||||
};
|
||||
|
||||
const normalizeAstrBotVersionSpec = (value) => String(value || "").trim();
|
||||
|
||||
const checkAstrBotVersionCompatibility = async (versionSpec) => {
|
||||
const normalizedSpec = normalizeAstrBotVersionSpec(versionSpec);
|
||||
if (!normalizedSpec) {
|
||||
return { checked: false, compatible: true, message: "" };
|
||||
}
|
||||
|
||||
if (marketCompatibilityCache.has(normalizedSpec)) {
|
||||
return marketCompatibilityCache.get(normalizedSpec);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post("/api/plugin/check-compat", {
|
||||
astrbot_version: normalizedSpec,
|
||||
});
|
||||
const result = {
|
||||
checked: res.data.status === "ok",
|
||||
compatible:
|
||||
res.data.status === "ok" ? !!res.data.data?.compatible : true,
|
||||
message: res.data.data?.message || "",
|
||||
};
|
||||
marketCompatibilityCache.set(normalizedSpec, result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.debug("Failed to check plugin compatibility:", err);
|
||||
const result = { checked: false, compatible: true, message: "" };
|
||||
marketCompatibilityCache.set(normalizedSpec, result);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
const annotateMarketCompatibility = async () => {
|
||||
const specs = [
|
||||
...new Set(
|
||||
pluginMarketData.value
|
||||
.map((plugin) => normalizeAstrBotVersionSpec(plugin?.astrbot_version))
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
|
||||
await Promise.all(specs.map((spec) => checkAstrBotVersionCompatibility(spec)));
|
||||
|
||||
pluginMarketData.value.forEach((plugin) => {
|
||||
const spec = normalizeAstrBotVersionSpec(plugin?.astrbot_version);
|
||||
const result = spec ? marketCompatibilityCache.get(spec) : null;
|
||||
plugin.astrbot_compat_checked = !!result?.checked;
|
||||
plugin.astrbot_compatible = result ? result.compatible : true;
|
||||
plugin.astrbot_compat_message = result?.message || "";
|
||||
});
|
||||
};
|
||||
|
||||
const showVersionCompatibilityWarning = (message) => {
|
||||
versionCompatibilityDialog.message = message;
|
||||
@@ -1268,19 +1200,23 @@ export const useExtensionPage = () => {
|
||||
versionCompatibilityDialog.show = false;
|
||||
};
|
||||
|
||||
const handleInstallResponse = async (resData) => {
|
||||
const handleInstallResponse = async (resData, { toastStatus = false } = {}) => {
|
||||
if (
|
||||
resData.status === "warning" &&
|
||||
resData.data?.warning_type === "astrbot_version_incompatible"
|
||||
) {
|
||||
toast(resData.message, "warning");
|
||||
onLoadingDialogResult(2, resData.message, -1);
|
||||
showVersionCompatibilityWarning(resData.message);
|
||||
await refreshExtensionsAfterInstallFailure();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toastStatus) {
|
||||
toast(resData.message, resData.status === "ok" ? "success" : "error");
|
||||
}
|
||||
|
||||
if (resData.status === "error") {
|
||||
toast(resData.message, "error");
|
||||
onLoadingDialogResult(2, resData.message, -1);
|
||||
await refreshExtensionsAfterInstallFailure();
|
||||
return false;
|
||||
}
|
||||
@@ -1302,8 +1238,7 @@ export const useExtensionPage = () => {
|
||||
|
||||
return axios.post("/api/plugin/install", {
|
||||
url: extension_url.value,
|
||||
download_url: selectedInstallDownloadUrl.value,
|
||||
proxy: selectedInstallDownloadUrl.value ? "" : getSelectedGitHubProxy(),
|
||||
proxy: getSelectedGitHubProxy(),
|
||||
ignore_version_check: ignoreVersionCheck,
|
||||
});
|
||||
};
|
||||
@@ -1315,9 +1250,8 @@ export const useExtensionPage = () => {
|
||||
extension_url.value = "";
|
||||
}
|
||||
|
||||
toast(resData.message, "success");
|
||||
onLoadingDialogResult(1, resData.message);
|
||||
dialog.value = false;
|
||||
selectedMarketInstallPlugin.value = null;
|
||||
await getExtensions();
|
||||
checkAlreadyInstalled();
|
||||
|
||||
@@ -1339,21 +1273,35 @@ export const useExtensionPage = () => {
|
||||
toast(tm("messages.dontFillBoth"), "error");
|
||||
return;
|
||||
}
|
||||
const source = upload_file.value !== null ? "file" : "url";
|
||||
loading_.value = true;
|
||||
loadingDialog.title = tm("status.loading");
|
||||
loadingDialog.show = true;
|
||||
|
||||
const source = upload_file.value !== null ? "file" : "url";
|
||||
toast(
|
||||
source === "file"
|
||||
? tm("messages.installing")
|
||||
: tm("messages.installingFromUrl") + " " + extension_url.value,
|
||||
"primary",
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await performInstallRequest({ source, ignoreVersionCheck });
|
||||
loading_.value = false;
|
||||
|
||||
const canContinue = await handleInstallResponse(res.data);
|
||||
const canContinue = await handleInstallResponse(res.data, {
|
||||
toastStatus: source === "url",
|
||||
});
|
||||
if (!canContinue) return;
|
||||
|
||||
await finalizeSuccessfulInstall(res.data, source);
|
||||
} catch (err) {
|
||||
loading_.value = false;
|
||||
const message = resolveErrorMessage(err, tm("messages.installFailed"));
|
||||
toast(message, "error");
|
||||
if (source === "url") {
|
||||
toast(message, "error");
|
||||
}
|
||||
onLoadingDialogResult(2, message, -1);
|
||||
await refreshExtensionsAfterInstallFailure();
|
||||
}
|
||||
};
|
||||
@@ -1418,7 +1366,6 @@ export const useExtensionPage = () => {
|
||||
pluginMarketData.value = data;
|
||||
trimExtensionName();
|
||||
checkAlreadyInstalled();
|
||||
await annotateMarketCompatibility();
|
||||
checkUpdate();
|
||||
refreshRandomPlugins();
|
||||
currentPage.value = 1; // 重置到第一页
|
||||
@@ -1460,7 +1407,6 @@ export const useExtensionPage = () => {
|
||||
pluginMarketData.value = data;
|
||||
trimExtensionName();
|
||||
checkAlreadyInstalled();
|
||||
await annotateMarketCompatibility();
|
||||
checkUpdate();
|
||||
refreshRandomPlugins();
|
||||
} catch (err) {
|
||||
@@ -1507,9 +1453,6 @@ export const useExtensionPage = () => {
|
||||
installCompat.checked = false;
|
||||
installCompat.compatible = true;
|
||||
installCompat.message = "";
|
||||
if (!dialogOpen) {
|
||||
selectedMarketInstallPlugin.value = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
await checkInstallCompatibility();
|
||||
@@ -1616,6 +1559,7 @@ export const useExtensionPage = () => {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
showRandomPlugins,
|
||||
normalizeStr,
|
||||
toPinyinText,
|
||||
toInitials,
|
||||
@@ -1628,6 +1572,8 @@ export const useExtensionPage = () => {
|
||||
randomPlugins,
|
||||
shufflePlugins,
|
||||
refreshRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
displayItemsPerPage,
|
||||
totalPages,
|
||||
paginatedPlugins,
|
||||
@@ -1659,8 +1605,6 @@ export const useExtensionPage = () => {
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
openInstallDialog,
|
||||
closeInstallDialog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
@@ -1684,9 +1628,6 @@ export const useExtensionPage = () => {
|
||||
getPlatformDisplayList,
|
||||
resolveSelectedInstallPlugin,
|
||||
selectedInstallPlugin,
|
||||
selectedInstallDownloadUrl,
|
||||
selectedInstallSourceUrl,
|
||||
installUsesGithubSource,
|
||||
checkInstallCompatibility,
|
||||
refreshPluginMarket,
|
||||
handleLocaleChange,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-card class="persona-card" :class="{ 'dragging': isDragging }" rounded="lg" variant="outlined" @click="$emit('view')"
|
||||
elevation="0" draggable="true" @dragstart="handleDragStart" @dragend="handleDragEnd">
|
||||
<v-card class="persona-card" :class="{ 'dragging': isDragging }" rounded="lg" @click="$emit('view')" elevation="0"
|
||||
draggable="true" @dragstart="handleDragStart" @dragend="handleDragEnd">
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<div class="text-truncate ml-2">{{ persona.persona_id }}</div>
|
||||
<v-menu offset-y>
|
||||
@@ -142,15 +142,9 @@ export default defineComponent({
|
||||
|
||||
<style scoped>
|
||||
.persona-card {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
height: 100%;
|
||||
cursor: grab;
|
||||
transition: background-color 0.16s ease, opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.persona-card:hover,
|
||||
.persona-card:focus-within {
|
||||
background: rgba(var(--v-theme-on-surface), 0.04);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.persona-card:active {
|
||||
|
||||
@@ -191,7 +191,6 @@ export default defineConfig({
|
||||
{ text: "接收消息事件", link: "/guides/listen-message-event" },
|
||||
{ text: "发送消息", link: "/guides/send-message" },
|
||||
{ text: "插件配置", link: "/guides/plugin-config" },
|
||||
{ text: "插件国际化", link: "/guides/plugin-i18n" },
|
||||
{ text: "调用 AI", link: "/guides/ai" },
|
||||
{ text: "存储", link: "/guides/storage" },
|
||||
{ text: "文转图", link: "/guides/html-to-pic" },
|
||||
@@ -434,7 +433,6 @@ export default defineConfig({
|
||||
{ text: "Listen to Message Events", link: "/guides/listen-message-event" },
|
||||
{ text: "Send Messages", link: "/guides/send-message" },
|
||||
{ text: "Plugin Configuration", link: "/guides/plugin-config" },
|
||||
{ text: "Plugin Internationalization", link: "/guides/plugin-i18n" },
|
||||
{ text: "AI", link: "/guides/ai" },
|
||||
{ text: "Storage", link: "/guides/storage" },
|
||||
{ text: "HTML to Image", link: "/guides/html-to-pic" },
|
||||
|
||||
@@ -56,10 +56,6 @@ The file content is a `Schema` that represents the configuration. The Schema is
|
||||
- `editor_theme`: Optional. The theme for the code editor. Options are `vs-light` (default) and `vs-dark`.
|
||||
- `_special`: Optional. Used to call AstrBot's visualization features for provider selection, persona selection, knowledge base selection, etc. See details below.
|
||||
|
||||
### Configuration Internationalization (Optional)
|
||||
|
||||
Configuration `description`, `hint`, and select `labels` can follow the WebUI language. See [Plugin Internationalization](./plugin-i18n).
|
||||
|
||||
When the code editor is enabled, it looks like this:
|
||||
|
||||

|
||||
@@ -220,4 +216,4 @@ class ConfigPlugin(Star):
|
||||
|
||||
## Configuration Updates
|
||||
|
||||
When you update the Schema across different versions, AstrBot will recursively inspect the configuration items in the Schema, automatically adding default values for missing items and removing those that no longer exist.
|
||||
When you update the Schema across different versions, AstrBot will recursively inspect the configuration items in the Schema, automatically adding default values for missing items and removing those that no longer exist.
|
||||
@@ -1,146 +0,0 @@
|
||||
# Plugin Internationalization
|
||||
|
||||
Plugins can provide `.astrbot-plugin/i18n/*.json` files in their own directory so the WebUI can display plugin names, descriptions, and configuration text in the current language.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```text
|
||||
your_plugin/
|
||||
metadata.yaml
|
||||
_conf_schema.json
|
||||
.astrbot-plugin/
|
||||
i18n/
|
||||
zh-CN.json
|
||||
en-US.json
|
||||
```
|
||||
|
||||
Locale file names use WebUI locales, such as `zh-CN.json` and `en-US.json`. Each file must contain a JSON object.
|
||||
|
||||
When the current locale has no translation, a field is missing, or the locale file does not exist, AstrBot falls back to the default text:
|
||||
|
||||
- Plugin names, card short descriptions, and descriptions fall back to `display_name`, `short_desc`, and `desc` in `metadata.yaml`.
|
||||
- Configuration text falls back to `description`, `hint`, and `labels` in `_conf_schema.json`.
|
||||
|
||||
## Metadata
|
||||
|
||||
`metadata` overrides the plugin name, card short description, and description shown on plugin pages.
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"display_name": "Weather Assistant",
|
||||
"short_desc": "One-line weather lookup.",
|
||||
"desc": "Query weather and provide travel suggestions."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`config` overrides text from `_conf_schema.json`. The structure is nested by configuration item name.
|
||||
|
||||
Example `_conf_schema.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enable": {
|
||||
"description": "Enable",
|
||||
"type": "bool",
|
||||
"hint": "Whether to enable this plugin.",
|
||||
"default": true
|
||||
},
|
||||
"mode": {
|
||||
"description": "Mode",
|
||||
"type": "string",
|
||||
"options": ["fast", "safe"],
|
||||
"labels": ["Fast", "Safe"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Corresponding `.astrbot-plugin/i18n/zh-CN.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"enable": {
|
||||
"description": "启用",
|
||||
"hint": "是否启用这个插件。"
|
||||
},
|
||||
"mode": {
|
||||
"description": "模式",
|
||||
"labels": ["快速", "安全"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`options` are stored configuration values and should usually not be translated. Use `labels` for select display text.
|
||||
|
||||
## Nested Configuration
|
||||
|
||||
For `object` items in `_conf_schema.json`, translations use the same nested field structure.
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"sub_config": {
|
||||
"name": {
|
||||
"description": "Name",
|
||||
"hint": "The name shown in messages."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Template Lists
|
||||
|
||||
`template_list` template names and fields can also be translated. Put template names under `templates.<template>.name`, then continue nesting for fields inside the template.
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"rules": {
|
||||
"description": "Rules",
|
||||
"templates": {
|
||||
"default": {
|
||||
"name": "Default template",
|
||||
"threshold": {
|
||||
"description": "Threshold",
|
||||
"hint": "Triggers the rule after reaching this value."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here is an English translation example for a real configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"display_name": "HAPI Vibe Coding Remote",
|
||||
"desc": "Connect to a HAPI service and control coding agent sessions from chat platforms."
|
||||
},
|
||||
"config": {
|
||||
"hapi_endpoint": {
|
||||
"description": "HAPI service URL",
|
||||
"hint": "Example: http://localhost:3006"
|
||||
},
|
||||
"output_level": {
|
||||
"description": "SSE delivery level",
|
||||
"hint": "silence: permission requests only; simple: plain text messages and system events; summary: recent N messages when a task completes; detail: all messages in real time",
|
||||
"labels": ["Silence", "Simple", "Summary", "Detail"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
Plugin internationalization only reads the `.astrbot-plugin/i18n` directory. Locale files must use nested JSON objects; dot-key flat entries are not supported.
|
||||
@@ -51,16 +51,6 @@ You can add a `logo.png` file in the plugin directory as the plugin's logo. Plea
|
||||
|
||||
You can modify (or add) the `display_name` field in the `metadata.yaml` file to serve as the plugin's display name in scenarios like the plugin marketplace, making it easier for users to read.
|
||||
|
||||
Plugin display names and descriptions can follow the WebUI language. See [Plugin Internationalization](./guides/plugin-i18n).
|
||||
|
||||
### Plugin Short Description (Optional)
|
||||
|
||||
You can add a `short_desc` field to `metadata.yaml` as the short description shown on plugin marketplace cards. Keep it to a concise one-sentence summary. If it is not provided, cards fall back to `desc`.
|
||||
|
||||
```yaml
|
||||
short_desc: A one-line summary of your plugin.
|
||||
```
|
||||
|
||||
### Declare Supported Platforms (Optional)
|
||||
|
||||
You can add a `support_platforms` field (`list[str]`) to `metadata.yaml` to declare which platform adapters your plugin supports. The WebUI plugin page will display this field.
|
||||
|
||||
@@ -56,10 +56,6 @@ AstrBot 提供了“强大”的配置解析和可视化功能。能够让用户
|
||||
- `editor_theme`: 可选。代码编辑器的主题,可选值有 `vs-light`(默认), `vs-dark`。
|
||||
- `_special`: 可选。用于调用 AstrBot 提供的可视化提供商选取、人格选取、知识库选取等功能,详见下文。
|
||||
|
||||
### 配置项国际化(可选)
|
||||
|
||||
配置项的 `description`、`hint` 和下拉选项 `labels` 支持按 WebUI 语言显示,详见[插件国际化](./plugin-i18n)。
|
||||
|
||||
其中,如果启用了代码编辑器,效果如下图所示:
|
||||
|
||||

|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
# 插件国际化
|
||||
|
||||
插件可以在自己的目录下提供 `.astrbot-plugin/i18n/*.json`,让 WebUI 根据当前语言显示插件名称、描述和配置项文案。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
your_plugin/
|
||||
metadata.yaml
|
||||
_conf_schema.json
|
||||
.astrbot-plugin/
|
||||
i18n/
|
||||
zh-CN.json
|
||||
en-US.json
|
||||
```
|
||||
|
||||
语言文件名使用 WebUI 的 locale,例如 `zh-CN.json`、`en-US.json`。文件内容必须是 JSON object。
|
||||
|
||||
当当前语言没有对应翻译、某个字段缺失,或语言文件不存在时,AstrBot 会回退到默认文案:
|
||||
|
||||
- 插件名称、卡片短描述和描述回退到 `metadata.yaml` 中的 `display_name`、`short_desc`、`desc`。
|
||||
- 配置项文案回退到 `_conf_schema.json` 中的 `description`、`hint`、`labels`。
|
||||
|
||||
## 元数据
|
||||
|
||||
`metadata` 用于覆盖插件在插件页展示的名称、卡片短描述和描述。
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"display_name": "天气助手",
|
||||
"short_desc": "一句话天气查询。",
|
||||
"desc": "查询天气并提供出行建议。"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配置项
|
||||
|
||||
`config` 用于覆盖 `_conf_schema.json` 中的配置文案。结构按配置项名称嵌套。
|
||||
|
||||
例如 `_conf_schema.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enable": {
|
||||
"description": "Enable",
|
||||
"type": "bool",
|
||||
"hint": "Whether to enable this plugin.",
|
||||
"default": true
|
||||
},
|
||||
"mode": {
|
||||
"description": "Mode",
|
||||
"type": "string",
|
||||
"options": ["fast", "safe"],
|
||||
"labels": ["Fast", "Safe"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对应 `.astrbot-plugin/i18n/zh-CN.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"enable": {
|
||||
"description": "启用",
|
||||
"hint": "是否启用这个插件。"
|
||||
},
|
||||
"mode": {
|
||||
"description": "模式",
|
||||
"labels": ["快速", "安全"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`options` 是配置保存值,不建议翻译。下拉框的展示文本请使用 `labels`。
|
||||
|
||||
## 嵌套配置
|
||||
|
||||
如果 `_conf_schema.json` 中有 `object` 类型配置,翻译也按同样的字段结构继续嵌套。
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"sub_config": {
|
||||
"name": {
|
||||
"description": "名称",
|
||||
"hint": "显示在消息中的名称。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 模板列表
|
||||
|
||||
`template_list` 的模板名称和模板内字段也可以翻译。模板名称放在 `templates.<模板名>.name`,模板内字段继续往下嵌套。
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"rules": {
|
||||
"description": "规则",
|
||||
"templates": {
|
||||
"default": {
|
||||
"name": "默认模板",
|
||||
"threshold": {
|
||||
"description": "阈值",
|
||||
"hint": "达到该值后触发规则。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
下面是一个真实配置项的英文翻译示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"display_name": "HAPI Vibe Coding Remote",
|
||||
"desc": "Connect to a HAPI service and control coding agent sessions from chat platforms."
|
||||
},
|
||||
"config": {
|
||||
"hapi_endpoint": {
|
||||
"description": "HAPI service URL",
|
||||
"hint": "Example: http://localhost:3006"
|
||||
},
|
||||
"output_level": {
|
||||
"description": "SSE delivery level",
|
||||
"hint": "silence: permission requests only; simple: plain text messages and system events; summary: recent N messages when a task completes; detail: all messages in real time",
|
||||
"labels": ["Silence", "Simple", "Summary", "Detail"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 约束
|
||||
|
||||
插件国际化只读取 `.astrbot-plugin/i18n` 目录。语言文件必须使用嵌套 JSON 结构,不支持点号扁平 key。
|
||||
@@ -53,16 +53,6 @@ git clone 插件仓库地址
|
||||
|
||||
可以修改(或添加) `metadata.yaml` 文件中的 `display_name` 字段,作为插件在插件市场等场景中的展示名,以方便用户阅读。
|
||||
|
||||
插件展示名和描述支持按 WebUI 语言显示,详见[插件国际化](./guides/plugin-i18n)。
|
||||
|
||||
### 插件短描述(可选)
|
||||
|
||||
你可以在 `metadata.yaml` 中新增 `short_desc` 字段,作为插件市场卡片上的短描述。它适合写成一句简短介绍;如果没有提供,卡片会回退显示 `desc`。
|
||||
|
||||
```yaml
|
||||
short_desc: 一句话介绍你的插件。
|
||||
```
|
||||
|
||||
### 声明支持平台(Optional)
|
||||
|
||||
你可以在 `metadata.yaml` 中新增 `support_platforms` 字段(`list[str]`),声明插件支持的平台适配器。WebUI 插件页会展示该字段。
|
||||
|
||||
@@ -226,14 +226,6 @@ async def on_message(self, event: AstrMessageEvent):
|
||||
|
||||
你可以修改(或添加) `metadata.yaml` 文件中的 `display_name` 字段,作为插件在插件市场等场景中的展示名,以方便用户阅读。
|
||||
|
||||
### 插件短描述
|
||||
|
||||
你可以在 `metadata.yaml` 中新增 `short_desc` 字段,作为插件市场卡片上的短描述。它适合写成一句简短介绍;如果没有提供,卡片会回退显示 `desc`。
|
||||
|
||||
```yaml
|
||||
short_desc: 一句话介绍你的插件。
|
||||
```
|
||||
|
||||
### 声明支持平台(Optional)
|
||||
|
||||
你可以在 `metadata.yaml` 中新增 `support_platforms` 字段(`list[str]`),声明插件支持的平台适配器。WebUI 插件页会展示该字段。
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
@@ -37,7 +36,6 @@ def _write_local_test_plugin(plugin_path: Path, repo_url: str):
|
||||
"version": "1.0.0",
|
||||
"author": "AstrBot Team",
|
||||
"desc": "Local test plugin",
|
||||
"short_desc": "Local test short description",
|
||||
}
|
||||
with open(plugin_path / "metadata.yaml", "w", encoding="utf-8") as f:
|
||||
yaml.dump(metadata, f)
|
||||
@@ -54,79 +52,6 @@ def _write_requirements(plugin_path: Path):
|
||||
f.write("networkx\n")
|
||||
|
||||
|
||||
def test_load_plugin_i18n_reads_locale_files(tmp_path: Path):
|
||||
plugin_path = tmp_path / "plugin"
|
||||
i18n_path = plugin_path / ".astrbot-plugin" / "i18n"
|
||||
i18n_path.mkdir(parents=True)
|
||||
(i18n_path / "zh-CN.json").write_text(
|
||||
json.dumps({"metadata": {"desc": "中文描述"}}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(i18n_path / "en-US.json").write_text(
|
||||
json.dumps({"metadata": {"desc": "English description"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(i18n_path / "README.md").write_text("ignored", encoding="utf-8")
|
||||
|
||||
assert PluginManager._load_plugin_i18n(str(plugin_path)) == {
|
||||
"zh-CN": {"metadata": {"desc": "中文描述"}},
|
||||
"en-US": {"metadata": {"desc": "English description"}},
|
||||
}
|
||||
|
||||
|
||||
def test_load_plugin_i18n_ignores_legacy_directories(tmp_path: Path):
|
||||
plugin_path = tmp_path / "plugin"
|
||||
hidden_legacy_i18n_path = plugin_path / ".i18n"
|
||||
legacy_i18n_path = plugin_path / "i18n"
|
||||
hidden_legacy_i18n_path.mkdir(parents=True)
|
||||
legacy_i18n_path.mkdir()
|
||||
(hidden_legacy_i18n_path / "zh-CN.json").write_text(
|
||||
json.dumps({"metadata": {"desc": "隐藏旧目录"}}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(legacy_i18n_path / "zh-CN.json").write_text(
|
||||
json.dumps({"metadata": {"desc": "中文描述"}}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert PluginManager._load_plugin_i18n(str(plugin_path)) == {}
|
||||
|
||||
|
||||
def test_load_plugin_metadata_includes_i18n(tmp_path: Path):
|
||||
plugin_path = tmp_path / "helloworld"
|
||||
_write_local_test_plugin(plugin_path, TEST_PLUGIN_REPO)
|
||||
i18n_path = plugin_path / ".astrbot-plugin" / "i18n"
|
||||
i18n_path.mkdir(parents=True)
|
||||
(i18n_path / "zh-CN.json").write_text(
|
||||
json.dumps({"metadata": {"display_name": "你好世界"}}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
metadata = PluginManager._load_plugin_metadata(str(plugin_path))
|
||||
|
||||
assert metadata is not None
|
||||
assert metadata.short_desc == "Local test short description"
|
||||
assert metadata.i18n == {"zh-CN": {"metadata": {"display_name": "你好世界"}}}
|
||||
|
||||
|
||||
def test_loaded_metadata_can_copy_i18n_into_existing_star_metadata(tmp_path: Path):
|
||||
plugin_path = tmp_path / "helloworld"
|
||||
_write_local_test_plugin(plugin_path, TEST_PLUGIN_REPO)
|
||||
i18n_path = plugin_path / ".astrbot-plugin" / "i18n"
|
||||
i18n_path.mkdir(parents=True)
|
||||
(i18n_path / "zh-CN.json").write_text(
|
||||
json.dumps({"metadata": {"desc": "中文描述"}}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
existing_metadata = star_manager_module.StarMetadata(name="old")
|
||||
loaded_metadata = PluginManager._load_plugin_metadata(str(plugin_path))
|
||||
|
||||
assert loaded_metadata is not None
|
||||
existing_metadata.i18n = loaded_metadata.i18n
|
||||
assert existing_metadata.i18n == {"zh-CN": {"metadata": {"desc": "中文描述"}}}
|
||||
|
||||
|
||||
def _clear_module_cache():
|
||||
"""Clear test-specific modules from sys.modules to allow reloading."""
|
||||
import sys
|
||||
|
||||
@@ -6,7 +6,6 @@ import certifi
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from astrbot.core.star.updator import PluginUpdator
|
||||
from astrbot.core.zip_updator import RepoZipUpdator
|
||||
|
||||
|
||||
@@ -139,44 +138,6 @@ def fake_async_client_state() -> _FakeAsyncClientState:
|
||||
return _FakeAsyncClientState()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_updator_install_prefers_download_url(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
calls = {}
|
||||
updator = PluginUpdator()
|
||||
updator.plugin_store_path = str(tmp_path)
|
||||
|
||||
async def fake_download_file(url: str, path: str, timeout: float = 1800.0): # noqa: ARG001
|
||||
calls["download"] = (url, path)
|
||||
Path(path).write_bytes(b"zip-data")
|
||||
|
||||
async def fail_download_from_repo_url(*args, **kwargs): # noqa: ARG001
|
||||
raise AssertionError("install should use download_url instead of GitHub")
|
||||
|
||||
def fake_unzip_file(zip_path: str, target_dir: str):
|
||||
calls["unzip"] = (zip_path, target_dir)
|
||||
|
||||
monkeypatch.setattr(updator, "_download_file", fake_download_file)
|
||||
monkeypatch.setattr(updator, "download_from_repo_url", fail_download_from_repo_url)
|
||||
monkeypatch.setattr(updator, "unzip_file", fake_unzip_file)
|
||||
|
||||
plugin_path = await updator.install(
|
||||
"https://github.com/Owner/plugin-name",
|
||||
proxy="https://gh-proxy.example",
|
||||
download_url="https://cdn.example/plugin.zip",
|
||||
)
|
||||
|
||||
expected_path = tmp_path / "plugin_name"
|
||||
assert plugin_path == str(expected_path)
|
||||
assert calls["download"] == (
|
||||
"https://cdn.example/plugin.zip",
|
||||
str(expected_path) + ".zip",
|
||||
)
|
||||
assert calls["unzip"] == (str(expected_path) + ".zip", str(expected_path))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_release_info_uses_httpx_client_with_env_proxy_support(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -226,9 +187,7 @@ async def test_fetch_release_info_uses_httpx_client_with_env_proxy_support(
|
||||
"zipball_url": "https://example.com/astrbot.zip",
|
||||
}
|
||||
]
|
||||
assert fake_async_client_state.requested_urls == [
|
||||
"https://api.soulter.top/releases"
|
||||
]
|
||||
assert fake_async_client_state.requested_urls == ["https://api.soulter.top/releases"]
|
||||
assert fake_async_client_state.init_kwargs is not None
|
||||
assert fake_async_client_state.init_kwargs["follow_redirects"] is True
|
||||
assert fake_async_client_state.init_kwargs["timeout"] == 30.0
|
||||
@@ -262,9 +221,7 @@ async def test_download_from_repo_url_uses_httpx_stream_for_zip_download(
|
||||
zip_updator_module,
|
||||
"download_file",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError(
|
||||
"download_from_repo_url should not use aiohttp download_file"
|
||||
)
|
||||
AssertionError("download_from_repo_url should not use aiohttp download_file")
|
||||
),
|
||||
raising=False,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user