mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-09-24 16:39:52 +08:00
Merge branch 'master' into dev
This commit is contained in:
@@ -40,6 +40,7 @@ jobs:
|
||||
pytest --cov=astrbot -v -o log_cli=true -o log_level=DEBUG
|
||||
|
||||
- name: Upload results to Codecov
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-nightly-image:
|
||||
if: github.event_name == 'schedule'
|
||||
if: github.repository == 'AstrBotDevs/AstrBot' && github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
run: echo "Test Docker image has been built and pushed successfully"
|
||||
|
||||
build-release-image:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
|
||||
if: github.repository == 'AstrBotDevs/AstrBot' && (github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')))
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
title-format:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
@@ -20,6 +20,7 @@ permissions:
|
||||
jobs:
|
||||
build-dashboard:
|
||||
name: Build Dashboard
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
@@ -104,6 +105,7 @@ jobs:
|
||||
|
||||
publish-release:
|
||||
name: Publish GitHub Release
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- build-dashboard
|
||||
@@ -183,6 +185,7 @@ jobs:
|
||||
|
||||
publish-pypi:
|
||||
name: Publish PyPI
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- publish-release
|
||||
|
||||
@@ -18,6 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
@@ -18,6 +18,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -82,14 +82,19 @@ from .permissions import check_admin_permission
|
||||
@dataclass
|
||||
class FileUploadTool(FunctionTool):
|
||||
name: str = "astrbot_upload_file"
|
||||
description: str = "Upload a local file to the sandbox. The file must exist on the local filesystem."
|
||||
description: str = (
|
||||
"Transfer a file FROM the host machine INTO the sandbox so that sandbox "
|
||||
"code can access it. Use this when the user sends/attaches a file and you "
|
||||
"need to process it inside the sandbox. The local_path must point to an "
|
||||
"existing file on the host filesystem."
|
||||
)
|
||||
parameters: dict = field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local_path": {
|
||||
"type": "string",
|
||||
"description": "The local file path to upload. This must be an absolute path to an existing file on the local filesystem.",
|
||||
"description": "Absolute path to the file on the host filesystem that will be copied into the sandbox.",
|
||||
},
|
||||
# "remote_path": {
|
||||
# "type": "string",
|
||||
@@ -143,14 +148,18 @@ class FileUploadTool(FunctionTool):
|
||||
@dataclass
|
||||
class FileDownloadTool(FunctionTool):
|
||||
name: str = "astrbot_download_file"
|
||||
description: str = "Download a file from the sandbox. Only call this when user explicitly need you to download a file."
|
||||
description: str = (
|
||||
"Transfer a file FROM the sandbox OUT to the host and optionally send it "
|
||||
"to the user. Use this ONLY when the user asks to retrieve/export a file "
|
||||
"that was created or modified inside the sandbox."
|
||||
)
|
||||
parameters: dict = field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"remote_path": {
|
||||
"type": "string",
|
||||
"description": "The path of the file in the sandbox to download.",
|
||||
"description": "Path of the file inside the sandbox to copy out to the host.",
|
||||
},
|
||||
"also_send_to_user": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from enum import IntEnum, StrEnum
|
||||
from enum import Enum, IntEnum
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
@@ -36,7 +36,7 @@ class KookMessageType(IntEnum):
|
||||
SYSTEM = 255
|
||||
|
||||
|
||||
class KookModuleType(StrEnum):
|
||||
class KookModuleType(str, Enum):
|
||||
PLAIN_TEXT = "plain-text"
|
||||
KMARKDOWN = "kmarkdown"
|
||||
IMAGE = "image"
|
||||
@@ -311,7 +311,7 @@ class KookMessageSignal(IntEnum):
|
||||
"""server->client resume ack"""
|
||||
|
||||
|
||||
class KookChannelType(StrEnum):
|
||||
class KookChannelType(str, Enum):
|
||||
GROUP = "GROUP"
|
||||
PERSON = "PERSON"
|
||||
BROADCAST = "BROADCAST"
|
||||
|
||||
@@ -5,6 +5,7 @@ import contextlib
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import keyword
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@@ -423,6 +424,43 @@ class PluginManager:
|
||||
|
||||
return metadata
|
||||
|
||||
@staticmethod
|
||||
def _normalize_plugin_dir_name(plugin_name: str) -> str:
|
||||
return plugin_name.strip()
|
||||
|
||||
@staticmethod
|
||||
def _validate_importable_name(plugin_name: str) -> None:
|
||||
if "/" in plugin_name or "\\" in plugin_name:
|
||||
raise ValueError(
|
||||
"metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。"
|
||||
)
|
||||
if not plugin_name.isidentifier() or keyword.iskeyword(plugin_name):
|
||||
raise Exception(
|
||||
"metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_plugin_dir_name_from_metadata(plugin_path: str) -> str:
|
||||
metadata_path = os.path.join(plugin_path, "metadata.yaml")
|
||||
if not os.path.exists(metadata_path):
|
||||
raise Exception("未找到 metadata.yaml,无法获取插件目录名。")
|
||||
|
||||
with open(metadata_path, encoding="utf-8") as f:
|
||||
metadata = yaml.safe_load(f)
|
||||
|
||||
if not isinstance(metadata, dict):
|
||||
raise Exception("metadata.yaml 格式错误。")
|
||||
|
||||
plugin_name = metadata.get("name")
|
||||
if not isinstance(plugin_name, str) or not plugin_name.strip():
|
||||
raise Exception("metadata.yaml 中缺少 name 字段。")
|
||||
|
||||
plugin_dir_name = PluginManager._normalize_plugin_dir_name(plugin_name)
|
||||
if not plugin_dir_name:
|
||||
raise Exception("metadata.yaml 中 name 字段内容非法。")
|
||||
PluginManager._validate_importable_name(plugin_dir_name)
|
||||
return plugin_dir_name
|
||||
|
||||
@staticmethod
|
||||
def _validate_astrbot_version_specifier(
|
||||
version_spec: str | None,
|
||||
@@ -1205,10 +1243,30 @@ class PluginManager:
|
||||
plugin_path = ""
|
||||
dir_name = ""
|
||||
try:
|
||||
_, repo_name, _ = self.updator.parse_github_url(repo_url)
|
||||
repo_name = self.updator.format_name(repo_name)
|
||||
plugin_path = os.path.join(self.plugin_store_path, repo_name)
|
||||
if os.path.exists(plugin_path):
|
||||
raise Exception(
|
||||
f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。"
|
||||
)
|
||||
plugin_path = await self.updator.install(repo_url, proxy)
|
||||
|
||||
# reload the plugin
|
||||
dir_name = os.path.basename(plugin_path)
|
||||
metadata_dir_name = self._get_plugin_dir_name_from_metadata(plugin_path)
|
||||
target_plugin_path = os.path.join(
|
||||
self.plugin_store_path,
|
||||
metadata_dir_name,
|
||||
)
|
||||
if target_plugin_path != plugin_path and os.path.exists(
|
||||
target_plugin_path
|
||||
):
|
||||
raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。")
|
||||
if target_plugin_path != plugin_path:
|
||||
os.rename(plugin_path, target_plugin_path)
|
||||
plugin_path = target_plugin_path
|
||||
dir_name = metadata_dir_name
|
||||
await self._ensure_plugin_requirements(
|
||||
plugin_path,
|
||||
dir_name,
|
||||
@@ -1578,52 +1636,25 @@ class PluginManager:
|
||||
async def install_plugin_from_file(
|
||||
self, zip_file_path: str, ignore_version_check: bool = False
|
||||
):
|
||||
dir_name = os.path.basename(zip_file_path).replace(".zip", "")
|
||||
dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
|
||||
desti_dir = os.path.join(self.plugin_store_path, dir_name)
|
||||
|
||||
# 第一步:检查是否已安装同目录名的插件,先终止旧插件
|
||||
existing_plugin = None
|
||||
for star in self.context.get_all_stars():
|
||||
if star.root_dir_name == dir_name:
|
||||
existing_plugin = star
|
||||
break
|
||||
|
||||
if existing_plugin:
|
||||
logger.info(f"检测到插件 {existing_plugin.name} 已安装,正在终止旧插件...")
|
||||
try:
|
||||
await self._terminate_plugin(existing_plugin)
|
||||
except Exception:
|
||||
logger.warning(traceback.format_exc())
|
||||
if existing_plugin.name and existing_plugin.module_path:
|
||||
await self._unbind_plugin(
|
||||
existing_plugin.name, existing_plugin.module_path
|
||||
)
|
||||
dir_name = os.path.splitext(os.path.basename(zip_file_path))[0]
|
||||
desti_dir = tempfile.mkdtemp(
|
||||
dir=self.plugin_store_path, prefix="plugin_upload_"
|
||||
)
|
||||
temp_desti_dir = desti_dir
|
||||
|
||||
try:
|
||||
self.updator.unzip_file(zip_file_path, desti_dir)
|
||||
|
||||
# 第二步:解压后,读取新插件的 metadata.yaml,检查是否存在同名但不同目录的插件
|
||||
try:
|
||||
new_metadata = self._load_plugin_metadata(desti_dir)
|
||||
if new_metadata and new_metadata.name:
|
||||
for star in self.context.get_all_stars():
|
||||
if (
|
||||
star.name == new_metadata.name
|
||||
and star.root_dir_name != dir_name
|
||||
):
|
||||
logger.warning(
|
||||
f"检测到同名插件 {star.name} 存在于不同目录 {star.root_dir_name},正在终止..."
|
||||
)
|
||||
try:
|
||||
await self._terminate_plugin(star)
|
||||
except Exception:
|
||||
logger.warning(traceback.format_exc())
|
||||
if star.name and star.module_path:
|
||||
await self._unbind_plugin(star.name, star.module_path)
|
||||
break # 只处理第一个匹配的
|
||||
except Exception as e:
|
||||
logger.debug(f"读取新插件 metadata.yaml 失败,跳过同名检查: {e!s}")
|
||||
metadata_dir_name = self._get_plugin_dir_name_from_metadata(desti_dir)
|
||||
target_plugin_path = os.path.join(
|
||||
self.plugin_store_path,
|
||||
metadata_dir_name,
|
||||
)
|
||||
if target_plugin_path != desti_dir and os.path.exists(target_plugin_path):
|
||||
raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。")
|
||||
if target_plugin_path != desti_dir:
|
||||
os.rename(desti_dir, target_plugin_path)
|
||||
dir_name = metadata_dir_name
|
||||
desti_dir = target_plugin_path
|
||||
|
||||
# remove the zip
|
||||
try:
|
||||
@@ -1692,3 +1723,11 @@ class PluginManager:
|
||||
f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}",
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
if temp_desti_dir != desti_dir and os.path.isdir(temp_desti_dir):
|
||||
try:
|
||||
remove_dir(temp_desti_dir)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"清理临时插件解压目录失败: {temp_desti_dir},原因: {e!s}",
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"author": "CodedThemes",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"subset-icons": "node scripts/subset-mdi-font.mjs",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build-stage": "vue-tsc --noEmit && vite build --base=/vue/free/stage/",
|
||||
"build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/",
|
||||
@@ -33,7 +34,6 @@
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pinia": "2.1.6",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"remixicon": "3.5.0",
|
||||
"shiki": "^3.20.0",
|
||||
"stream-markdown": "^0.0.13",
|
||||
"vee-validate": "4.11.3",
|
||||
@@ -60,12 +60,17 @@
|
||||
"eslint-plugin-vue": "9.17.0",
|
||||
"prettier": "3.0.2",
|
||||
"sass": "1.66.1",
|
||||
"sass-loader": "13.3.2",
|
||||
"subset-font": "^2.4.0",
|
||||
"typescript": "5.1.6",
|
||||
"vite": "6.4.1",
|
||||
"vue-tsc": "1.8.27"
|
||||
"vite-plugin-webfont-dl": "^3.12.0",
|
||||
"vue-cli-plugin-vuetify": "2.5.8",
|
||||
"vue-tsc": "1.8.8",
|
||||
"vuetify-loader": "^2.0.0-alpha.9"
|
||||
},
|
||||
"overrides": {
|
||||
"immutable": "4.3.8",
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+235
-19
@@ -68,9 +68,6 @@ importers:
|
||||
pinyin-pro:
|
||||
specifier: ^3.26.0
|
||||
version: 3.28.0
|
||||
remixicon:
|
||||
specifier: 3.5.0
|
||||
version: 3.5.0
|
||||
shiki:
|
||||
specifier: ^3.20.0
|
||||
version: 3.22.0
|
||||
@@ -144,12 +141,24 @@ importers:
|
||||
sass:
|
||||
specifier: 1.66.1
|
||||
version: 1.66.1
|
||||
sass-loader:
|
||||
specifier: 13.3.2
|
||||
version: 13.3.2(sass@1.66.1)(webpack@5.105.0)
|
||||
subset-font:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
typescript:
|
||||
specifier: 5.1.6
|
||||
version: 5.1.6
|
||||
vite:
|
||||
specifier: 6.4.1
|
||||
version: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)
|
||||
vite-plugin-webfont-dl:
|
||||
specifier: ^3.12.0
|
||||
version: 3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))
|
||||
vue-cli-plugin-vuetify:
|
||||
specifier: 2.5.8
|
||||
version: 2.5.8(sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0))(vue@3.3.4)(vuetify-loader@2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0))(webpack@5.105.0)
|
||||
vue-tsc:
|
||||
specifier: 1.8.27
|
||||
version: 1.8.27(typescript@5.1.6)
|
||||
@@ -183,6 +192,12 @@ packages:
|
||||
'@braintree/sanitize-url@7.1.2':
|
||||
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
|
||||
|
||||
'@cacheable/memory@2.0.8':
|
||||
resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==}
|
||||
|
||||
'@cacheable/utils@2.4.0':
|
||||
resolution: {integrity: sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==}
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.0.3':
|
||||
resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==}
|
||||
|
||||
@@ -438,6 +453,15 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@keyv/bigmap@1.3.1':
|
||||
resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
keyv: ^5.6.0
|
||||
|
||||
'@keyv/serialize@1.1.1':
|
||||
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
|
||||
|
||||
'@mdi/font@7.2.96':
|
||||
resolution: {integrity: sha512-e//lmkmpFUMZKhmCY9zdjRe4zNXfbOIJnn6xveHbaV2kSw5aJ5dLXUxcRt1Gxfi7ZYpFLUWlkG2MGSFAiqAu7w==}
|
||||
|
||||
@@ -503,79 +527,66 @@ packages:
|
||||
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
|
||||
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.59.0':
|
||||
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
|
||||
@@ -1129,6 +1140,9 @@ packages:
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
cacheable@2.3.3:
|
||||
resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1165,6 +1179,14 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
chrome-trace-event@1.0.4:
|
||||
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
||||
clean-css@5.3.3:
|
||||
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
|
||||
engines: {node: '>= 10.0'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -1579,6 +1601,9 @@ packages:
|
||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
|
||||
flat-cache@6.1.20:
|
||||
resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==}
|
||||
|
||||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
|
||||
@@ -1591,6 +1616,9 @@ packages:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
fontverter@2.0.0:
|
||||
resolution: {integrity: sha512-DFVX5hvXuhi1Jven1tbpebYTCT9XYnvx6/Z+HFUPb7ZRMCW+pj2clU9VMhoTPgWKPhAs7JJDSk3CW1jNUvKCZQ==}
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1644,6 +1672,9 @@ packages:
|
||||
hachure-fill@0.5.2:
|
||||
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
|
||||
|
||||
harfbuzzjs@0.4.15:
|
||||
resolution: {integrity: sha512-p1edvnlc+vpRe2kz7OKzcscf0gyFiDZpco+miDxAiiZ67cu1oNlbuOkmP/A/i1l/w938VrkF2FdZ8scNcnkPrQ==}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1656,6 +1687,10 @@ packages:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hashery@1.5.0:
|
||||
resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1674,6 +1709,9 @@ packages:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
hookified@1.15.1:
|
||||
resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
@@ -1760,6 +1798,9 @@ packages:
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
keyv@5.6.0:
|
||||
resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
|
||||
|
||||
khroma@2.1.0:
|
||||
resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
|
||||
|
||||
@@ -1968,6 +2009,9 @@ packages:
|
||||
package-manager-detector@1.6.0:
|
||||
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2128,6 +2172,15 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
qified@0.6.0:
|
||||
resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
querystring@0.2.1:
|
||||
resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -2144,8 +2197,9 @@ packages:
|
||||
regex@6.1.0:
|
||||
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
|
||||
|
||||
remixicon@3.5.0:
|
||||
resolution: {integrity: sha512-wNzWGKf4frb3tEmgvP5shry0n1OdTjjEk9RHLuRuAhfA50bvEdpKH1XWNUYrHUSjAQQkkdyIm+lf4mOuysIKTQ==}
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
@@ -2253,6 +2307,9 @@ packages:
|
||||
stylis@4.3.6:
|
||||
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
|
||||
|
||||
subset-font@2.4.0:
|
||||
resolution: {integrity: sha512-DA/45nIj4NiseVdfHxVdVGL7hvNo3Ol6HjEm3KSYtPyDcsr6jh8Q37vSgz+A722wMfUd6nL8kgsi7uGv9DExXQ==}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2415,6 +2472,11 @@ packages:
|
||||
vue: ^3.0.0
|
||||
vuetify: '>=3'
|
||||
|
||||
vite-plugin-webfont-dl@3.12.0:
|
||||
resolution: {integrity: sha512-0jxsr8ycuoK/uV5Y3ytttTRhgvfZo8v3O4JZBlVc4C7QWIws/vCLVR4B3ag+TGVkLNQya6hXfY3UnZge3M8vmA==}
|
||||
peerDependencies:
|
||||
vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8
|
||||
|
||||
vite@6.4.1:
|
||||
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
@@ -2543,11 +2605,36 @@ packages:
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
watchpack@2.5.1:
|
||||
resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
wawoff2@2.0.1:
|
||||
resolution: {integrity: sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==}
|
||||
hasBin: true
|
||||
|
||||
webpack-sources@3.3.4:
|
||||
resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
webpack@5.105.0:
|
||||
resolution: {integrity: sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
webpack-cli: '*'
|
||||
peerDependenciesMeta:
|
||||
webpack-cli:
|
||||
optional: true
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
woff2sfnt-sfnt2woff@1.0.0:
|
||||
resolution: {integrity: sha512-edK4COc1c1EpRfMqCZO1xJOvdUtM5dbVb9iz97rScvnTevqEB3GllnLWCmMVp1MfQBdF1DFg/11I0rSyAdS4qQ==}
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2593,6 +2680,18 @@ snapshots:
|
||||
|
||||
'@braintree/sanitize-url@7.1.2': {}
|
||||
|
||||
'@cacheable/memory@2.0.8':
|
||||
dependencies:
|
||||
'@cacheable/utils': 2.4.0
|
||||
'@keyv/bigmap': 1.3.1(keyv@5.6.0)
|
||||
hookified: 1.15.1
|
||||
keyv: 5.6.0
|
||||
|
||||
'@cacheable/utils@2.4.0':
|
||||
dependencies:
|
||||
hashery: 1.5.0
|
||||
keyv: 5.6.0
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.0.3':
|
||||
dependencies:
|
||||
'@chevrotain/gast': 11.0.3
|
||||
@@ -2784,6 +2883,14 @@ snapshots:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
optional: true
|
||||
|
||||
'@keyv/bigmap@1.3.1(keyv@5.6.0)':
|
||||
dependencies:
|
||||
hashery: 1.5.0
|
||||
hookified: 1.15.1
|
||||
keyv: 5.6.0
|
||||
|
||||
'@keyv/serialize@1.1.1': {}
|
||||
|
||||
'@mdi/font@7.2.96': {}
|
||||
|
||||
'@mermaid-js/parser@0.6.3':
|
||||
@@ -3543,6 +3650,14 @@ snapshots:
|
||||
buffer-from@1.1.2:
|
||||
optional: true
|
||||
|
||||
cacheable@2.3.3:
|
||||
dependencies:
|
||||
'@cacheable/memory': 2.0.8
|
||||
'@cacheable/utils': 2.4.0
|
||||
hookified: 1.15.1
|
||||
keyv: 5.6.0
|
||||
qified: 0.6.0
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -3589,6 +3704,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
chrome-trace-event@1.0.4: {}
|
||||
|
||||
clean-css@5.3.3:
|
||||
dependencies:
|
||||
source-map: 0.6.1
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@@ -4061,10 +4182,21 @@ snapshots:
|
||||
keyv: 4.5.4
|
||||
rimraf: 3.0.2
|
||||
|
||||
flat-cache@6.1.20:
|
||||
dependencies:
|
||||
cacheable: 2.3.3
|
||||
flatted: 3.3.3
|
||||
hookified: 1.15.1
|
||||
|
||||
flatted@3.3.3: {}
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
fontverter@2.0.0:
|
||||
dependencies:
|
||||
wawoff2: 2.0.1
|
||||
woff2sfnt-sfnt2woff: 1.0.0
|
||||
|
||||
form-data@4.0.5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
@@ -4134,6 +4266,8 @@ snapshots:
|
||||
|
||||
hachure-fill@0.5.2: {}
|
||||
|
||||
harfbuzzjs@0.4.15: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
@@ -4142,6 +4276,10 @@ snapshots:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hashery@1.5.0:
|
||||
dependencies:
|
||||
hookified: 1.15.1
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@@ -4168,6 +4306,8 @@ snapshots:
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
hookified@1.15.1: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
@@ -4234,6 +4374,10 @@ snapshots:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
keyv@5.6.0:
|
||||
dependencies:
|
||||
'@keyv/serialize': 1.1.1
|
||||
|
||||
khroma@2.1.0: {}
|
||||
|
||||
langium@3.3.1:
|
||||
@@ -4454,6 +4598,8 @@ snapshots:
|
||||
|
||||
package-manager-detector@1.6.0: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -4633,6 +4779,12 @@ snapshots:
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
qified@0.6.0:
|
||||
dependencies:
|
||||
hookified: 1.15.1
|
||||
|
||||
querystring@0.2.1: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
@@ -4649,7 +4801,7 @@ snapshots:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
remixicon@3.5.0: {}
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
@@ -4787,6 +4939,13 @@ snapshots:
|
||||
|
||||
stylis@4.3.6: {}
|
||||
|
||||
subset-font@2.4.0:
|
||||
dependencies:
|
||||
fontverter: 2.0.0
|
||||
harfbuzzjs: 0.4.15
|
||||
lodash: 4.17.23
|
||||
p-limit: 3.1.0
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
@@ -4944,6 +5103,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-webfont-dl@3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)):
|
||||
dependencies:
|
||||
axios: 1.13.5
|
||||
clean-css: 5.3.3
|
||||
flat-cache: 6.1.20
|
||||
picocolors: 1.1.1
|
||||
vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
@@ -5042,10 +5211,57 @@ snapshots:
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
watchpack@2.5.1:
|
||||
dependencies:
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
wawoff2@2.0.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
webpack-sources@3.3.4: {}
|
||||
|
||||
webpack@5.105.0:
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
'@types/json-schema': 7.0.15
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@webassemblyjs/wasm-edit': 1.14.1
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.16.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
||||
browserslist: 4.28.1
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.20.0
|
||||
es-module-lexer: 2.0.0
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.11
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
loader-runner: 4.3.1
|
||||
mime-types: 2.1.35
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.0
|
||||
terser-webpack-plugin: 5.4.0(webpack@5.105.0)
|
||||
watchpack: 2.5.1
|
||||
webpack-sources: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
woff2sfnt-sfnt2woff@1.0.0:
|
||||
dependencies:
|
||||
pako: 1.0.11
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* subset-mdi-font.mjs
|
||||
*
|
||||
* Build script that:
|
||||
* 1. Scans src/ for all mdi-* icon names used in .vue/.ts files
|
||||
* 2. Resolves their Unicode codepoints from @mdi/font CSS
|
||||
* 3. Subsets the MDI font to include only those glyphs (via subset-font, pure JS)
|
||||
* 4. Generates a minimal CSS file with only the needed icon classes
|
||||
* 5. Outputs to src/assets/mdi-subset/
|
||||
*
|
||||
* Fallback: if any step fails, copies the original full @mdi/font CSS and fonts
|
||||
* so the build never breaks.
|
||||
*/
|
||||
import { readFileSync, writeFileSync, copyFileSync, readdirSync, statSync, existsSync, mkdirSync } from "fs";
|
||||
import { join, resolve, extname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// Derive __dirname portably from import.meta.url (works across all Node ESM versions)
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const ROOT = resolve(__dirname, "..");
|
||||
const SRC = join(ROOT, "src");
|
||||
const MDI_CSS_PATH = join(ROOT, "node_modules/@mdi/font/css/materialdesignicons.css");
|
||||
const MDI_TTF_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf");
|
||||
const MDI_WOFF2_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2");
|
||||
const MDI_WOFF_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff");
|
||||
const OUT_DIR = join(ROOT, "src/assets/mdi-subset");
|
||||
|
||||
// Utility classes that should not be treated as icon names
|
||||
const UTILITY_CLASSES = new Set([
|
||||
"mdi-set", "mdi-spin", "mdi-rotate-45", "mdi-rotate-90", "mdi-rotate-135",
|
||||
"mdi-rotate-180", "mdi-rotate-225", "mdi-rotate-270", "mdi-rotate-315",
|
||||
"mdi-flip-h", "mdi-flip-v", "mdi-light", "mdi-dark", "mdi-inactive",
|
||||
"mdi-18px", "mdi-24px", "mdi-36px", "mdi-48px",
|
||||
]);
|
||||
|
||||
// Regex to match individual icon class definitions in MDI CSS
|
||||
export const ICON_CLASS_PATTERN = /\.(mdi-[a-z][a-z0-9-]*)::before\s*\{\s*content:\s*"\\([0-9A-Fa-f]+)"\s*;?\s*}/g;
|
||||
|
||||
// ── Helper functions ────────────────────────────────────────────────────────
|
||||
|
||||
/** Recursively collect files with given extensions, skipping node_modules. */
|
||||
export function* collectFiles(dir, exts) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory() && entry.name !== "node_modules") {
|
||||
yield* collectFiles(full, exts);
|
||||
} else if (exts.includes(extname(entry.name))) {
|
||||
yield full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Scan source files and return a Set of used mdi-* icon names. */
|
||||
export function scanUsedIcons(sourceFiles) {
|
||||
const iconPattern = /mdi-[a-z][a-z0-9-]*/g;
|
||||
const usedIcons = new Set();
|
||||
for (const file of sourceFiles) {
|
||||
const content = readFileSync(file, "utf-8");
|
||||
for (const match of content.matchAll(iconPattern)) {
|
||||
if (!UTILITY_CLASSES.has(match[0])) {
|
||||
usedIcons.add(match[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return usedIcons;
|
||||
}
|
||||
|
||||
/** Parse @mdi/font CSS and return a Map of icon-name → hex codepoint. */
|
||||
export function parseIconCodepoints(mdiCSS) {
|
||||
const iconMap = new Map();
|
||||
for (const match of mdiCSS.matchAll(ICON_CLASS_PATTERN)) {
|
||||
iconMap.set(match[1], match[2]);
|
||||
}
|
||||
return iconMap;
|
||||
}
|
||||
|
||||
/** Resolve used icons against the codepoint map, returning resolved/missing/subsetChars. */
|
||||
export function resolveUsedIcons(usedIcons, iconMap) {
|
||||
const resolvedIcons = [];
|
||||
const missingIcons = [];
|
||||
const subsetChars = [];
|
||||
for (const icon of usedIcons) {
|
||||
const cp = iconMap.get(icon);
|
||||
if (cp) {
|
||||
resolvedIcons.push(icon);
|
||||
subsetChars.push(String.fromCodePoint(parseInt(cp, 16)));
|
||||
} else {
|
||||
missingIcons.push(icon);
|
||||
}
|
||||
}
|
||||
return { resolvedIcons, missingIcons, subsetChars };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract utility CSS rules (size, rotation, flip, spin, etc.) from the original MDI CSS.
|
||||
* Uses a subtraction approach: removes the parts we regenerate (icon definitions,
|
||||
* @font-face, base .mdi rules) and keeps everything else. This is more robust than
|
||||
* relying on a fixed start marker, as it tolerates CSS reordering in future versions.
|
||||
*/
|
||||
export function extractUtilityCss(mdiCSS, iconClassPattern) {
|
||||
let utilityCss = mdiCSS
|
||||
.replace(iconClassPattern, "") // Remove icon definitions
|
||||
.replace(/@font-face\s*\{[\s\S]*?}/g, "") // Remove @font-face
|
||||
.replace(/\.mdi:before,\s*\.mdi-set\s*\{[\s\S]*?}/g, "") // Remove base rules
|
||||
.replace(/\/\*# sourceMappingURL=.*\*\//, "") // Remove source map
|
||||
.trim();
|
||||
|
||||
// Clean up excess blank lines left after removals
|
||||
utilityCss = utilityCss.replace(/(\r\n|\n){3,}/g, "\n\n");
|
||||
|
||||
return utilityCss;
|
||||
}
|
||||
|
||||
/** Build a fallback CSS that rewrites font URLs to use subset filenames. */
|
||||
function buildFallbackCss() {
|
||||
const mdiCSS = readFileSync(MDI_CSS_PATH, "utf-8");
|
||||
return mdiCSS
|
||||
// Rewrite woff/woff2 URLs to point at subset filenames
|
||||
.replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(woff2?)\?[^"]*"\)/g,
|
||||
(_, ext) => `url("./materialdesignicons-webfont-subset.${ext}")`)
|
||||
// Remove legacy eot/ttf sources
|
||||
.replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(eot|ttf)\?[^"]*"\)[^,]*/g, "")
|
||||
// Clean up dangling commas/separators
|
||||
.replace(/src:\s*,/g, "src:")
|
||||
.replace(/,\s*;/g, ";");
|
||||
}
|
||||
|
||||
// ── Fallback: copy original full MDI font if subsetting fails ───────────────
|
||||
function fallbackToFullFont(reason) {
|
||||
console.warn(`\n⚠️ Subsetting failed: ${reason}`);
|
||||
console.warn(`⚠️ Falling back to full @mdi/font (build will not break)\n`);
|
||||
|
||||
// Copy original font files
|
||||
if (existsSync(MDI_WOFF2_PATH)) {
|
||||
copyFileSync(MDI_WOFF2_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff2"));
|
||||
}
|
||||
if (existsSync(MDI_WOFF_PATH)) {
|
||||
copyFileSync(MDI_WOFF_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff"));
|
||||
}
|
||||
|
||||
writeFileSync(join(OUT_DIR, "materialdesignicons-subset.css"), buildFallbackCss());
|
||||
|
||||
const size = existsSync(MDI_WOFF2_PATH) ? statSync(MDI_WOFF2_PATH).size : 0;
|
||||
console.warn(`⚠️ Fallback complete: using full font (${(size / 1024).toFixed(1)} KB woff2)`);
|
||||
}
|
||||
|
||||
// ── Exported entry point ────────────────────────────────────────────────────
|
||||
|
||||
export async function runMdiSubset() {
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
try {
|
||||
// Pre-checks
|
||||
if (!existsSync(MDI_CSS_PATH)) {
|
||||
throw new Error(`@mdi/font CSS not found at ${MDI_CSS_PATH}. Run 'pnpm install' first.`);
|
||||
}
|
||||
if (!existsSync(MDI_TTF_PATH)) {
|
||||
throw new Error(`@mdi/font TTF not found at ${MDI_TTF_PATH}. Run 'pnpm install' first.`);
|
||||
}
|
||||
|
||||
// Dynamic import subset-font (may not be installed in all environments)
|
||||
let subsetFont;
|
||||
try {
|
||||
subsetFont = (await import("subset-font")).default;
|
||||
} catch (e) {
|
||||
throw new Error(`subset-font package not available: ${e.message}. Run 'pnpm install' first.`);
|
||||
}
|
||||
|
||||
// Step 1: Scan source files for mdi-* icon names
|
||||
const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]);
|
||||
const usedIcons = scanUsedIcons(sourceFiles);
|
||||
if (usedIcons.size === 0) {
|
||||
throw new Error("No mdi-* icons found in source files. Something is wrong with scanning.");
|
||||
}
|
||||
console.log(`✅ Found ${usedIcons.size} unique mdi-* icons in source files`);
|
||||
|
||||
// Step 2: Parse @mdi/font CSS to get codepoints for each icon
|
||||
const mdiCSS = readFileSync(MDI_CSS_PATH, "utf-8");
|
||||
const iconMap = parseIconCodepoints(mdiCSS);
|
||||
if (iconMap.size === 0) {
|
||||
throw new Error("Could not parse any icon definitions from @mdi/font CSS. Format may have changed.");
|
||||
}
|
||||
console.log(`📦 MDI font CSS contains ${iconMap.size} icon definitions`);
|
||||
|
||||
// Step 3: Resolve codepoints for used icons
|
||||
const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap);
|
||||
if (missingIcons.length > 0) {
|
||||
console.warn(`⚠️ ${missingIcons.length} icons not found in MDI CSS:`, missingIcons.join(", "));
|
||||
}
|
||||
if (resolvedIcons.length === 0) {
|
||||
throw new Error("No icon codepoints could be resolved. Icon name format may have changed.");
|
||||
}
|
||||
console.log(`🔍 Resolved ${resolvedIcons.length} codepoints for subsetting`);
|
||||
|
||||
// Add space character
|
||||
subsetChars.push(" ");
|
||||
const subsetText = subsetChars.join("");
|
||||
|
||||
// Step 4: Subset font with subset-font (pure JS/WASM)
|
||||
const fontBuffer = readFileSync(MDI_TTF_PATH);
|
||||
|
||||
console.log(`🔧 Subsetting font to woff2...`);
|
||||
const woff2Buffer = await subsetFont(fontBuffer, subsetText, {
|
||||
targetFormat: "woff2",
|
||||
});
|
||||
|
||||
console.log(`🔧 Subsetting font to woff...`);
|
||||
const woffBuffer = await subsetFont(fontBuffer, subsetText, {
|
||||
targetFormat: "woff",
|
||||
});
|
||||
|
||||
if (woff2Buffer.length === 0 || woffBuffer.length === 0) {
|
||||
throw new Error("subset-font produced empty output. Font file may be corrupted.");
|
||||
}
|
||||
|
||||
const outWoff2 = join(OUT_DIR, "materialdesignicons-webfont-subset.woff2");
|
||||
const outWoff = join(OUT_DIR, "materialdesignicons-webfont-subset.woff");
|
||||
writeFileSync(outWoff2, woff2Buffer);
|
||||
writeFileSync(outWoff, woffBuffer);
|
||||
|
||||
// Step 5: Generate subset CSS
|
||||
let css = `/* Auto-generated MDI subset – ${resolvedIcons.length} icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
font-family: "Material Design Icons";
|
||||
src: url("./materialdesignicons-webfont-subset.woff2") format("woff2"),
|
||||
url("./materialdesignicons-webfont-subset.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.mdi:before,
|
||||
.mdi-set {
|
||||
display: inline-block;
|
||||
font: normal normal normal 24px/1 "Material Design Icons";
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
line-height: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
for (const icon of resolvedIcons.sort()) {
|
||||
const cp = iconMap.get(icon);
|
||||
css += `.${icon}::before {\n content: "\\${cp}";\n}\n\n`;
|
||||
}
|
||||
|
||||
const utilityCss = extractUtilityCss(mdiCSS, ICON_CLASS_PATTERN);
|
||||
if (utilityCss) {
|
||||
css += `/* Utility classes (extracted from @mdi/font) */\n${utilityCss}\n`;
|
||||
} else {
|
||||
console.warn("⚠️ Could not find MDI utility classes in original CSS, skipping");
|
||||
}
|
||||
|
||||
const outCSS = join(OUT_DIR, "materialdesignicons-subset.css");
|
||||
writeFileSync(outCSS, css);
|
||||
|
||||
// Report
|
||||
const origSize = statSync(MDI_TTF_PATH).size;
|
||||
const subsetWoff2Size = woff2Buffer.length;
|
||||
console.log(`\n📊 Results:`);
|
||||
console.log(` Original TTF font: ${(origSize / 1024).toFixed(1)} KB`);
|
||||
console.log(` Subset WOFF2: ${(subsetWoff2Size / 1024).toFixed(1)} KB`);
|
||||
console.log(` Reduction: ${((1 - subsetWoff2Size / origSize) * 100).toFixed(1)}%`);
|
||||
console.log(` Icons included: ${resolvedIcons.length}`);
|
||||
console.log(` CSS file: ${outCSS}`);
|
||||
console.log(`\n✅ MDI font subset generated successfully!`);
|
||||
|
||||
} catch (err) {
|
||||
// Fallback: any failure → use original full font so build never breaks
|
||||
try {
|
||||
fallbackToFullFont(err.message);
|
||||
} catch (fallbackErr) {
|
||||
console.error(`❌ Fallback also failed: ${fallbackErr.message}`);
|
||||
console.error(`❌ Please ensure @mdi/font is installed: pnpm install`);
|
||||
throw fallbackErr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── CLI entry point: allows running directly via `node scripts/subset-mdi-font.mjs` ──
|
||||
|
||||
if (import.meta.url.startsWith('file:') && process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
runMdiSubset().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
import { createVuetify } from "vuetify";
|
||||
import "@mdi/font/css/materialdesignicons.css";
|
||||
import * as components from "vuetify/components";
|
||||
import * as directives from "vuetify/directives";
|
||||
import { PurpleTheme } from "@/theme/LightTheme";
|
||||
import { createVuetify } from 'vuetify';
|
||||
import '@/assets/mdi-subset/materialdesignicons-subset.css';
|
||||
import * as components from 'vuetify/components';
|
||||
import * as directives from 'vuetify/directives';
|
||||
import { PurpleTheme } from '@/theme/LightTheme';
|
||||
import { PurpleThemeDark } from "@/theme/DarkTheme";
|
||||
import { LIGHT_THEME_NAME, DARK_THEME_NAME } from "@/theme/constants";
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
import {
|
||||
collectFiles,
|
||||
scanUsedIcons,
|
||||
parseIconCodepoints,
|
||||
resolveUsedIcons,
|
||||
extractUtilityCss,
|
||||
ICON_CLASS_PATTERN,
|
||||
} from '../scripts/subset-mdi-font.mjs';
|
||||
|
||||
// ── Helper: create a temporary directory tree for file-system tests ─────────
|
||||
|
||||
function makeTmpDir() {
|
||||
const base = join(tmpdir(), `mdi-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
mkdirSync(base, { recursive: true });
|
||||
return base;
|
||||
}
|
||||
|
||||
// ── collectFiles ────────────────────────────────────────────────────────────
|
||||
|
||||
test('collectFiles yields files matching given extensions', () => {
|
||||
const tmp = makeTmpDir();
|
||||
writeFileSync(join(tmp, 'a.vue'), '');
|
||||
writeFileSync(join(tmp, 'b.ts'), '');
|
||||
writeFileSync(join(tmp, 'c.txt'), '');
|
||||
|
||||
const files = [...collectFiles(tmp, ['.vue', '.ts'])];
|
||||
assert.equal(files.length, 2);
|
||||
assert.ok(files.some(f => f.endsWith('a.vue')));
|
||||
assert.ok(files.some(f => f.endsWith('b.ts')));
|
||||
|
||||
rmSync(tmp, { recursive: true });
|
||||
});
|
||||
|
||||
test('collectFiles recurses into subdirectories', () => {
|
||||
const tmp = makeTmpDir();
|
||||
const sub = join(tmp, 'sub');
|
||||
mkdirSync(sub);
|
||||
writeFileSync(join(sub, 'deep.vue'), '');
|
||||
|
||||
const files = [...collectFiles(tmp, ['.vue'])];
|
||||
assert.equal(files.length, 1);
|
||||
assert.ok(files[0].endsWith('deep.vue'));
|
||||
|
||||
rmSync(tmp, { recursive: true });
|
||||
});
|
||||
|
||||
test('collectFiles skips node_modules directories', () => {
|
||||
const tmp = makeTmpDir();
|
||||
const nm = join(tmp, 'node_modules');
|
||||
mkdirSync(nm);
|
||||
writeFileSync(join(nm, 'pkg.vue'), '');
|
||||
writeFileSync(join(tmp, 'app.vue'), '');
|
||||
|
||||
const files = [...collectFiles(tmp, ['.vue'])];
|
||||
assert.equal(files.length, 1);
|
||||
assert.ok(files[0].endsWith('app.vue'));
|
||||
|
||||
rmSync(tmp, { recursive: true });
|
||||
});
|
||||
|
||||
test('collectFiles yields nothing for empty directory', () => {
|
||||
const tmp = makeTmpDir();
|
||||
const files = [...collectFiles(tmp, ['.vue'])];
|
||||
assert.equal(files.length, 0);
|
||||
|
||||
rmSync(tmp, { recursive: true });
|
||||
});
|
||||
|
||||
// ── scanUsedIcons ───────────────────────────────────────────────────────────
|
||||
|
||||
test('scanUsedIcons extracts mdi-* icon names from files', () => {
|
||||
const tmp = makeTmpDir();
|
||||
writeFileSync(join(tmp, 'A.vue'), '<v-icon>mdi-home</v-icon><v-icon>mdi-close</v-icon>');
|
||||
writeFileSync(join(tmp, 'B.vue'), 'icon="mdi-home"');
|
||||
|
||||
const icons = scanUsedIcons(collectFiles(tmp, ['.vue']));
|
||||
assert.ok(icons instanceof Set);
|
||||
assert.ok(icons.has('mdi-home'));
|
||||
assert.ok(icons.has('mdi-close'));
|
||||
assert.equal(icons.size, 2); // mdi-home deduplicated
|
||||
|
||||
rmSync(tmp, { recursive: true });
|
||||
});
|
||||
|
||||
test('scanUsedIcons excludes utility classes', () => {
|
||||
const tmp = makeTmpDir();
|
||||
writeFileSync(join(tmp, 'A.vue'), 'mdi-spin mdi-rotate-90 mdi-flip-h mdi-home');
|
||||
|
||||
const icons = scanUsedIcons(collectFiles(tmp, ['.vue']));
|
||||
assert.ok(icons.has('mdi-home'));
|
||||
assert.ok(!icons.has('mdi-spin'));
|
||||
assert.ok(!icons.has('mdi-rotate-90'));
|
||||
assert.ok(!icons.has('mdi-flip-h'));
|
||||
|
||||
rmSync(tmp, { recursive: true });
|
||||
});
|
||||
|
||||
test('scanUsedIcons returns empty set when no icons found', () => {
|
||||
const tmp = makeTmpDir();
|
||||
writeFileSync(join(tmp, 'A.vue'), '<div>Hello</div>');
|
||||
|
||||
const icons = scanUsedIcons(collectFiles(tmp, ['.vue']));
|
||||
assert.equal(icons.size, 0);
|
||||
|
||||
rmSync(tmp, { recursive: true });
|
||||
});
|
||||
|
||||
// ── parseIconCodepoints ─────────────────────────────────────────────────────
|
||||
|
||||
test('parseIconCodepoints parses icon definitions from CSS', () => {
|
||||
const css = `
|
||||
.mdi-home::before { content: "\\F02DC"; }
|
||||
.mdi-close::before { content: "\\F0156"; }
|
||||
`;
|
||||
const map = parseIconCodepoints(css);
|
||||
assert.equal(map.size, 2);
|
||||
assert.equal(map.get('mdi-home'), 'F02DC');
|
||||
assert.equal(map.get('mdi-close'), 'F0156');
|
||||
});
|
||||
|
||||
test('parseIconCodepoints handles CSS with semicolons inside braces', () => {
|
||||
const css = `.mdi-check::before { content: "\\F012C"; }`;
|
||||
const map = parseIconCodepoints(css);
|
||||
assert.equal(map.get('mdi-check'), 'F012C');
|
||||
});
|
||||
|
||||
test('parseIconCodepoints returns empty map for non-matching CSS', () => {
|
||||
const css = `.some-other-class { color: red; }`;
|
||||
const map = parseIconCodepoints(css);
|
||||
assert.equal(map.size, 0);
|
||||
});
|
||||
|
||||
// ── resolveUsedIcons ────────────────────────────────────────────────────────
|
||||
|
||||
test('resolveUsedIcons separates resolved and missing icons', () => {
|
||||
const usedIcons = new Set(['mdi-home', 'mdi-close', 'mdi-nonexistent']);
|
||||
const iconMap = new Map([
|
||||
['mdi-home', 'F02DC'],
|
||||
['mdi-close', 'F0156'],
|
||||
]);
|
||||
|
||||
const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap);
|
||||
|
||||
assert.ok(resolvedIcons.includes('mdi-home'));
|
||||
assert.ok(resolvedIcons.includes('mdi-close'));
|
||||
assert.equal(resolvedIcons.length, 2);
|
||||
|
||||
assert.deepEqual(missingIcons, ['mdi-nonexistent']);
|
||||
|
||||
// Verify subsetChars contains correct Unicode characters
|
||||
assert.equal(subsetChars.length, 2);
|
||||
assert.equal(subsetChars[0], String.fromCodePoint(0xF02DC));
|
||||
assert.equal(subsetChars[1], String.fromCodePoint(0xF0156));
|
||||
});
|
||||
|
||||
test('resolveUsedIcons returns all missing when iconMap is empty', () => {
|
||||
const usedIcons = new Set(['mdi-home']);
|
||||
const iconMap = new Map();
|
||||
|
||||
const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap);
|
||||
assert.equal(resolvedIcons.length, 0);
|
||||
assert.deepEqual(missingIcons, ['mdi-home']);
|
||||
assert.equal(subsetChars.length, 0);
|
||||
});
|
||||
|
||||
// ── extractUtilityCss ───────────────────────────────────────────────────────
|
||||
|
||||
test('extractUtilityCss removes icon definitions and keeps utility rules', () => {
|
||||
const css = `
|
||||
@font-face {
|
||||
font-family: "Material Design Icons";
|
||||
src: url("../fonts/materialdesignicons-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
.mdi:before,
|
||||
.mdi-set {
|
||||
display: inline-block;
|
||||
font: normal normal normal 24px/1 "Material Design Icons";
|
||||
}
|
||||
|
||||
.mdi-home::before { content: "\\F02DC"; }
|
||||
.mdi-close::before { content: "\\F0156"; }
|
||||
|
||||
.mdi-spin:before {
|
||||
animation: mdi-spin 2s infinite linear;
|
||||
}
|
||||
|
||||
.mdi-18px.mdi-set, .mdi-18px.mdi:before {
|
||||
font-size: 18px;
|
||||
}
|
||||
/*# sourceMappingURL=materialdesignicons.css.map */
|
||||
`;
|
||||
|
||||
const result = extractUtilityCss(css, ICON_CLASS_PATTERN);
|
||||
|
||||
// Should NOT contain icon definitions
|
||||
assert.ok(!result.includes('mdi-home'));
|
||||
assert.ok(!result.includes('mdi-close'));
|
||||
|
||||
// Should NOT contain @font-face
|
||||
assert.ok(!result.includes('@font-face'));
|
||||
|
||||
// Should NOT contain base .mdi rules
|
||||
assert.ok(!result.includes('display: inline-block'));
|
||||
|
||||
// Should NOT contain source map
|
||||
assert.ok(!result.includes('sourceMappingURL'));
|
||||
|
||||
// SHOULD contain utility classes
|
||||
assert.ok(result.includes('mdi-spin'));
|
||||
assert.ok(result.includes('mdi-18px'));
|
||||
});
|
||||
|
||||
test('extractUtilityCss returns empty string when only icon defs exist', () => {
|
||||
const css = `
|
||||
@font-face { font-family: "MDI"; src: url("font.woff2"); }
|
||||
.mdi:before, .mdi-set { display: inline-block; }
|
||||
.mdi-home::before { content: "\\F02DC"; }
|
||||
`;
|
||||
|
||||
const result = extractUtilityCss(css, ICON_CLASS_PATTERN);
|
||||
assert.equal(result, '');
|
||||
});
|
||||
|
||||
test('extractUtilityCss handles empty CSS input', () => {
|
||||
const result = extractUtilityCss('', ICON_CLASS_PATTERN);
|
||||
assert.equal(result, '');
|
||||
});
|
||||
|
||||
// ── ICON_CLASS_PATTERN ──────────────────────────────────────────────────────
|
||||
|
||||
test('ICON_CLASS_PATTERN matches standard MDI icon definitions', () => {
|
||||
const css = `.mdi-home::before { content: "\\F02DC"; }`;
|
||||
const matches = [...css.matchAll(ICON_CLASS_PATTERN)];
|
||||
assert.equal(matches.length, 1);
|
||||
assert.equal(matches[0][1], 'mdi-home');
|
||||
assert.equal(matches[0][2], 'F02DC');
|
||||
});
|
||||
|
||||
test('ICON_CLASS_PATTERN does not match non-icon classes', () => {
|
||||
const css = `.some-class::before { content: "hello"; }`;
|
||||
const matches = [...css.matchAll(ICON_CLASS_PATTERN)];
|
||||
assert.equal(matches.length, 0);
|
||||
});
|
||||
+28
-11
@@ -1,11 +1,27 @@
|
||||
import { fileURLToPath, URL } from "url";
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vuetify from "vite-plugin-vuetify";
|
||||
import { fileURLToPath, URL } from 'url';
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vuetify from 'vite-plugin-vuetify';
|
||||
import webfontDl from 'vite-plugin-webfont-dl';
|
||||
// @ts-ignore — .mjs not in TS project scope; Vite resolves this at runtime
|
||||
import { runMdiSubset } from './scripts/subset-mdi-font.mjs';
|
||||
|
||||
// Vite plugin: run MDI icon font subsetting (build only)
|
||||
function mdiSubset() {
|
||||
return {
|
||||
name: 'vite-plugin-mdi-subset',
|
||||
async buildStart() {
|
||||
console.log('\n🔧 Running MDI icon font subsetting...');
|
||||
await runMdiSubset();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ command }) => ({
|
||||
plugins: [
|
||||
// Only run MDI subsetting during production builds, skip in dev server
|
||||
...(command === 'build' ? [mdiSubset()] : []),
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
@@ -14,8 +30,9 @@ export default defineConfig({
|
||||
},
|
||||
}),
|
||||
vuetify({
|
||||
autoImport: true,
|
||||
autoImport: true
|
||||
}),
|
||||
webfontDl()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -43,8 +60,8 @@ export default defineConfig({
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:6185/",
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -87,13 +87,7 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
text: "OneBot v11",
|
||||
base: "/platform/aiocqhttp",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "NapCat", link: "/napcat" },
|
||||
{ text: "Lagrange", link: "/lagrange" },
|
||||
{ text: "其他端", link: "/others" },
|
||||
],
|
||||
link: "/aiocqhttp"
|
||||
},
|
||||
{ text: "企微应用", link: "/wecom" },
|
||||
{ text: "企微智能机器人", link: "/wecom_ai_bot" },
|
||||
@@ -111,7 +105,7 @@ export default defineConfig({
|
||||
base: "/platform/satori",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "使用 LLOneBot", link: "/llonebot" },
|
||||
{ text: "接入 Satori", link: "/guide" },
|
||||
{ text: "使用 server-satori", link: "/server-satori" },
|
||||
],
|
||||
},
|
||||
@@ -327,13 +321,7 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
text: "OneBot v11",
|
||||
base: "/en/platform/aiocqhttp",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "NapCat", link: "/napcat" },
|
||||
{ text: "Lagrange", link: "/lagrange" },
|
||||
{ text: "Other Clients", link: "/others" },
|
||||
],
|
||||
link: "/aiocqhttp",
|
||||
},
|
||||
{ text: "WeCom Application", link: "/wecom" },
|
||||
{ text: "WeCom AI Bot", link: "/wecom_ai_bot" },
|
||||
@@ -350,7 +338,7 @@ export default defineConfig({
|
||||
base: "/en/platform/satori",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "Using LLOneBot", link: "/llonebot" },
|
||||
{ text: "Connect Satori", link: "/guide" },
|
||||
{ text: "Using server-satori", link: "/server-satori" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Connect OneBot v11 Protocol Implementations
|
||||
|
||||
OneBot is a standardized bot application interface designed to unify bot development across different chat platforms, so developers can write business logic once and use it on multiple platforms.
|
||||
|
||||
AstrBot supports all client implementations that implement OneBot v11 reverse WebSocket (AstrBot acts as the server).
|
||||
|
||||
Common OneBot v11 implementation projects are listed below:
|
||||
|
||||
- [NapCat](https://github.com/NapNeko/NapCatQQ)
|
||||
- [OneDisc](https://github.com/ITCraftDevelopmentTeam/OneDisc)
|
||||
- [Tele-KiraLink](https://github.com/Echomirix/Tele-KiraLink)
|
||||
|
||||
Please refer to each implementation project's deployment documentation.
|
||||
|
||||
## 1. Configure OneBot v11
|
||||
|
||||
1. Open AstrBot's WebUI
|
||||
2. Click `Bots` in the left sidebar
|
||||
3. In the right panel, click `+ Create Bot`
|
||||
4. Select `OneBot v11`
|
||||
|
||||
Fill in the form:
|
||||
|
||||
- ID (`id`): any value, used only to distinguish instances of different platforms.
|
||||
- Enable (`enable`): check it.
|
||||
- Reverse WebSocket host: fill your machine IP, usually `0.0.0.0`.
|
||||
- Reverse WebSocket port: choose any port, default is `6199`.
|
||||
- Reverse WebSocket token: fill this only when NapCat network configuration has a token set.
|
||||
|
||||
Click `Save`.
|
||||
|
||||
## 2. Configure the protocol implementation side
|
||||
|
||||
Please refer to each protocol implementation project's deployment documentation.
|
||||
|
||||
Notes:
|
||||
|
||||
1. The implementation must support `Reverse WebSocket`, with AstrBot acting as the server and the implementation client as the client.
|
||||
2. The reverse WebSocket URL is `ws(s)://<your-host>:6199/ws`.
|
||||
|
||||
## 3. Verify
|
||||
|
||||
Go to AstrBot WebUI `Console`. If a blue log appears saying `aiocqhttp(OneBot v11) adapter connected.`, the connection is successful.
|
||||
If after a few seconds you see `aiocqhttp adapter has been closed`, it means the connection timed out (failed). Please double-check your configuration.
|
||||
@@ -1,55 +0,0 @@
|
||||
# Connect to Lagrange
|
||||
|
||||
> [!TIP]
|
||||
> - Please control message frequency responsibly. Sending messages too frequently may trigger risk control.
|
||||
> - This project must not be used for illegal purposes.
|
||||
> - For the latest deployment steps, always refer to the official [Lagrange Docs](https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E4%B8%8B%E8%BD%BD%E5%AE%89%E8%A3%85).
|
||||
|
||||
## Download
|
||||
|
||||
Download the latest `Lagrange.OneBot` from [GitHub Releases](https://github.com/LagrangeDev/Lagrange.Core/releases).
|
||||
|
||||
- Windows: `Lagrange.OneBot_win-x64_xxxx`
|
||||
- Linux x86_64: `Lagrange.OneBot_linux-x64_xxx`
|
||||
- Linux ARM64: `Lagrange.OneBot_linux-arm64_xxx`
|
||||
- macOS Apple Silicon: `Lagrange.OneBot_osx-arm64_xxx`
|
||||
- macOS Intel: `Lagrange.OneBot_osx-x64_xxx`
|
||||
|
||||
## Deploy
|
||||
|
||||
Follow the official docs:
|
||||
|
||||
- Run guide: <https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E8%BF%90%E8%A1%8C>
|
||||
- Config file guide: <https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6>
|
||||
|
||||
In your config file, add this under `Implementations`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Type": "ReverseWebSocket",
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 6199,
|
||||
"Suffix": "/ws",
|
||||
"ReconnectInterval": 5000,
|
||||
"HeartBeatInterval": 5000,
|
||||
"AccessToken": ""
|
||||
}
|
||||
```
|
||||
|
||||
Make sure `Suffix` is exactly `/ws`.
|
||||
|
||||
## Connect to AstrBot
|
||||
|
||||
### Configure `aiocqhttp` Adapter
|
||||
|
||||
1. Open AstrBot Dashboard.
|
||||
2. Click `Bots` in the left sidebar.
|
||||
3. Click `+ Create Bot`.
|
||||
4. Select `aiocqhttp (OneBot v11)`.
|
||||
|
||||
Fill in:
|
||||
|
||||
- ID (`id`): any unique identifier.
|
||||
- Enable (`enable`): checked.
|
||||
- Reverse WebSocket host: your machine IP (usually `0.0.0.0`).
|
||||
- Reverse WebSocket port: an available port, for example `6199`.
|
||||
@@ -1,141 +0,0 @@
|
||||
# Using NapCat
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> - Please control usage frequency appropriately. Sending messages too frequently may be identified as abnormal behavior, increasing the risk of triggering risk control mechanisms.
|
||||
> - This project is strictly prohibited from being used for any purpose that violates laws and regulations. If you intend to use AstrBot for illegal industries or activities, we **explicitly oppose and refuse** your use of this project.
|
||||
> - AstrBot connects to the OneBot v11 protocol through the `aiocqhttp` adapter. OneBot v11 protocol is an open communication protocol and does not represent any specific software or service.
|
||||
|
||||
NapCat's GitHub Repository: [NapCat](https://github.com/NapNeko/NapCatQQ)
|
||||
NapCat's Documentation: [NapCat Documentation](https://napcat.napneko.icu/)
|
||||
|
||||
NapCat provides multiple deployment methods, including Docker, Windows one-click installation packages, and more.
|
||||
|
||||
## Deploy via One-Click Script
|
||||
|
||||
This deployment method is recommended.
|
||||
|
||||
### Windows
|
||||
|
||||
Refer to this article: [NapCat.Shell - Windows Manual Start Tutorial](https://napneko.github.io/guide/boot/Shell#napcat-shell-win%E6%89%8B%E5%8A%A8%E5%90%AF%E5%8A%A8%E6%95%99%E7%A8%8B)
|
||||
|
||||
### Linux
|
||||
|
||||
Refer to this article: [NapCat.Installer - Linux One-Click Script (Supports Ubuntu 20+/Debian 10+/Centos9)](https://napneko.github.io/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)
|
||||
|
||||
> [!TIP]
|
||||
> **Where to open Napcat WebUI**:
|
||||
> The WebUI link will be displayed in napcat's logs.
|
||||
>
|
||||
> If napcat is deployed via Linux command line one-click deployment: `docker log <account>`.
|
||||
>
|
||||
> For Docker-deployed NapCat: `docker logs napcat`.
|
||||
|
||||
## Deploy via Docker Compose
|
||||
|
||||
> [!TIP]
|
||||
> If deploying with Docker Compose, no configuration is needed on the NapCat side. Just log in via NapCat WebUI (running on port 6099) or `docker logs napcat`, enable the aiocqhttp adapter on the AstrBot side to connect, and you can directly implement normal receiving and sending of `voice data` and `file data`.
|
||||
|
||||
1. Download or copy the content of [astrbot.yml](https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml)
|
||||
2. Rename the downloaded file to `astrbot.yml`
|
||||
3. Modify `astrbot.yml`, change `#- "6199:6199` to `- "6199:6199"`, remove the flag of "#"
|
||||
4. Execute in the directory where the `astrbot.yml` file is located:
|
||||
|
||||
```bash
|
||||
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose -f ./astrbot.yml up -d
|
||||
```
|
||||
|
||||
## Deploy via Docker
|
||||
|
||||
> [!TIP]
|
||||
> If deploying with Docker, you will not be able to properly receive `voice data` and `file data`. This means voice-to-text and sandbox file input functions will not be available. You can receive text messages, image messages, and other types of messages.
|
||||
|
||||
This tutorial assumes you have Docker installed.
|
||||
|
||||
Execute the following command in the terminal for one-click deployment.
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-e NAPCAT_GID=$(id -g) \
|
||||
-e NAPCAT_UID=$(id -u) \
|
||||
-p 3000:3000 \
|
||||
-p 3001:3001 \
|
||||
-p 6099:6099 \
|
||||
--name napcat \
|
||||
--restart=always \
|
||||
mlikiowa/napcat-docker:latest
|
||||
```
|
||||
|
||||
After successful execution, you need to check the logs to get the login QR code and the management panel URL.
|
||||
|
||||
```bash
|
||||
docker logs napcat
|
||||
```
|
||||
|
||||
Please copy the management panel URL and open it in your browser.
|
||||
|
||||
Then use the account you want to log in with to scan the QR code that appears.
|
||||
|
||||
If there are no issues during the login stage, deployment is successful.
|
||||
|
||||
## Connect to AstrBot
|
||||
|
||||
## Configure aiocqhttp in AstrBot
|
||||
|
||||
1. Enter AstrBot's management panel
|
||||
2. Click `Bots` in the left sidebar
|
||||
3. Then in the interface on the right, click `+ Create Bot`
|
||||
4. Select `OneBot v11`
|
||||
|
||||
Fill in the configuration items that appear:
|
||||
- ID(id): Fill in arbitrarily, only used to distinguish different messaging platform instances.
|
||||
- Enable: Check this.
|
||||
- Reverse WebSocket Host Address: Please fill in your machine's IP address, generally fill in `0.0.0.0` directly
|
||||
- Reverse WebSocket Port: Fill in a port, default is `6199`.
|
||||
- Reverse Websocket Token: Only needs to be filled when a token is configured in NapCat's network settings.
|
||||
|
||||
Example image: (At the fastest, just check Enable, then save)
|
||||
|
||||
<img width="818" height="799" alt="xinjianya" src="https://github.com/user-attachments/assets/813ac338-2fd7-4add-bde4-8b0f6d0bda95" />
|
||||
|
||||
|
||||
Click `Save`.
|
||||
|
||||
### Configure Administrator
|
||||
|
||||
After filling in, go to the `Configuration File` page, click the `Platform Configuration` tab, find `Administrator ID`, and fill in your account number (not the bot's account number).
|
||||
|
||||
Remember to click `Save` in the lower right corner, AstrBot will restart and apply the configuration.
|
||||
|
||||
### Add WebSocket Client in NapCat
|
||||
|
||||
Switch back to NapCat's management panel, click `Network Configuration->New->WebSockets Client`.
|
||||
|
||||
<img width="649" height="751" alt="jiaochenXJY" src="https://github.com/user-attachments/assets/5044f96a-a81f-407a-a3b1-0c518499eda4" />
|
||||
|
||||
|
||||
In the newly opened window:
|
||||
|
||||
- Check `Enable`.
|
||||
- Fill in `URL` with `ws://HostIP:Port/ws`. For example, `ws://localhost:6199/ws` or `ws://127.0.0.1:6199/ws`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 1. If deploying with Docker and both AstrBot and NapCat containers are connected to the same network, use `ws://astrbot:6199/ws` (refer to the Docker script in this documentation).
|
||||
> 2. Due to Docker network isolation, when not on the same network, please use the internal network IP address or public network IP address ***(unsafe)*** to connect, i.e., `ws://(internal/public IP):6199/ws`.
|
||||
|
||||
- Message Format: `Array`
|
||||
- Heartbeat Interval: `5000`
|
||||
- Reconnection Interval: `5000`
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> 1. Remember to add `/ws` at the end!
|
||||
> 2. The IP here cannot be `0.0.0.0`
|
||||
|
||||
Click `Save`.
|
||||
|
||||
Go to AstrBot WebUI `Console`, if you see the blue log ` aiocqhttp(OneBot v11) adapter connected.`, it means the connection is successful. If not, and after several seconds ` aiocqhttp adapter has been closed` appears, it indicates connection timeout (failed), please check if the configuration is correct.
|
||||
|
||||
## 🎉 All Done
|
||||
|
||||
At this point, your AstrBot and NapCat should be successfully connected! Use `private message` to send `/help` to the bot to check if the connection is successful.
|
||||
@@ -1 +0,0 @@
|
||||
AstrBot can connect to any bot protocol client that supports OneBot v11 reverse WebSocket (AstrBot acts as the server side).
|
||||
@@ -0,0 +1,32 @@
|
||||
# Connect to Satori Protocol
|
||||
|
||||
## Satori protocol overview
|
||||
|
||||
> Excerpt from: https://satori.chat/introduction.html
|
||||
|
||||
Satori is a unified chat protocol. It aims to reduce differences between chat platforms and let developers build cross-platform, extensible, high-performance chat applications with lower cost.
|
||||
|
||||
The protocol is named after [Komeiji Satori](https://satori.js.org) in Touhou Project. The idea is that Satori can serve as a bridge between chat platforms, as Komeiji Satori communicates telepathically.
|
||||
|
||||
The development team behind Satori has long worked on bot development and is familiar with the communication patterns of many platforms. After about 4 years, Satori now has a mature design and implementation. The official project currently provides adapters for more than 15 platforms, covering major messaging services worldwide such as QQ, Discord, WeCom, KOOK, and others.
|
||||
|
||||
## 1. Configure the protocol server side
|
||||
|
||||
Please refer to the deployment documentation of the chosen implementation project.
|
||||
|
||||
## 2. Configure Satori protocol in AstrBot
|
||||
|
||||
1. Open AstrBot WebUI.
|
||||
2. Click `Bots` in the left sidebar.
|
||||
3. In the right panel, click `+ Create Bot`.
|
||||
4. Select `satori`.
|
||||
|
||||
Fill in the form:
|
||||
|
||||
- Bot ID (`id`): e.g. `satori` (any value is fine).
|
||||
- Enable (`enable`): check it.
|
||||
- Satori API base URL (`satori_api_base_url`): `http://localhost:5600/v1` (same port as the protocol implementation).
|
||||
- Satori WebSocket endpoint (`satori_endpoint`): `ws://localhost:5600/v1/events` (same port as the protocol implementation).
|
||||
- Satori token (`satori_token`): fill according to implementation settings.
|
||||
|
||||
Click `Save`.
|
||||
@@ -1,73 +0,0 @@
|
||||
# Connect LLTwoBot (Satori)
|
||||
|
||||
> [!TIP]
|
||||
> LLTwoBot is a multi-protocol implementation based on QQNT (OneBot v11 + Satori), allowing AstrBot to communicate with QQ via Satori.
|
||||
|
||||
> [!TIP]
|
||||
> - Please control message frequency responsibly.
|
||||
> - This project must not be used for illegal purposes.
|
||||
|
||||
## Preparation
|
||||
|
||||
First complete basic setup using official LLTwoBot documentation:
|
||||
|
||||
[LLTwoBot Docs](https://llonebot.com/guide/getting-started)
|
||||
|
||||
Make sure you have:
|
||||
|
||||
1. Installed LLTwoBot.
|
||||
2. Logged into a QQ account successfully.
|
||||
|
||||
## Configure Satori in LLTwoBot
|
||||
|
||||
After QQ login succeeds, open LLTwoBot WebUI:
|
||||
|
||||
> Default WebUI URL: <http://localhost:3080/>
|
||||
|
||||
In the WebUI sidebar, open the `Satori` tab and configure:
|
||||
|
||||
1. Enable Satori protocol.
|
||||
2. Port defaults to `5600`.
|
||||
3. Set Satori token if needed.
|
||||
4. Click Save.
|
||||
|
||||

|
||||
|
||||
## Configure Satori Adapter in AstrBot
|
||||
|
||||
1. Open AstrBot Dashboard.
|
||||
2. Click `Bots`.
|
||||
3. Click `+ Create Bot`.
|
||||
4. Select `satori`.
|
||||
|
||||
Fill in:
|
||||
|
||||
- Bot ID (`id`): `LLTwoBot`
|
||||
- Enable (`enable`): checked
|
||||
- Satori API endpoint (`satori_api_base_url`): `http://localhost:5600/v1`
|
||||
- Satori WebSocket endpoint (`satori_endpoint`): `ws://localhost:5600/v1/events`
|
||||
- Satori token (`satori_token`): from LLTwoBot config if set
|
||||
|
||||
> [!NOTE]
|
||||
> - LLTwoBot Satori service defaults to port `5600`.
|
||||
> - The complete API base path is `http://localhost:5600/v1`.
|
||||
> - If your Satori service runs on another port/path, adjust these values.
|
||||
|
||||

|
||||
|
||||
Click `Save`.
|
||||
|
||||
## Done
|
||||
|
||||
AstrBot should now be connected to LLTwoBot via Satori.
|
||||
|
||||
Send `/help` in QQ to verify.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If connection fails, check:
|
||||
|
||||
1. LLTwoBot is running.
|
||||
2. Satori service is enabled.
|
||||
3. Port/path are configured correctly.
|
||||
4. Token matches (if configured).
|
||||
@@ -0,0 +1,83 @@
|
||||
# 接入 OneBot v11 协议实现
|
||||
|
||||
OneBot 是一个**聊天机器人应用接口标准**,旨在统一不同聊天平台上的机器人应用开发接口。
|
||||
|
||||
AstrBot 支持接入所有适配了 OneBotv11 反向 Websockets(AstrBot 做服务器端)的机器人协议端。
|
||||
|
||||
下文给出一些常见的 OneBot v11 协议实现端项目。
|
||||
|
||||
- [NapCat](https://github.com/NapNeko/NapCatQQ) (连接到 QQ)
|
||||
- [OneDisc](https://github.com/ITCraftDevelopmentTeam/OneDisc) (连接到 Discord)
|
||||
- [Tele-KiraLink](https://github.com/Echomirix/Tele-KiraLink) (连接到 Telegram)
|
||||
|
||||
请参阅对应的协议实现端项目的部署文档。
|
||||
|
||||
对于 Napcat 项目,请参考下文的 `附录:部署 Napcat`
|
||||
|
||||
## 1. 配置 OneBot v11
|
||||
|
||||
1. 进入 AstrBot 的 WebUI
|
||||
2. 点击左边栏 `机器人`
|
||||
3. 然后在右边的界面中,点击 `+ 创建机器人`
|
||||
4. 选择 `OneBot v11`
|
||||
|
||||
在出现的表单中,填写:
|
||||
|
||||
- ID(id):随意填写,仅用于区分不同的消息平台实例。
|
||||
- 启用(enable): 勾选。
|
||||
- 反向 WebSocket 主机地址:请填写你的机器的 IP 地址,一般情况下请直接填写 `0.0.0.0`
|
||||
- 反向 WebSocket 端口:填写一个端口,默认为 `6199`。
|
||||
- 反向 Websocket Token:只有当 NapCat 网络配置中配置了 token 才需填写。
|
||||
|
||||
点击 `保存`。
|
||||
|
||||
## 2. 配置协议实现端
|
||||
|
||||
请参阅对应的协议实现端项目的部署文档。
|
||||
|
||||
一些注意点:
|
||||
|
||||
1. 协议实现端需要支持 `反向 WebSocket` 实现,及 AstrBot 端作为服务端,实现端作为客户端。
|
||||
2. `反向 WebSocket` 的 URL 为 `ws(s)://<your-host>:6199/ws`。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
前往 AstrBot WebUI `控制台`,如果出现 ` aiocqhttp(OneBot v11) 适配器已连接。` 蓝色的日志,说明连接成功。如果没有,若干秒后出现` aiocqhttp 适配器已被关闭` 则为连接超时(失败),请检查配置是否正确。
|
||||
|
||||
## 附录:部署 Napcat
|
||||
|
||||
### 通过一键启动脚本部署
|
||||
|
||||
推荐采用这种方式部署。
|
||||
|
||||
#### Windows
|
||||
|
||||
看这篇文章:[NapCat.Shell - Win手动启动教程](https://napneko.github.io/guide/boot/Shell#napcat-shell-win%E6%89%8B%E5%8A%A8%E5%90%AF%E5%8A%A8%E6%95%99%E7%A8%8B)
|
||||
|
||||
#### Linux
|
||||
|
||||
看这篇文章:[NapCat.Installer - Linux一键使用脚本(支持Ubuntu 20+/Debian 10+/Centos9)](https://napneko.github.io/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)
|
||||
|
||||
> [!TIP]
|
||||
> **Napcat WebUI 在哪打开**:
|
||||
> 在 napcat 的日志里会显示 WebUI 链接。
|
||||
>
|
||||
> 如果是 linux 命令行一键部署的napcat:`docker log <账号>`。
|
||||
>
|
||||
> Docker部署的 NapCat:`docker logs napcat`。
|
||||
|
||||
## 通过 Docker Compose 部署
|
||||
|
||||
1. 下载或复制 [astrbot.yml](https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml) 内容
|
||||
2. 将刚刚下载的文件重命名为 `astrbot.yml`
|
||||
3. 编辑 `astrbot.yml`,将 `# - "6199:6199"` 修改为 `- "6199:6199"`,移除开头的 `#`
|
||||
4. 在 `astrbot.yml` 文件所在目录执行:
|
||||
|
||||
```bash
|
||||
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose -f ./astrbot.yml up -d
|
||||
```
|
||||
|
||||
部署完毕之后,可以去 Napcat 的 WebUI(默认端口 6099)中新增 OneBot 连接实例:点击`网络配置->新建->WebSockets客户端`,在新弹出的窗口中:勾选`启用`,
|
||||
URL 填写 `ws://宿主机IP:端口/ws`。如 `ws://127.0.0.1:6199/ws`。如果采用上面的 Docker Compose 部署,可以填写 `ws://astrbot:6199/ws`(参考本文档的 Docker 脚本)。心跳间隔和重连间隔可以改为 `1000`(1 秒)。点击保存,然后去 AstrBot WebUI 的控制台中检查是否连接成功,出现 `aiocqhttp(OneBot v11) 适配器已连接` 日志即代表成功。
|
||||
|
||||
如果您对部署、网络配置不了解,请千万不要在公网暴露 Napcat 的端口。
|
||||
@@ -1,61 +0,0 @@
|
||||
# 接入 Lagrange
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> - 请合理控制使用频率。过于频繁地发送消息可能会被判定为异常行为,增加触发风控机制的风险。
|
||||
> - 本项目严禁用于任何违反法律法规的用途。若您意图将 AstrBot 应用于非法产业或活动,我们**明确反对并拒绝**您使用本项目。
|
||||
> - 最新的部署方式请以 [Lagrange Doc](https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E4%B8%8B%E8%BD%BD%E5%AE%89%E8%A3%85) 为准。
|
||||
|
||||
## 下载
|
||||
|
||||
从 [GitHub Release](https://github.com/LagrangeDev/Lagrange.Core/releases) 下载最新版的 `Lagrange.OneBot`。
|
||||
|
||||
对于 Windows 设备,请下载 `Lagrange.OneBot_win-x64_xxxx` 压缩包。
|
||||
|
||||
对于 X86 的 Linux 用户,下载 `Lagrange.OneBot_linux-x64_xxx` 压缩包。
|
||||
|
||||
对于 Arm 的 Linux 用户,下载 `Lagrange.OneBot_linux-arm64_xxx` 压缩包。
|
||||
|
||||
对于 M 芯片 Mac 用户,下载 `Lagrange.OneBot_osx-arm64_xxx` 压缩包。
|
||||
|
||||
对于 Intel 芯片 Mac 用户,下载 `Lagrange.OneBot_osx-x64_xxx` 压缩包。
|
||||
|
||||
## 部署
|
||||
|
||||
请参阅 [Lagrange Doc](https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E8%BF%90%E8%A1%8C)。
|
||||
|
||||
运行完成后,请修改 [配置文件](https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6),
|
||||
|
||||
在 `Implementations` 字段下添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"Type": "ReverseWebSocket",
|
||||
"Host": "127.0.0.1",
|
||||
"Port": 6199,
|
||||
"Suffix": "/ws",
|
||||
"ReconnectInterval": 5000,
|
||||
"HeartBeatInterval": 5000,
|
||||
"AccessToken": ""
|
||||
}
|
||||
```
|
||||
|
||||
一定要保证 `Suffix` 为 `/ws`。
|
||||
|
||||
## 连接到 AstrBot
|
||||
|
||||
### 配置 aiocqhttp
|
||||
|
||||
1. 进入 AstrBot 的管理面板
|
||||
2. 点击左边栏 `机器人`
|
||||
3. 然后在右边的界面中,点击 `+ 创建机器人`
|
||||
4. 选择 `aiocqhttp(OneBotv11)`
|
||||
|
||||
弹出的配置项填写:
|
||||
|
||||
配置项填写:
|
||||
|
||||
- ID(id):随意填写,用于区分不同的消息平台实例。
|
||||
- 启用(enable): 勾选。
|
||||
- 反向 WebSocket 主机地址:请填写你的机器的 IP 地址。一般情况下请直接填写 `0.0.0.0`
|
||||
- 反向 WebSocket 端口:填写一个端口,例如 `6199`。
|
||||
@@ -1,134 +0,0 @@
|
||||
# 使用 NapCat
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> - 本项目严禁用于任何违反法律法规的用途。若您意图将 AstrBot 应用于非法产业或活动,我们**明确反对并拒绝**您使用本项目。
|
||||
> - AstrBot 通过 `aiocqhttp` 适配器接入 OneBot v11 协议。OneBot v11 协议是一个开放的通信协议,并不代表任何具体的软件或服务。
|
||||
|
||||
NapCat 的 GitHub 仓库:[NapCat](https://github.com/NapNeko/NapCatQQ)
|
||||
NapCat 的文档:[NapCat 文档](https://napcat.napneko.icu/)
|
||||
|
||||
NapCat 提供了大量的部署方式,包括 Docker、Windows 一键安装包等等。
|
||||
|
||||
## 通过一键脚本部署
|
||||
|
||||
推荐采用这种方式部署。
|
||||
|
||||
### Windows
|
||||
|
||||
看这篇文章:[NapCat.Shell - Win手动启动教程](https://napneko.github.io/guide/boot/Shell#napcat-shell-win%E6%89%8B%E5%8A%A8%E5%90%AF%E5%8A%A8%E6%95%99%E7%A8%8B)
|
||||
|
||||
### Linux
|
||||
|
||||
看这篇文章:[NapCat.Installer - Linux一键使用脚本(支持Ubuntu 20+/Debian 10+/Centos9)](https://napneko.github.io/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)
|
||||
|
||||
> [!TIP]
|
||||
> **Napcat WebUI 在哪打开**:
|
||||
> 在 napcat 的日志里会显示 WebUI 链接。
|
||||
>
|
||||
> 如果是 linux 命令行一键部署的napcat:`docker log <账号>`。
|
||||
>
|
||||
> Docker部署的 NapCat:`docker logs napcat`。
|
||||
|
||||
## 通过 Docker Compose 部署
|
||||
|
||||
1. 下载或复制 [astrbot.yml](https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml) 内容
|
||||
2. 将刚刚下载的文件重命名为 `astrbot.yml`
|
||||
3. 编辑 `astrbot.yml`,将 `# - "6199:6199"` 修改为 `- "6199:6199"`,移除开头的 `#`
|
||||
4. 在 `astrbot.yml` 文件所在目录执行:
|
||||
|
||||
```bash
|
||||
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose -f ./astrbot.yml up -d
|
||||
```
|
||||
|
||||
## 通过 Docker 部署
|
||||
|
||||
此教程默认您安装了 Docker。
|
||||
|
||||
在终端执行以下命令即可一键部署。
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-e NAPCAT_GID=$(id -g) \
|
||||
-e NAPCAT_UID=$(id -u) \
|
||||
-p 3000:3000 \
|
||||
-p 3001:3001 \
|
||||
-p 6099:6099 \
|
||||
--name napcat \
|
||||
--restart=always \
|
||||
mlikiowa/napcat-docker:latest
|
||||
```
|
||||
|
||||
执行成功后,需要查看日志以得到登录二维码和管理面板的 URL。
|
||||
|
||||
```bash
|
||||
docker logs napcat
|
||||
```
|
||||
|
||||
请复制管理面板的 URL,然后在浏览器中打开备用。
|
||||
|
||||
然后使用你要登录的账号扫描出现的二维码,即可登录。
|
||||
|
||||
如果登录阶段没有出现问题,即成功部署。
|
||||
|
||||
## 连接到 AstrBot
|
||||
|
||||
## 在 AstrBot 配置 aiocqhttp
|
||||
|
||||
1. 进入 AstrBot 的管理面板
|
||||
2. 点击左边栏 `机器人`
|
||||
3. 然后在右边的界面中,点击 `+ 创建机器人`
|
||||
4. 选择 `OneBot v11`
|
||||
|
||||
弹出的配置项填写:
|
||||
- ID(id):随意填写,仅用于区分不同的消息平台实例。
|
||||
- 启用(enable): 勾选。
|
||||
- 反向 WebSocket 主机地址:请填写你的机器的 IP 地址,一般情况下请直接填写 `0.0.0.0`
|
||||
- 反向 WebSocket 端口:填写一个端口,默认为 `6199`。
|
||||
- 反向 Websocket Token:只有当 NapCat 网络配置中配置了 token 才需填写。
|
||||
|
||||
图例:(最快只需要点击启用,然后保存即可)
|
||||
|
||||
<img width="818" height="799" alt="xinjianya" src="https://github.com/user-attachments/assets/813ac338-2fd7-4add-bde4-8b0f6d0bda95" />
|
||||
|
||||
|
||||
点击 `保存`。
|
||||
|
||||
### 配置管理员
|
||||
|
||||
填写完毕后,进入 `配置文件` 页,点击 `平台配置` 选项卡,找到 `管理员 ID`,填写你的账号(不是机器人的账号)。
|
||||
|
||||
切记点击右下角 `保存`,AstrBot 重启并会应用配置。
|
||||
|
||||
### 在 NapCat 中添加 WebSocket 客户端
|
||||
|
||||
切换回 NapCat 的管理面板,点击 `网络配置->新建->WebSockets客户端`。
|
||||
|
||||
<img width="649" height="751" alt="jiaochenXJY" src="https://github.com/user-attachments/assets/5044f96a-a81f-407a-a3b1-0c518499eda4" />
|
||||
|
||||
|
||||
在新弹出的窗口中:
|
||||
|
||||
- 勾选 `启用`。
|
||||
- `URL` 填写 `ws://宿主机IP:端口/ws`。如 `ws://localhost:6199/ws` 或 `ws://127.0.0.1:6199/ws`。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 1. 如果采用 Docker 部署并同时把 AstrBot 和 NapCat 两个容器接入了同一网络,`ws://astrbot:6199/ws`(参考本文档的 Docker 脚本)。
|
||||
> 2. 由于 Docker 网络隔离的原因,不在同一个网络时请使用内网 IP 地址或公网 IP 地址 ***(不安全)*** 进行连接,即 `ws://(内网/公网):6199/ws`。
|
||||
|
||||
- 消息格式:`Array`
|
||||
- 心跳间隔: `5000`
|
||||
- 重连间隔: `5000`
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> 1. 切记后面加一个 `/ws`!
|
||||
> 2. 这里的 IP 不能填为 `0.0.0.0`
|
||||
|
||||
点击 `保存`。
|
||||
|
||||
前往 AstrBot WebUI `控制台`,如果出现 ` aiocqhttp(OneBot v11) 适配器已连接。` 蓝色的日志,说明连接成功。如果没有,若干秒后出现` aiocqhttp 适配器已被关闭` 则为连接超时(失败),请检查配置是否正确。
|
||||
|
||||
## 🎉 大功告成
|
||||
|
||||
此时,你的 AstrBot 和 NapCat 应该已经连接成功!使用 `私聊` 的方式对机器人发送 `/help` 以检查是否连接成功。
|
||||
@@ -1 +0,0 @@
|
||||
支持接入所有适配了 OneBotv11 反向 Websockets(AstrBot 做服务器端) 的机器人协议端。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 接入 Satori 协议
|
||||
|
||||
## Satori 协议简介
|
||||
|
||||
> 摘录自:https://satori.chat/zh-CN/introduction.html
|
||||
|
||||
Satori 是一个通用的聊天协议。Satori 协议希望能够抹平不同聊天平台之间的差异,让开发者以更低的成本开发出跨平台、可扩展、高性能的聊天应用。
|
||||
|
||||
Satori 的名称来源于游戏东方 Project 中的角色 [古明地觉 (Komeiji Satori)](https://zh.touhouwiki.net/wiki/%E5%8F%A4%E6%98%8E%E5%9C%B0%E8%A7%89)。古明地觉能够以心灵感应的方式与各种动物交流,取这个名字是希望 Satori 能够成为各个聊天平台之间的桥梁。
|
||||
|
||||
Satori 的开发团队长期从事聊天机器人开发,熟悉各种聊天平台的通信方式。经过长达 4 年的发展,Satori 有了健全的设计和完善的实现。目前,Satori 官方提供了超过 15 个聊天平台的适配器,完全覆盖了世界上主流的聊天平台,如 QQ、Discord、企业微信、KOOK 等等。
|
||||
|
||||
## 1. 配置协议实现端
|
||||
|
||||
请参阅对应的协议实现端项目的部署文档。
|
||||
|
||||
## 2. 配置 Satori 协议
|
||||
|
||||
1. 进入 AstrBot 的 WebUI
|
||||
2. 点击左边栏 `机器人`
|
||||
3. 然后在右边的界面中,点击 `+ 创建机器人`
|
||||
4. 选择 `Satori`
|
||||
|
||||
弹出的配置项填写:
|
||||
|
||||
- 机器人名称 (id): `satori` (随意)
|
||||
- 启用 (enable): 勾选
|
||||
- Satori API 终结点 (satori_api_base_url):`http://localhost:5600/v1`(端口和上面配置的协议端端口一致)
|
||||
- Satori WebSocket 终结点 (satori_endpoint):`ws://localhost:5600/v1/events`(端口和上面配置的协议端端口一致)
|
||||
- Satori 令牌 (satori_token):根据协议端配置情况选择填写
|
||||
|
||||
点击 `保存`。
|
||||
@@ -1,78 +0,0 @@
|
||||
# 接入 LLTwoBot (Satori)
|
||||
|
||||
> [!TIP]
|
||||
> LLTwoBot 是一个基于 QQNT 的 Onebot v11、Satori 多协议实现端,可以让你在 QQ 平台使用 Satori 协议与 AstrBot 进行通信。
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> - 请合理控制使用频率。过于频繁地发送消息可能会被判定为异常行为,增加触发风控机制的风险。
|
||||
> - 本项目严禁用于任何违反法律法规的用途。若您意图将 AstrBot 应用于非法产业或活动,我们**明确反对并拒绝**您使用本项目。
|
||||
|
||||
## 准备工作
|
||||
|
||||
请先参考 LLTwoBot 官方文档完成基础配置:
|
||||
[LLTwoBot 文档](https://llonebot.com/guide/getting-started)
|
||||
|
||||
完成文档中的步骤,确保你已经:
|
||||
|
||||
1. 下载并安装了 LLTwoBot
|
||||
2. 成功登录了 QQ 账号
|
||||
|
||||
## 配置 LLTwoBot 的 Satori 服务
|
||||
|
||||
在成功登录 QQ 后,先打开 LLTwoBot 的 WebUI 配置界面。
|
||||
> WebUI 默认地址为:<http://localhost:3080/>
|
||||
|
||||
---
|
||||
|
||||
在WebUI的配置界面侧边,选择【Satori】选项卡,进行如下配置:
|
||||
|
||||
1. 确认【启用 Satori 协议】配置项已开启
|
||||
2. 端口默认为 5600(如需修改请记住新端口)
|
||||
3. 如有必要,可填写【Satori Token】
|
||||
4. 点击右下角的【保存配置】
|
||||
|
||||

|
||||
|
||||
## 在 AstrBot 中配置 Satori 适配器
|
||||
|
||||
1. 进入 AstrBot 的管理面板
|
||||
2. 点击左边栏 `机器人`
|
||||
3. 然后在右边的界面中,点击 `+ 创建机器人`
|
||||
4. 选择 `satori`
|
||||
|
||||
弹出的配置项填写:
|
||||
|
||||
- 机器人名称 (id): `LLTwoBot`
|
||||
- 启用 (enable): 勾选
|
||||
- Satori API 终结点 (satori_api_base_url):`http://localhost:5600/v1`
|
||||
- Satori WebSocket 终结点 (satori_endpoint):`ws://localhost:5600/v1/events`
|
||||
- Satori 令牌 (satori_token):根据 LLTwoBot 配置填写(如有设置)
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> - LLTwoBot 的 satori协议 默认在 `5600` 端口提供服务
|
||||
> - 因此完整的 URL 路径为 `http://localhost:5600/v1`
|
||||
>
|
||||
> 如果你的 satori协议运行在其他端口,请根据实际情况修改对应的配置!
|
||||
|
||||

|
||||
|
||||
点击右下角 `保存` 完成配置。
|
||||
|
||||
## 🎉 大功告成
|
||||
|
||||
此时,你的 AstrBot 应该已经通过 Satori 协议成功连接到 LLTwoBot。
|
||||
|
||||
在 QQ 中发送 `/help` 以检查是否连接成功。
|
||||
|
||||
如果成功回复,则配置成功。
|
||||
|
||||
## 常见问题
|
||||
|
||||
如果遇到连接问题,请检查:
|
||||
|
||||
1. LLTwoBot 是否正常运行
|
||||
2. Satori 服务是否已启用
|
||||
3. 端口配置是否正确
|
||||
4. Token 是否匹配(如设置了 Token)
|
||||
Reference in New Issue
Block a user