diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 246469680..f0c25a6c8 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest # 运行环境 steps: - name: checkout - uses: actions/checkout@master + uses: actions/checkout@v6 - name: nodejs installation uses: actions/setup-node@v6 with: @@ -23,7 +23,7 @@ jobs: run: npm run docs:build working-directory: './docs' - name: scp - uses: appleboy/scp-action@master + uses: appleboy/scp-action@v1.0.0 with: host: ${{ secrets.HOST_NEKO }} username: ${{ secrets.USERNAME }} @@ -31,7 +31,7 @@ jobs: source: 'docs/.vitepress/dist/*' target: '/tmp/' - name: script - uses: appleboy/ssh-action@master + uses: appleboy/ssh-action@v1.2.5 with: host: ${{ secrets.HOST_NEKO }} username: ${{ secrets.USERNAME }} diff --git a/.github/workflows/dashboard_ci.yml b/.github/workflows/dashboard_ci.yml index 46d2fea73..7bfbf6361 100644 --- a/.github/workflows/dashboard_ci.yml +++ b/.github/workflows/dashboard_ci.yml @@ -45,7 +45,7 @@ jobs: - name: Create GitHub Release if: github.event_name == 'push' - uses: ncipollo/release-action@v1 + uses: ncipollo/release-action@v1.20.0 with: tag: release-${{ github.sha }} owner: AstrBotDevs diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index d79d628c3..ccf560435 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -64,20 +64,20 @@ jobs: echo "build_date=$build_date" >> $GITHUB_OUTPUT - name: Set QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@v4.0.0 - name: Set Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v4.0.0 - name: Log in to DockerHub - uses: docker/login-action@v4 + uses: docker/login-action@v4.0.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - name: Login to GitHub Container Registry if: env.HAS_GHCR_TOKEN == 'true' - uses: docker/login-action@v4 + uses: docker/login-action@v4.0.0 with: registry: ghcr.io username: ${{ env.GHCR_OWNER }} @@ -98,7 +98,7 @@ jobs: echo "EOF" >> $GITHUB_OUTPUT - name: Build and Push Nightly Image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v7.0.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -163,27 +163,27 @@ jobs: cp -r dashboard/dist data/ - name: Set QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@v4.0.0 - name: Set Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v4.0.0 - name: Log in to DockerHub - uses: docker/login-action@v4 + uses: docker/login-action@v4.0.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - name: Login to GitHub Container Registry if: env.HAS_GHCR_TOKEN == 'true' - uses: docker/login-action@v4 + uses: docker/login-action@v4.0.0 with: registry: ghcr.io username: ${{ env.GHCR_OWNER }} password: ${{ secrets.GHCR_GITHUB_TOKEN }} - name: Build and Push Release Image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v7.0.0 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41f59f0a6..0cfe18261 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v4.4.0 with: version: 10.28.2 diff --git a/.python-version b/.python-version index fdcfcfdfc..e4fba2183 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12 \ No newline at end of file +3.12 diff --git a/README.md b/README.md index 8ad697131..7b56c756a 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ For users who want to quickly experience AstrBot, are familiar with command-line ```bash uv tool install astrbot astrbot init # Only execute this command for the first time to initialize the environment -astrbot +astrbot run ``` > Requires [uv](https://docs.astral.sh/uv/) to be installed. diff --git a/README_fr.md b/README_fr.md index e406d32b2..98e7f9955 100644 --- a/README_fr.md +++ b/README_fr.md @@ -78,7 +78,7 @@ Pour les utilisateurs qui veulent découvrir AstrBot rapidement, qui sont famili ```bash uv tool install astrbot astrbot init # Exécutez cette commande uniquement la première fois pour initialiser l'environnement -astrbot +astrbot run ``` > [uv](https://docs.astral.sh/uv/) doit être installé. diff --git a/README_ja.md b/README_ja.md index 7aa146c13..2b7c43d48 100644 --- a/README_ja.md +++ b/README_ja.md @@ -78,7 +78,7 @@ AstrBot を素早く試したいユーザーで、コマンドラインに慣れ ```bash uv tool install astrbot astrbot init # 初回のみ実行して環境を初期化します -astrbot +astrbot run ``` > [uv](https://docs.astral.sh/uv/) のインストールが必要です。 diff --git a/README_ru.md b/README_ru.md index 35da14acb..29d077b45 100644 --- a/README_ru.md +++ b/README_ru.md @@ -78,7 +78,7 @@ AstrBot — это универсальная платформа Agent-чатб ```bash uv tool install astrbot astrbot init # Выполните эту команду только при первом запуске для инициализации окружения -astrbot +astrbot run ``` > Требуется установленный [uv](https://docs.astral.sh/uv/). diff --git a/README_zh-TW.md b/README_zh-TW.md index 1ace852b8..20749a077 100644 --- a/README_zh-TW.md +++ b/README_zh-TW.md @@ -78,7 +78,7 @@ AstrBot 是一個開源的一站式 Agent 聊天機器人平台,可接入主 ```bash uv tool install astrbot astrbot init # 僅首次執行此命令以初始化環境 -astrbot +astrbot run ``` > 需要安裝 [uv](https://docs.astral.sh/uv/)。 diff --git a/README_zh.md b/README_zh.md index e13d9b4e5..1e7c6b7f3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -78,7 +78,7 @@ AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、 ```bash uv tool install astrbot astrbot init # 仅首次执行此命令以初始化环境 -astrbot +astrbot run ``` > 需要安装 [uv](https://docs.astral.sh/uv/)。 diff --git a/astrbot/cli/__init__.py b/astrbot/cli/__init__.py index 593f1c94e..9abbe5d75 100644 --- a/astrbot/cli/__init__.py +++ b/astrbot/cli/__init__.py @@ -1 +1 @@ -__version__ = "4.19.5" +__version__ = "4.20.0" diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index e7e047cca..5d1c8e93d 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -30,8 +30,13 @@ async def initialize_astrbot(astrbot_root: Path) -> None: for name, path in paths.items(): path.mkdir(parents=True, exist_ok=True) click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}") - - await check_dashboard(astrbot_root / "data") + if click.confirm( + "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", + default=True, + ): + await check_dashboard(astrbot_root) + else: + click.echo("你可以使用在线面版(v4.14.4+),填写后端地址的方式来控制。") @click.command() diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index de09e5852..98acdcd19 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -15,7 +15,8 @@ async def run_astrbot(astrbot_root: Path) -> None: from astrbot.core import LogBroker, LogManager, db_helper, logger from astrbot.core.initial_loader import InitialLoader - await check_dashboard(astrbot_root / "data") + if os.environ.get("DASHBOARD_ENABLE") == "True": + await check_dashboard(astrbot_root) log_broker = LogBroker() LogManager.set_queue_handler(logger, log_broker) @@ -27,9 +28,16 @@ async def run_astrbot(astrbot_root: Path) -> None: @click.option("--reload", "-r", is_flag=True, help="Auto-reload plugins") +@click.option("--host", "-H", help="AstrBot Dashboard Host", required=False, type=str) @click.option("--port", "-p", help="AstrBot Dashboard port", required=False, type=str) +@click.option( + "--backend-only", + is_flag=True, + default=False, + help="Disable WebUI, run backend only", +) @click.command() -def run(reload: bool, port: str) -> None: +def run(reload: bool, host: str, port: str, backend_only: bool) -> None: """Run AstrBot""" try: os.environ["ASTRBOT_CLI"] = "1" @@ -43,8 +51,11 @@ def run(reload: bool, port: str) -> None: os.environ["ASTRBOT_ROOT"] = str(astrbot_root) sys.path.insert(0, str(astrbot_root)) - if port: + if port is not None: os.environ["DASHBOARD_PORT"] = port + if host is not None: + os.environ["DASHBOARD_HOST"] = host + os.environ["DASHBOARD_ENABLE"] = str(not backend_only) if reload: click.echo("Plugin auto-reload enabled") diff --git a/astrbot/cli/utils/basic.py b/astrbot/cli/utils/basic.py index 16b03218e..b90fd6e11 100644 --- a/astrbot/cli/utils/basic.py +++ b/astrbot/cli/utils/basic.py @@ -47,7 +47,7 @@ async def check_dashboard(astrbot_root: Path) -> None: click.echo("Installing dashboard...") await download_dashboard( path="data/dashboard.zip", - extract_path=str(astrbot_root), + extract_path=str(astrbot_root / "data"), version=f"v{VERSION}", latest=False, ) @@ -62,7 +62,7 @@ async def check_dashboard(astrbot_root: Path) -> None: click.echo(f"Dashboard version: {version}") await download_dashboard( path="data/dashboard.zip", - extract_path=str(astrbot_root), + extract_path=str(astrbot_root / "data"), version=f"v{VERSION}", latest=False, ) @@ -73,8 +73,8 @@ async def check_dashboard(astrbot_root: Path) -> None: click.echo("Initializing dashboard directory...") try: await download_dashboard( - path=str(astrbot_root / "dashboard.zip"), - extract_path=str(astrbot_root), + path=str(astrbot_root / "data" / "dashboard.zip"), + extract_path=str(astrbot_root / "data"), version=f"v{VERSION}", latest=False, ) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index ffc8623e6..1a49fc37a 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -739,9 +739,14 @@ def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None: continue mp = tool.handler_module_path if not mp: + # 没有 plugin 归属信息的工具(如 subagent transfer_to_*) + # 不应受到会话插件过滤影响。 + new_tool_set.add_tool(tool) continue plugin = star_map.get(mp) if not plugin: + # 无法解析插件归属时,保守保留工具,避免误过滤。 + new_tool_set.add_tool(tool) continue if plugin.name in event.plugins_name or plugin.reserved: new_tool_set.add_tool(tool) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index ba656ff53..16d7e89e3 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -5,7 +5,7 @@ from typing import Any, TypedDict from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "4.19.5" +VERSION = "4.20.0" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") WEBHOOK_SUPPORTED_PLATFORMS = [ diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 166f770a5..608ecc710 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -647,6 +647,13 @@ class BaseDatabase(abc.ABC): """Get a Platform session by its ID.""" ... + @abc.abstractmethod + async def get_platform_sessions_by_ids( + self, session_ids: list[str] + ) -> list[PlatformSession]: + """Get platform sessions by IDs.""" + ... + @abc.abstractmethod async def get_platform_sessions_by_creator( self, diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index f496e19d5..c8e50909d 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -1417,6 +1417,21 @@ class SQLiteDatabase(BaseDatabase): result = await session.execute(query) return result.scalar_one_or_none() + async def get_platform_sessions_by_ids( + self, session_ids: list[str] + ) -> list[PlatformSession]: + """Get platform sessions by IDs.""" + if not session_ids: + return [] + + async with self.get_db() as session: + session: AsyncSession + query = select(PlatformSession).where( + col(PlatformSession.session_id).in_(session_ids) + ) + result = await session.execute(query) + return list(result.scalars().all()) + async def get_platform_sessions_by_creator( self, creator: str, diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index d9ea6aa26..6311681cd 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -96,10 +96,10 @@ class Plain(BaseMessageComponent): def __init__(self, text: str, convert: bool = True, **_) -> None: super().__init__(text=text, convert=convert, **_) - def toDict(self): - return {"type": "text", "data": {"text": self.text.strip()}} + def toDict(self) -> dict: + return {"type": "text", "data": {"text": self.text}} - async def to_dict(self): + async def to_dict(self) -> dict: return {"type": "text", "data": {"text": self.text}} diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 7e42a0fd8..4b642d8ce 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -6,6 +6,7 @@ from aiocqhttp import CQHttp, Event from astrbot.api.event import AstrMessageEvent, MessageChain from astrbot.api.message_components import ( + At, BaseMessageComponent, File, Image, @@ -70,11 +71,19 @@ class AiocqhttpMessageEvent(AstrMessageEvent): """解析成 OneBot json 格式""" ret = [] for segment in message_chain.chain: - if isinstance(segment, Plain): + if isinstance(segment, At): + # At 组件后插入一个空格,避免与后续文本粘连 + d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) + ret.append(d) + ret.append({"type": "text", "data": {"text": " "}}) + elif isinstance(segment, Plain): if not segment.text.strip(): continue - d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) - ret.append(d) + d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) + ret.append(d) + else: + d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) + ret.append(d) return ret @classmethod diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index d1fd0e187..97b2b2fb4 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -51,6 +51,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): VIDEO_FILE_TYPE = 2 VOICE_FILE_TYPE = 3 FILE_FILE_TYPE = 4 + STREAM_MARKDOWN_NEWLINE_ERROR = "流式消息md分片需要\\n结束" def __init__( self, @@ -69,35 +70,71 @@ class QQOfficialMessageEvent(AstrMessageEvent): await self._post_send() async def send_streaming(self, generator, use_fallback: bool = False): - """流式输出仅支持消息列表私聊""" + """流式输出仅支持消息列表私聊(C2C),其他消息源退化为普通发送""" + # 先标记事件层“已执行发送操作”,避免异常路径遗漏 + await super().send_streaming(generator, use_fallback) + # QQ C2C 流式协议:开始/中间分片使用 state=1,结束分片使用 state=10 stream_payload = {"state": 1, "id": None, "index": 0, "reset": False} - last_edit_time = 0 # 上次编辑消息的时间 - throttle_interval = 1 # 编辑消息的间隔时间 (秒) + last_edit_time = 0 # 上次发送分片的时间 + throttle_interval = 1 # 分片间最短间隔 (秒) ret = None + source = ( + self.message_obj.raw_message + ) # 提前获取,避免 generator 为空时 NameError try: async for chain in generator: source = self.message_obj.raw_message + + if not isinstance(source, botpy.message.C2CMessage): + # 非 C2C 场景:直接累积,最后统一发 + if not self.send_buffer: + self.send_buffer = chain + else: + self.send_buffer.chain.extend(chain.chain) + continue + + # ---- C2C 流式场景 ---- + + # tool_call break 信号:工具开始执行,先把已有 buffer 以 state=10 结束当前流式段 + if chain.type == "break": + if self.send_buffer: + stream_payload["state"] = 10 + ret = await self._post_send(stream=stream_payload) + ret_id = self._extract_response_message_id(ret) + if ret_id is not None: + stream_payload["id"] = ret_id + # 重置 stream_payload,为下一段流式做准备 + stream_payload = { + "state": 1, + "id": None, + "index": 0, + "reset": False, + } + last_edit_time = 0 + continue + + # 累积内容 if not self.send_buffer: self.send_buffer = chain else: self.send_buffer.chain.extend(chain.chain) - if isinstance(source, botpy.message.C2CMessage): - # 真流式传输 - current_time = asyncio.get_running_loop().time() - time_since_last_edit = current_time - last_edit_time - - if time_since_last_edit >= throttle_interval: - ret = cast( - message.Message, - await self._post_send(stream=stream_payload), - ) - stream_payload["index"] += 1 - stream_payload["id"] = ret["id"] - last_edit_time = asyncio.get_running_loop().time() + # 节流:按时间间隔发送中间分片 + current_time = asyncio.get_running_loop().time() + if current_time - last_edit_time >= throttle_interval: + ret = cast( + message.Message, + await self._post_send(stream=stream_payload), + ) + stream_payload["index"] += 1 + ret_id = self._extract_response_message_id(ret) + if ret_id is not None: + stream_payload["id"] = ret_id + last_edit_time = asyncio.get_running_loop().time() + self.send_buffer = None # 清空已发送的分片,避免下次重复发送旧内容 if isinstance(source, botpy.message.C2CMessage): - # 结束流式对话,并且传输 buffer 中剩余的消息 + # 结束流式对话,发送 buffer 中剩余内容 stream_payload["state"] = 10 ret = await self._post_send(stream=stream_payload) else: @@ -105,9 +142,22 @@ class QQOfficialMessageEvent(AstrMessageEvent): except Exception as e: logger.error(f"发送流式消息时出错: {e}", exc_info=True) + # 避免累计内容在异常后被整包重复发送:仅清理缓存,不做非流式整包兜底 + # 如需兜底,应该只发送未发送 delta(后续可继续优化) self.send_buffer = None - return await super().send_streaming(generator, use_fallback) + return None + + @staticmethod + def _extract_response_message_id(ret) -> str | None: + """兼容 qq-botpy 返回 Message 对象或 dict 两种形态。""" + if ret is None: + return None + if isinstance(ret, dict): + ret_id = ret.get("id") + return str(ret_id) if ret_id is not None else None + ret_id = getattr(ret, "id", None) + return str(ret_id) if ret_id is not None else None async def _post_send(self, stream: dict | None = None): if not self.send_buffer: @@ -135,6 +185,11 @@ class QQOfficialMessageEvent(AstrMessageEvent): file_name, ) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer) + # C2C 流式仅用于文本分片,富媒体时降级为普通发送,避免平台侧流式校验报错。 + if stream and (image_base64 or record_file_path): + logger.debug("[QQOfficial] 检测到富媒体,降级为非流式发送。") + stream = None + if ( not plain_text and not image_base64 @@ -145,6 +200,17 @@ class QQOfficialMessageEvent(AstrMessageEvent): ): return None + # QQ C2C 流式 API 说明: + # - 开始/中间分片(state=1):增量追加内容,不需要 \n(加了会导致强制换行) + # - 最终分片(state=10):结束流,content 必须以 \n 结尾(QQ API 要求) + if ( + stream + and stream.get("state") == 10 + and plain_text + and not plain_text.endswith("\n") + ): + plain_text = plain_text + "\n" + payload: dict = { # "content": plain_text, "markdown": MarkdownPayload(content=plain_text) if plain_text else None, @@ -214,6 +280,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): ), payload=payload, plain_text=plain_text, + stream=stream, ) case botpy.message.C2CMessage(): @@ -270,6 +337,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): ), payload=payload, plain_text=plain_text, + stream=stream, ) else: ret = await self._send_with_markdown_fallback( @@ -279,6 +347,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): ), payload=payload, plain_text=plain_text, + stream=stream, ) logger.debug(f"Message sent to C2C: {ret}") @@ -294,6 +363,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): ), payload=payload, plain_text=plain_text, + stream=stream, ) case botpy.message.DirectMessage(): @@ -308,6 +378,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): ), payload=payload, plain_text=plain_text, + stream=stream, ) case _: @@ -324,10 +395,31 @@ class QQOfficialMessageEvent(AstrMessageEvent): send_func, payload: dict, plain_text: str, + stream: dict | None = None, ): try: return await send_func(payload) except botpy.errors.ServerError as err: + # QQ 流式 markdown 分片校验:内容必须以换行结尾。 + # 某些边界场景服务端仍可能判定失败,这里做一次修正重试。 + if stream and self.STREAM_MARKDOWN_NEWLINE_ERROR in str(err): + retry_payload = payload.copy() + + markdown_payload = retry_payload.get("markdown") + if isinstance(markdown_payload, dict): + md_content = cast(str, markdown_payload.get("content", "") or "") + if md_content and not md_content.endswith("\n"): + retry_payload["markdown"] = {"content": md_content + "\n"} + + content = cast(str | None, retry_payload.get("content")) + if content and not content.endswith("\n"): + retry_payload["content"] = content + "\n" + + logger.warning( + "[QQOfficial] 流式 markdown 分片换行校验失败,已修正后重试一次。" + ) + return await send_func(retry_payload) + if ( self.MARKDOWN_NOT_ALLOWED_ERROR not in str(err) or not payload.get("markdown") @@ -339,10 +431,14 @@ class QQOfficialMessageEvent(AstrMessageEvent): "[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。" ) fallback_payload = payload.copy() - fallback_payload["markdown"] = None + fallback_payload.pop("markdown", None) fallback_payload["content"] = plain_text if fallback_payload.get("msg_type") == 2: fallback_payload["msg_type"] = 0 + if stream: + fallback_content = cast(str, fallback_payload.get("content") or "") + if fallback_content and not fallback_content.endswith("\n"): + fallback_payload["content"] = fallback_content + "\n" return await send_func(fallback_payload) async def upload_group_and_c2c_image( @@ -460,13 +556,21 @@ class QQOfficialMessageEvent(AstrMessageEvent): ) -> message.Message: payload = locals() payload.pop("self", None) + # QQ API does not accept stream.id=None; remove it when not yet assigned + if "stream" in payload and payload["stream"] is not None: + stream_data = dict(payload["stream"]) + if stream_data.get("id") is None: + stream_data.pop("id", None) + payload["stream"] = stream_data route = Route("POST", "/v2/users/{openid}/messages", openid=openid) result = await self.bot.api._http.request(route, json=payload) + if result is None: + logger.warning("[QQOfficial] post_c2c_message: API 返回 None,跳过本次发送") + return None if not isinstance(result, dict): - raise RuntimeError( - f"Failed to post c2c message, response is not dict: {result}" - ) + logger.error(f"[QQOfficial] post_c2c_message: 响应不是 dict: {result}") + return None return message.Message(**result) diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py index 88d4a2128..436be70db 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py @@ -212,6 +212,7 @@ class QQOfficialPlatformAdapter(Platform): if media: payload["media"] = media payload["msg_type"] = 7 + payload.pop("msg_id", None) if file_source: media = await QQOfficialMessageEvent.upload_group_and_c2c_media( send_helper, # type: ignore @@ -223,6 +224,7 @@ class QQOfficialPlatformAdapter(Platform): if media: payload["media"] = media payload["msg_type"] = 7 + payload.pop("msg_id", None) ret = await self.client.api.post_group_message( group_openid=session.session_id, **payload, @@ -266,6 +268,9 @@ class QQOfficialPlatformAdapter(Platform): if media: payload["media"] = media payload["msg_type"] = 7 + # QQ API rejects msg_id for media (video/file) messages sent + # via the proactive tool-call path; remove it to avoid 越权 error. + payload.pop("msg_id", None) if file_source: media = await QQOfficialMessageEvent.upload_group_and_c2c_media( send_helper, # type: ignore @@ -277,6 +282,7 @@ class QQOfficialPlatformAdapter(Platform): if media: payload["media"] = media payload["msg_type"] = 7 + payload.pop("msg_id", None) ret = await QQOfficialMessageEvent.post_c2c_message( send_helper, # type: ignore diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 2dd72bd0c..87e21391e 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -289,8 +289,8 @@ class TelegramPlatformAdapter(Platform): else: message.type = MessageType.GROUP_MESSAGE message.group_id = str(update.message.chat.id) - if update.message.message_thread_id: - # Topic Group + if update.message.is_topic_message and update.message.message_thread_id: + # Telegram Topic Group: include thread id to isolate per-topic sessions. message.group_id += "#" + str(update.message.message_thread_id) message.session_id = message.group_id message.message_id = str(update.message.message_id) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index adee24073..c40234ed4 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -629,7 +629,8 @@ class ProviderOpenAIOfficial(Provider): # 最后一次不等待 if retry_cnt < max_retries - 1: await asyncio.sleep(1) - available_api_keys.remove(chosen_key) + if chosen_key in available_api_keys: + available_api_keys.remove(chosen_key) if len(available_api_keys) > 0: chosen_key = random.choice(available_api_keys) return ( diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index cf000c5a4..57be1e9a9 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -1,12 +1,14 @@ """插件的重载、启停、安装、卸载等操作。""" import asyncio +import contextlib import functools import inspect import json import logging import os import sys +import tempfile import traceback from types import ModuleType @@ -29,12 +31,12 @@ from astrbot.core.utils.astrbot_path import ( get_astrbot_config_path, get_astrbot_path, get_astrbot_plugin_path, + get_astrbot_temp_path, ) from astrbot.core.utils.io import remove_dir from astrbot.core.utils.metrics import Metric from astrbot.core.utils.requirements_utils import ( - RequirementsPrecheckFailed, - find_missing_requirements_or_raise, + plan_missing_requirements_install, ) from . import StarMetadata @@ -74,30 +76,78 @@ class PluginDependencyInstallError(Exception): self.error = error +@contextlib.contextmanager +def _temporary_filtered_requirements_file( + *, + install_lines: tuple[str, ...], +): + filtered_requirements_path: str | None = None + temp_dir = get_astrbot_temp_path() + + try: + os.makedirs(temp_dir, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode="w", + suffix="_plugin_requirements.txt", + delete=False, + dir=temp_dir, + encoding="utf-8", + ) as filtered_requirements_file: + filtered_requirements_file.write("\n".join(install_lines) + "\n") + filtered_requirements_path = filtered_requirements_file.name + + yield filtered_requirements_path + finally: + if filtered_requirements_path and os.path.exists(filtered_requirements_path): + try: + os.remove(filtered_requirements_path) + except OSError as exc: + logger.warning( + "删除临时插件依赖文件失败:%s(路径:%s)", + exc, + filtered_requirements_path, + ) + + async def _install_requirements_with_precheck( *, plugin_label: str, requirements_path: str, ) -> None: - try: - missing = find_missing_requirements_or_raise(requirements_path) - except RequirementsPrecheckFailed: + install_plan = plan_missing_requirements_install(requirements_path) + + if install_plan is None: logger.info( - f"正在安装插件 {plugin_label} 的依赖库(预检查失败,回退到完整安装): " + f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): " f"{requirements_path}" ) await pip_installer.install(requirements_path=requirements_path) return - if not missing: + if not install_plan.missing_names: logger.info(f"插件 {plugin_label} 的依赖已满足,跳过安装。") return + if not install_plan.install_lines: + fallback_reason = install_plan.fallback_reason or "unknown reason" + logger.info( + "检测到插件 %s 缺失依赖,但无法安全裁剪 requirements,回退到完整安装: %s (%s)", + plugin_label, + requirements_path, + fallback_reason, + ) + await pip_installer.install(requirements_path=requirements_path) + return + logger.info( f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: " - f"{requirements_path} -> {sorted(missing)}" + f"{requirements_path} -> {sorted(install_plan.missing_names)}" ) - await pip_installer.install(requirements_path=requirements_path) + + with _temporary_filtered_requirements_file( + install_lines=install_plan.install_lines, + ) as filtered_requirements_path: + await pip_installer.install(requirements_path=filtered_requirements_path) class PluginManager: diff --git a/astrbot/core/tools/cron_tools.py b/astrbot/core/tools/cron_tools.py index 387ef49e4..0fa2a8bf4 100644 --- a/astrbot/core/tools/cron_tools.py +++ b/astrbot/core/tools/cron_tools.py @@ -30,7 +30,7 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]): "properties": { "cron_expression": { "type": "string", - "description": "Cron expression defining recurring schedule (e.g., '0 8 * * *').", + "description": "Cron expression defining recurring schedule (e.g., '0 8 * * *' or '0 23 * * mon-fri'). Prefer named weekdays like 'mon-fri' or 'sat,sun' instead of numeric day-of-week ranges such as '1-5' to avoid ambiguity across cron implementations.", }, "run_at": { "type": "string", diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index b56592674..f9ad47dbf 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -1,3 +1,4 @@ +import asyncio import base64 import logging import os @@ -7,6 +8,7 @@ import ssl import time import uuid import zipfile +from ipaddress import IPv4Address, IPv6Address, ip_address from pathlib import Path import aiohttp @@ -206,18 +208,53 @@ def file_to_base64(file_path: str) -> str: return "base64://" + base64_str -def get_local_ip_addresses(): +def get_local_ip_addresses() -> list[IPv4Address | IPv6Address]: net_interfaces = psutil.net_if_addrs() - network_ips = [] + network_ips: list[IPv4Address | IPv6Address] = [] - for interface, addrs in net_interfaces.items(): + for _, addrs in net_interfaces.items(): for addr in addrs: - if addr.family == socket.AF_INET: # 使用 socket.AF_INET 代替 psutil.AF_INET - network_ips.append(addr.address) + if addr.family == socket.AF_INET: + network_ips.append(ip_address(addr.address)) + elif addr.family == socket.AF_INET6: + # 过滤掉 IPv6 的 link-local 地址(fe80:...) + ip = ip_address(addr.address.split("%")[0]) # 处理带 zone index 的情况 + if not ip.is_link_local: + network_ips.append(ip) return network_ips +async def get_public_ip_address() -> list[IPv4Address | IPv6Address]: + urls = [ + "https://api64.ipify.org", + "https://ident.me", + "https://ifconfig.me", + "https://icanhazip.com", + ] + found_ips: dict[int, IPv4Address | IPv6Address] = {} + + async def fetch(session: aiohttp.ClientSession, url: str): + try: + async with session.get(url, timeout=3) as resp: + if resp.status == 200: + raw_ip = (await resp.text()).strip() + ip = ip_address(raw_ip) + if ip.version not in found_ips: + found_ips[ip.version] = ip + except Exception as e: + # Ignore errors from individual services so that a single failing + # endpoint does not prevent discovering the public IP from others. + logger.debug("Failed to fetch public IP from %s: %s", url, e) + + async with aiohttp.ClientSession() as session: + tasks = [fetch(session, url) for url in urls] + await asyncio.gather(*tasks) + + # 返回找到的所有 IP 对象列表 + return list(found_ips.values()) + + async def get_dashboard_version(): # First check user data directory (manually updated / downloaded dashboard). dist_dir = os.path.join(get_astrbot_data_path(), "dist") diff --git a/astrbot/core/utils/requirements_utils.py b/astrbot/core/utils/requirements_utils.py index 7f3827256..e031de846 100644 --- a/astrbot/core/utils/requirements_utils.py +++ b/astrbot/core/utils/requirements_utils.py @@ -4,7 +4,7 @@ import os import re import shlex import sys -from collections.abc import Iterable, Iterator +from collections.abc import Iterable, Iterator, Sequence from dataclasses import dataclass from packaging.requirements import InvalidRequirement, Requirement @@ -29,6 +29,13 @@ class ParsedPackageInput: requirement_names: frozenset[str] +@dataclass(frozen=True) +class MissingRequirementsPlan: + missing_names: frozenset[str] + install_lines: tuple[str, ...] + fallback_reason: str | None = None + + def canonicalize_distribution_name(name: str) -> str: return re.sub(r"[-_.]+", "-", name).strip("-").lower() @@ -364,8 +371,8 @@ def _load_requirement_lines_for_precheck( None, ) if fallback_line is not None: - logger.warning( - "预检查缺失依赖失败,将回退到完整安装: unresolved direct reference in %s: %s", + logger.info( + "缺失依赖预检查发现无法安全裁剪的 option/direct-reference 行,将回退到完整安装: %s (%s)", requirements_path, fallback_line, ) @@ -381,6 +388,13 @@ def find_missing_requirements(requirements_path: str) -> set[str] | None: if not can_precheck or requirement_lines is None: return None + return find_missing_requirements_from_lines(requirement_lines) + + +def find_missing_requirements_from_lines( + requirement_lines: Sequence[str], +) -> set[str] | None: + required = list(iter_requirements(lines=requirement_lines)) if not required: return set() @@ -401,6 +415,70 @@ def find_missing_requirements(requirements_path: str) -> set[str] | None: return missing +def build_missing_requirements_install_lines( + requirements_path: str, + requirement_lines: Sequence[str], + missing_names: set[str] | frozenset[str], +) -> tuple[str, ...] | None: + wanted_names = set(missing_names) + install_lines: list[str] = [] + for line in requirement_lines: + parsed = _parse_requirement_line(line) + if parsed is None: + if looks_like_direct_reference(line) or line.startswith(("-", "--")): + logger.debug( + "缺失依赖行筛选回退到完整安装:requirements 中包含无法安全裁剪的 option/direct-reference 行: %s (%s)", + requirements_path, + line, + ) + return None + continue + + name, _specifier = parsed + if name in wanted_names: + install_lines.append(line) + + return tuple(install_lines) + + +def plan_missing_requirements_install( + requirements_path: str, +) -> MissingRequirementsPlan | None: + can_precheck, requirement_lines = _load_requirement_lines_for_precheck( + requirements_path + ) + if not can_precheck or requirement_lines is None: + return None + + missing = find_missing_requirements_from_lines(requirement_lines) + if missing is None: + return None + + install_lines = build_missing_requirements_install_lines( + requirements_path, + requirement_lines, + missing, + ) + if install_lines is None: + return None + if missing and not install_lines: + logger.warning( + "预检查缺失依赖成功,但无法映射到可安装 requirement 行,将回退到完整安装: %s -> %s", + requirements_path, + sorted(missing), + ) + return MissingRequirementsPlan( + missing_names=frozenset(missing), + install_lines=(), + fallback_reason="unmapped missing requirement names", + ) + + return MissingRequirementsPlan( + missing_names=frozenset(missing), + install_lines=install_lines, + ) + + def find_missing_requirements_or_raise(requirements_path: str) -> set[str]: missing = find_missing_requirements(requirements_path) if missing is None: diff --git a/astrbot/dashboard/routes/__init__.py b/astrbot/dashboard/routes/__init__.py index fbbd0c7a0..7e6e79146 100644 --- a/astrbot/dashboard/routes/__init__.py +++ b/astrbot/dashboard/routes/__init__.py @@ -9,16 +9,19 @@ from .conversation import ConversationRoute from .cron import CronRoute from .file import FileRoute from .knowledge_base import KnowledgeBaseRoute +from .live_chat import LiveChatRoute from .log import LogRoute from .open_api import OpenApiRoute from .persona import PersonaRoute from .platform import PlatformRoute from .plugin import PluginRoute +from .route import Response, RouteContext from .session_management import SessionManagementRoute from .skills import SkillsRoute from .stat import StatRoute from .static_file import StaticFileRoute from .subagent import SubAgentRoute +from .t2i import T2iRoute from .tools import ToolsRoute from .update import UpdateRoute @@ -46,4 +49,8 @@ __all__ = [ "ToolsRoute", "SkillsRoute", "UpdateRoute", + "T2iRoute", + "LiveChatRoute", + "Response", + "RouteContext", ] diff --git a/astrbot/dashboard/routes/auth.py b/astrbot/dashboard/routes/auth.py index 40db1f60b..eac5f65b0 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -82,7 +82,8 @@ class AuthRoute(Route): def generate_jwt(self, username): payload = { "username": username, - "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7), + "exp": datetime.datetime.now(datetime.timezone.utc) + + datetime.timedelta(days=7), } jwt_token = self.config["dashboard"].get("jwt_secret", None) if not jwt_token: diff --git a/astrbot/dashboard/routes/backup.py b/astrbot/dashboard/routes/backup.py index 952806beb..ecc5dbfc8 100644 --- a/astrbot/dashboard/routes/backup.py +++ b/astrbot/dashboard/routes/backup.py @@ -977,7 +977,17 @@ class BackupRoute(Route): if not jwt_secret: return Response().error("服务器配置错误").__dict__ - jwt.decode(token, jwt_secret, algorithms=["HS256"]) + # Verify JWT token with strict security options + jwt.decode( + token, + jwt_secret, + algorithms=["HS256"], + options={ + "require": ["exp"], # Require expiration claim + "verify_signature": True, # Explicitly verify signature + "verify_exp": True, # Verify expiration + }, + ) except jwt.ExpiredSignatureError: return Response().error("Token 已过期,请刷新页面后重试").__dict__ except jwt.InvalidTokenError: diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index a914f3cbf..3c888b492 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -51,6 +51,7 @@ class ChatRoute(Route): "/chat/get_session": ("GET", self.get_session), "/chat/stop": ("POST", self.stop_session), "/chat/delete_session": ("GET", self.delete_webchat_session), + "/chat/batch_delete_sessions": ("POST", self.batch_delete_sessions), "/chat/update_session_display_name": ( "POST", self.update_session_display_name, @@ -578,19 +579,9 @@ class ChatRoute(Route): return Response().ok(data={"stopped_count": stopped_count}).__dict__ - async def delete_webchat_session(self): - """Delete a Platform session and all its related data.""" - session_id = request.args.get("session_id") - if not session_id: - return Response().error("Missing key: session_id").__dict__ - username = g.get("username", "guest") - - # 验证会话是否存在且属于当前用户 - session = await self.db.get_platform_session_by_id(session_id) - if not session: - return Response().error(f"Session {session_id} not found").__dict__ - if session.creator != username: - return Response().error("Permission denied").__dict__ + async def _delete_session_internal(self, session, username: str) -> None: + """Delete a single session and all its related data.""" + session_id = session.session_id # 删除该会话下的所有对话 message_type = "GroupMessage" if session.is_group else "FriendMessage" @@ -632,8 +623,70 @@ class ChatRoute(Route): # 删除会话 await self.db.delete_platform_session(session_id) + async def delete_webchat_session(self): + """Delete a Platform session and all its related data.""" + session_id = request.args.get("session_id") + if not session_id: + return Response().error("Missing key: session_id").__dict__ + username = g.get("username", "guest") + + session = await self.db.get_platform_session_by_id(session_id) + if not session: + return Response().error(f"Session {session_id} not found").__dict__ + if session.creator != username: + return Response().error("Permission denied").__dict__ + + await self._delete_session_internal(session, username) + return Response().ok().__dict__ + async def batch_delete_sessions(self): + """Batch delete multiple Platform sessions.""" + post_data = await request.json + if post_data is None: + return Response().error("Missing JSON body").__dict__ + if not isinstance(post_data, dict): + return Response().error("Invalid JSON body: expected object").__dict__ + + session_ids = post_data.get("session_ids") + if not session_ids or not isinstance(session_ids, list): + return Response().error("Missing or invalid key: session_ids").__dict__ + + username = g.get("username", "guest") + sessions = await self.db.get_platform_sessions_by_ids(session_ids) + sessions_by_id = {session.session_id: session for session in sessions} + deleted_count = 0 + failed_items = [] + + for sid in session_ids: + session = sessions_by_id.get(sid) + if not session: + failed_items.append({"session_id": sid, "reason": "not found"}) + continue + if session.creator != username: + failed_items.append({"session_id": sid, "reason": "permission denied"}) + continue + + try: + await self._delete_session_internal(session, username) + deleted_count += 1 + sessions_by_id.pop(sid, None) + except Exception: + logger.warning("Failed to delete session %s", sid) + failed_items.append({"session_id": sid, "reason": "internal_error"}) + + return ( + Response() + .ok( + data={ + "deleted_count": deleted_count, + "failed_count": len(failed_items), + "failed_items": failed_items, + } + ) + .__dict__ + ) + def _extract_attachment_ids(self, history_list) -> list[str]: """从消息历史中提取所有 attachment_id""" attachment_ids = [] diff --git a/astrbot/dashboard/routes/route.py b/astrbot/dashboard/routes/route.py index 53c623443..4fdc37971 100644 --- a/astrbot/dashboard/routes/route.py +++ b/astrbot/dashboard/routes/route.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import asdict, dataclass from quart import Quart @@ -57,3 +57,7 @@ class Response: self.data = data self.message = message return self + + def to_json(self): + # Return a plain dict so callers can safely wrap with jsonify() + return asdict(self) diff --git a/astrbot/dashboard/routes/static_file.py b/astrbot/dashboard/routes/static_file.py index e056b6c5a..15fec95d1 100644 --- a/astrbot/dashboard/routes/static_file.py +++ b/astrbot/dashboard/routes/static_file.py @@ -5,6 +5,9 @@ class StaticFileRoute(Route): def __init__(self, context: RouteContext) -> None: super().__init__(context) + if "index" in self.app.view_functions: + return + index_ = [ "/", "/auth/login", diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index a4742aa67..7fa50fa1b 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -2,10 +2,13 @@ import asyncio import hashlib import logging import os +import platform import socket +from collections.abc import Callable from datetime import datetime +from ipaddress import IPv4Address, IPv6Address, ip_address from pathlib import Path -from typing import Protocol, cast +from typing import Protocol import jwt import psutil @@ -14,6 +17,7 @@ from hypercorn.asyncio import serve from hypercorn.config import Config as HyperConfig from quart import Quart, g, jsonify, request from quart.logging import default_handler +from quart_cors import cors from astrbot.core import logger from astrbot.core.config.default import VERSION @@ -25,13 +29,6 @@ from astrbot.core.utils.io import get_local_ip_addresses from .routes import * from .routes.api_key import ALL_OPEN_API_SCOPES -from .routes.backup import BackupRoute -from .routes.live_chat import LiveChatRoute -from .routes.platform import PlatformRoute -from .routes.route import Response, RouteContext -from .routes.session_management import SessionManagementRoute -from .routes.subagent import SubAgentRoute -from .routes.t2i import T2iRoute # Static assets shipped inside the wheel (built during `hatch build`). _BUNDLED_DIST = Path(__file__).parent / "dist" @@ -58,6 +55,16 @@ class AstrBotJSONProvider(DefaultJSONProvider): class AstrBotDashboard: + """AstrBot Web Dashboard""" + + ALLOWED_ENDPOINT_PREFIXES = ( + "/api/auth/login", + "/api/file", + "/api/platform/webhook", + "/api/stat/start-time", + "/api/backup/download", + ) + def __init__( self, core_lifecycle: AstrBotCoreLifecycle, @@ -68,7 +75,26 @@ class AstrBotDashboard: self.core_lifecycle = core_lifecycle self.config = core_lifecycle.astrbot_config self.db = db + self.shutdown_event = shutdown_event + self.enable_webui = self._check_webui_enabled() + + self._init_paths(webui_dir) + self._init_app() + self.context = RouteContext(self.config, self.app) + + self._init_routes(db) + self._init_plugin_route_index() + self._init_jwt_secret() + + def _check_webui_enabled(self) -> bool: + cfg = self.config.get("dashboard", {}) + _env = os.environ.get("DASHBOARD_ENABLE") + if _env is not None: + return _env.lower() in ("true", "1", "yes") + return cfg.get("enable", True) + + def _init_paths(self, webui_dir: str | None): # Path priority: # 1. Explicit webui_dir argument # 2. data/dist/ (user-installed / manually updated dashboard) @@ -83,62 +109,96 @@ class AstrBotDashboard: self.data_path = str(_BUNDLED_DIST) logger.info("Using bundled dashboard dist: %s", self.data_path) else: - # Fall back to expected user path (will fail gracefully later) self.data_path = os.path.abspath(user_dist) - self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/") - APP = self.app # noqa - self.app.config["MAX_CONTENT_LENGTH"] = ( - 128 * 1024 * 1024 - ) # 将 Flask 允许的最大上传文件体大小设置为 128 MB + def _init_app(self): + """初始化 Quart 应用""" + global APP + self.app = Quart( + "AstrBotDashboard", + static_folder=self.data_path, + static_url_path="/", + ) + APP = self.app + self.app.json_provider_class = DefaultJSONProvider + self.app.config["MAX_CONTENT_LENGTH"] = 128 * 1024 * 1024 # 128MB self.app.json = AstrBotJSONProvider(self.app) self.app.json.sort_keys = False - self.app.before_request(self.auth_middleware) - # token 用于验证请求 - logging.getLogger(self.app.name).removeHandler(default_handler) - self.context = RouteContext(self.config, self.app) - self.ur = UpdateRoute( - self.context, - core_lifecycle.astrbot_updator, - core_lifecycle, + + # 配置 CORS + self.app = cors( + self.app, + allow_origin="*", + allow_headers=["Authorization", "Content-Type", "X-API-Key"], + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], ) - self.sr = StatRoute(self.context, db, core_lifecycle) - self.pr = PluginRoute( - self.context, - core_lifecycle, - core_lifecycle.plugin_manager, + + @self.app.route("/") + async def index(): + if not self.enable_webui: + return "WebUI is disabled." + return await self.app.send_static_file("index.html") + + @self.app.errorhandler(404) + async def not_found(e): + if not self.enable_webui: + return "WebUI is disabled." + if request.path.startswith("/api/"): + return jsonify(Response().error("Not Found").to_json()), 404 + return await self.app.send_static_file("index.html") + + @self.app.before_serving + async def startup(): + pass + + @self.app.after_serving + async def shutdown(): + pass + + self.app.before_request(self.auth_middleware) + logging.getLogger(self.app.name).removeHandler(default_handler) + + def _init_routes(self, db: BaseDatabase): + UpdateRoute( + self.context, self.core_lifecycle.astrbot_updator, self.core_lifecycle + ) + StatRoute(self.context, db, self.core_lifecycle) + PluginRoute( + self.context, self.core_lifecycle, self.core_lifecycle.plugin_manager ) self.command_route = CommandRoute(self.context) - self.cr = ConfigRoute(self.context, core_lifecycle) - self.lr = LogRoute(self.context, core_lifecycle.log_broker) + self.cr = ConfigRoute(self.context, self.core_lifecycle) + self.lr = LogRoute(self.context, self.core_lifecycle.log_broker) self.sfr = StaticFileRoute(self.context) self.ar = AuthRoute(self.context) self.api_key_route = ApiKeyRoute(self.context, db) - self.chat_route = ChatRoute(self.context, db, core_lifecycle) + self.chat_route = ChatRoute(self.context, db, self.core_lifecycle) self.open_api_route = OpenApiRoute( self.context, db, - core_lifecycle, + self.core_lifecycle, self.chat_route, ) self.chatui_project_route = ChatUIProjectRoute(self.context, db) - self.tools_root = ToolsRoute(self.context, core_lifecycle) - self.subagent_route = SubAgentRoute(self.context, core_lifecycle) - self.skills_route = SkillsRoute(self.context, core_lifecycle) - self.conversation_route = ConversationRoute(self.context, db, core_lifecycle) + self.tools_root = ToolsRoute(self.context, self.core_lifecycle) + self.subagent_route = SubAgentRoute(self.context, self.core_lifecycle) + self.skills_route = SkillsRoute(self.context, self.core_lifecycle) + self.conversation_route = ConversationRoute( + self.context, db, self.core_lifecycle + ) self.file_route = FileRoute(self.context) self.session_management_route = SessionManagementRoute( self.context, db, - core_lifecycle, + self.core_lifecycle, ) - self.persona_route = PersonaRoute(self.context, db, core_lifecycle) - self.cron_route = CronRoute(self.context, core_lifecycle) - self.t2i_route = T2iRoute(self.context, core_lifecycle) - self.kb_route = KnowledgeBaseRoute(self.context, core_lifecycle) - self.platform_route = PlatformRoute(self.context, core_lifecycle) - self.backup_route = BackupRoute(self.context, db, core_lifecycle) - self.live_chat_route = LiveChatRoute(self.context, db, core_lifecycle) + self.persona_route = PersonaRoute(self.context, db, self.core_lifecycle) + self.cron_route = CronRoute(self.context, self.core_lifecycle) + self.t2i_route = T2iRoute(self.context, self.core_lifecycle) + self.kb_route = KnowledgeBaseRoute(self.context, self.core_lifecycle) + self.platform_route = PlatformRoute(self.context, self.core_lifecycle) + self.backup_route = BackupRoute(self.context, db, self.core_lifecycle) + self.live_chat_route = LiveChatRoute(self.context, db, self.core_lifecycle) self.app.add_url_rule( "/api/plug/", @@ -146,20 +206,31 @@ class AstrBotDashboard: methods=["GET", "POST"], ) - self.shutdown_event = shutdown_event + def _init_plugin_route_index(self): + """将插件路由索引,避免 O(n) 查找""" + self._plugin_route_map: dict[tuple[str, str], Callable] = {} - self._init_jwt_secret() + for ( + route, + handler, + methods, + _, + ) in self.core_lifecycle.star_context.registered_web_apis: + for method in methods: + self._plugin_route_map[(route, method)] = handler - async def srv_plug_route(self, subpath, *args, **kwargs): - """插件路由""" - registered_web_apis = self.core_lifecycle.star_context.registered_web_apis - for api in registered_web_apis: - route, view_handler, methods, _ = api - if route == f"/{subpath}" and request.method in methods: - return await view_handler(*args, **kwargs) - return jsonify(Response().error("未找到该路由").__dict__) + def _init_jwt_secret(self): + dashboard_cfg = self.config.setdefault("dashboard", {}) + if not dashboard_cfg.get("jwt_secret"): + dashboard_cfg["jwt_secret"] = os.urandom(32).hex() + self.config.save_config() + logger.info("Initialized random JWT secret for dashboard.") + self._jwt_secret = dashboard_cfg["jwt_secret"] async def auth_middleware(self): + # 放行CORS预检请求 + if request.method == "OPTIONS": + return None if not request.path.startswith("/api"): return None if request.path.startswith("/api/v1"): @@ -196,33 +267,42 @@ class AstrBotDashboard: await self.db.touch_api_key(api_key.key_id) return None - allowed_endpoints = [ - "/api/auth/login", - "/api/file", - "/api/platform/webhook", - "/api/stat/start-time", - "/api/backup/download", # 备份下载使用 URL 参数传递 token - ] - if any(request.path.startswith(prefix) for prefix in allowed_endpoints): + if any(request.path.startswith(p) for p in self.ALLOWED_ENDPOINT_PREFIXES): return None - # 声明 JWT + token = request.headers.get("Authorization") if not token: - r = jsonify(Response().error("未授权").__dict__) - r.status_code = 401 - return r - token = token.removeprefix("Bearer ") + return self._unauthorized("未授权") + try: - payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"]) + payload = jwt.decode( + token.removeprefix("Bearer "), + self._jwt_secret, + algorithms=["HS256"], + options={"require": ["username"]}, + ) g.username = payload["username"] except jwt.ExpiredSignatureError: - r = jsonify(Response().error("Token 过期").__dict__) - r.status_code = 401 - return r - except jwt.InvalidTokenError: - r = jsonify(Response().error("Token 无效").__dict__) - r.status_code = 401 - return r + return self._unauthorized("Token 过期") + except jwt.PyJWTError: + return self._unauthorized("Token 无效") + + @staticmethod + def _unauthorized(msg: str): + r = jsonify(Response().error(msg).to_json()) + r.status_code = 401 + return r + + async def srv_plug_route(self, subpath: str, *args, **kwargs): + handler = self._plugin_route_map.get((f"/{subpath}", request.method)) + if not handler: + return jsonify(Response().error("未找到该路由").to_json()) + + try: + return await handler(*args, **kwargs) + except Exception: + logger.exception("插件 Web API 执行异常") + return jsonify(Response().error("插件 Web API 执行异常").to_json()) @staticmethod def _extract_raw_api_key() -> str | None: @@ -252,126 +332,92 @@ class AstrBotDashboard: } return scope_map.get(path) - def check_port_in_use(self, port: int) -> bool: + def check_port_in_use(self, host: str, port: int) -> bool: """跨平台检测端口是否被占用""" + family = socket.AF_INET6 if ":" in host else socket.AF_INET try: - # 创建 IPv4 TCP Socket - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # 设置超时时间 - sock.settimeout(2) - result = sock.connect_ex(("127.0.0.1", port)) - sock.close() - # result 为 0 表示端口被占用 - return result == 0 - except Exception as e: - logger.warning(f"检查端口 {port} 时发生错误: {e!s}") - # 如果出现异常,保守起见认为端口可能被占用 + with socket.socket(family, socket.SOCK_STREAM) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) + return False + except OSError: return True def get_process_using_port(self, port: int) -> str: - """获取占用端口的进程详细信息""" + """获取占用端口的进程信息""" try: - for conn in psutil.net_connections(kind="inet"): - if cast(_AddrWithPort, conn.laddr).port == port: - try: - process = psutil.Process(conn.pid) - # 获取详细信息 - proc_info = [ - f"进程名: {process.name()}", - f"PID: {process.pid}", - f"执行路径: {process.exe()}", - f"工作目录: {process.cwd()}", - f"启动命令: {' '.join(process.cmdline())}", - ] - return "\n ".join(proc_info) - except (psutil.NoSuchProcess, psutil.AccessDenied) as e: - return f"无法获取进程详细信息(可能需要管理员权限): {e!s}" - return "未找到占用进程" + for proc in psutil.process_iter(["pid", "name"]): + try: + connections = proc.net_connections() + for conn in connections: + if conn.laddr.port == port: + return f"PID: {proc.info['pid']}, Name: {proc.info['name']}" + except ( + psutil.NoSuchProcess, + psutil.AccessDenied, + psutil.ZombieProcess, + ): + pass except Exception as e: return f"获取进程信息失败: {e!s}" + return "未知进程" - def _init_jwt_secret(self) -> None: - if not self.config.get("dashboard", {}).get("jwt_secret", None): - # 如果没有设置 JWT 密钥,则生成一个新的密钥 - jwt_secret = os.urandom(32).hex() - self.config["dashboard"]["jwt_secret"] = jwt_secret - self.config.save_config() - logger.info("Initialized random JWT secret for dashboard.") - self._jwt_secret = self.config["dashboard"]["jwt_secret"] + async def run(self) -> None: + """Run dashboard server (blocking)""" + if not self.enable_webui: + logger.warning( + "WebUI 已禁用 (dashboard.enable=false or DASHBOARD_ENABLE=false)" + ) - def run(self): - ip_addr = [] - dashboard_config = self.core_lifecycle.astrbot_config.get("dashboard", {}) - port = ( - os.environ.get("DASHBOARD_PORT") - or os.environ.get("ASTRBOT_DASHBOARD_PORT") - or dashboard_config.get("port", 6185) + dashboard_config = self.config.get("dashboard", {}) + host = os.environ.get("DASHBOARD_HOST") or dashboard_config.get( + "host", "0.0.0.0" ) - host = ( - os.environ.get("DASHBOARD_HOST") - or os.environ.get("ASTRBOT_DASHBOARD_HOST") - or dashboard_config.get("host", "0.0.0.0") + port = int( + os.environ.get("DASHBOARD_PORT") or dashboard_config.get("port", 6185) ) - enable = dashboard_config.get("enable", True) ssl_config = dashboard_config.get("ssl", {}) - if not isinstance(ssl_config, dict): - ssl_config = {} ssl_enable = _parse_env_bool( - os.environ.get("DASHBOARD_SSL_ENABLE") - or os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE"), - bool(ssl_config.get("enable", False)), + os.environ.get("DASHBOARD_SSL_ENABLE"), + ssl_config.get("enable", False), ) + scheme = "https" if ssl_enable else "http" + display_host = f"[{host}]" if ":" in host else host - if not enable: - logger.info("WebUI 已被禁用") - return None - - logger.info(f"正在启动 WebUI, 监听地址: {scheme}://{host}:{port}") - if host == "0.0.0.0": + if self.enable_webui: logger.info( - "提示: WebUI 将监听所有网络接口,请注意安全。(可在 data/cmd_config.json 中配置 dashboard.host 以修改 host)", + "正在启动 WebUI + API, 监听地址: %s://%s:%s", + scheme, + display_host, + port, + ) + else: + logger.info( + "正在启动 API Server (WebUI 已分离), 监听地址: %s://%s:%s", + scheme, + display_host, + port, ) - if host not in ["localhost", "127.0.0.1"]: - try: - ip_addr = get_local_ip_addresses() - except Exception as _: - pass - if isinstance(port, str): - port = int(port) + check_hosts = {host} + if host not in ("127.0.0.1", "localhost", "::1"): + check_hosts.add("127.0.0.1") + for check_host in check_hosts: + if self.check_port_in_use(check_host, port): + info = self.get_process_using_port(port) + raise RuntimeError(f"端口 {port} 已被占用\n{info}") - if self.check_port_in_use(port): - process_info = self.get_process_using_port(port) - logger.error( - f"错误:端口 {port} 已被占用\n" - f"占用信息: \n {process_info}\n" - f"请确保:\n" - f"1. 没有其他 AstrBot 实例正在运行\n" - f"2. 端口 {port} 没有被其他程序占用\n" - f"3. 如需使用其他端口,请修改配置文件", - ) - - raise Exception(f"端口 {port} 已被占用") - - parts = [f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动,可访问\n\n"] - parts.append(f" ➜ 本地: {scheme}://localhost:{port}\n") - for ip in ip_addr: - parts.append(f" ➜ 网络: {scheme}://{ip}:{port}\n") - parts.append(" ➜ 默认用户名和密码: astrbot\n ✨✨✨\n") - display = "".join(parts) - - if not ip_addr: - display += ( - "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" - ) - - logger.info(display) + if self.enable_webui: + self._print_access_urls(host, port, scheme) # 配置 Hypercorn config = HyperConfig() - config.bind = [f"{host}:{port}"] + binds: list[str] = [self._build_bind(host, port)] + if host == "::" and platform.system() in ("Windows", "Darwin"): + binds.append(self._build_bind("0.0.0.0", port)) + config.bind = binds + if ssl_enable: cert_file = ( os.environ.get("DASHBOARD_SSL_CERT") @@ -414,12 +460,46 @@ class AstrBotDashboard: if disable_access_log: config.accesslog = None else: - # 启用访问日志,使用简洁格式 config.accesslog = "-" config.access_log_format = "%(h)s %(r)s %(s)s %(b)s %(D)s" - return serve(self.app, config, shutdown_trigger=self.shutdown_trigger) + await serve(self.app, config, shutdown_trigger=self.shutdown_trigger) - async def shutdown_trigger(self) -> None: + @staticmethod + def _build_bind(host: str, port: int) -> str: + try: + ip: IPv4Address | IPv6Address = ip_address(host) + return f"[{ip}]:{port}" if ip.version == 6 else f"{ip}:{port}" + except ValueError: + return f"{host}:{port}" + + def _print_access_urls(self, host: str, port: int, scheme: str = "http") -> None: + local_ips: list[IPv4Address | IPv6Address] = get_local_ip_addresses() + + parts = [f"\n ✨✨✨\n AstrBot v{VERSION} WebUI 已启动\n\n"] + + parts.append(f" ➜ 本地: {scheme}://localhost:{port}\n") + + if host in ("::", "0.0.0.0"): + for ip in local_ips: + if ip.is_loopback: + continue + + if ip.version == 6: + display_url = f"{scheme}://[{ip}]:{port}" + else: + display_url = f"{scheme}://{ip}:{port}" + + parts.append(f" ➜ 网络: {display_url}\n") + + parts.append(" ➜ 默认用户名和密码: astrbot\n ✨✨✨\n") + + if not local_ips: + parts.append( + "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" + ) + + logger.info("".join(parts)) + + async def shutdown_trigger(self): await self.shutdown_event.wait() - logger.info("AstrBot WebUI 已经被优雅地关闭") diff --git a/changelogs/v4.20.0.md b/changelogs/v4.20.0.md new file mode 100644 index 000000000..3db18b5ea --- /dev/null +++ b/changelogs/v4.20.0.md @@ -0,0 +1,64 @@ +## What's Changed + +### 新增 + +- 新增俄语翻译([#6081](https://github.com/AstrBotDevs/AstrBot/pull/6081))。 +- QQ 官方 Bot 新增文件、语音、视频消息支持(含 WebSocket 模式)([#6063](https://github.com/AstrBotDevs/AstrBot/pull/6063))。 + +### 优化 + +- 优化 QQ 官方 Bot 的流式消息投递可靠性与主动媒体发送能力([#6131](https://github.com/AstrBotDevs/AstrBot/pull/6131))。 +- 优化边界场景下 booter 选择逻辑与消息发送工具([#6064](https://github.com/AstrBotDevs/AstrBot/pull/6064))。 + +### 修复 + +- 修复 Dashboard README 对话框锚点导航失效([#6083](https://github.com/AstrBotDevs/AstrBot/pull/6083))。 +- 优先使用具名 weekday 的 cron 示例,避免歧义([#6091](https://github.com/AstrBotDevs/AstrBot/pull/6091))。 +- 修复插件市场安装后状态未及时刷新的问题([#6124](https://github.com/AstrBotDevs/AstrBot/pull/6124))。 +- 修复插件依赖安装逻辑:仅安装缺失依赖([#6088](https://github.com/AstrBotDevs/AstrBot/pull/6088))。 +- 移除 Telegram 适配器中已废弃的 `normalize_whitespace` 参数([#6044](https://github.com/AstrBotDevs/AstrBot/pull/6044))。 +- 修复 Windows 本地 skill 文件读取问题([#6028](https://github.com/AstrBotDevs/AstrBot/pull/6028))。 +- 修复 Discord pre-ack emoji 配置重启后不持久化的问题([#6031](https://github.com/AstrBotDevs/AstrBot/pull/6031))。 +- 统一 WebUI 搜索框清空行为([#6017](https://github.com/AstrBotDevs/AstrBot/pull/6017))。 +- 优化插件依赖自动安装流程与 Dashboard 安装体验([#5954](https://github.com/AstrBotDevs/AstrBot/pull/5954))。 + + +### 文档 + +- 新增 Astrbook 和玖帕喵社区链接([#6135](https://github.com/AstrBotDevs/AstrBot/pull/6135))。 +- 修正文档 `docker.md` 与 `napcat.md` 中的拼写错误([#6048](https://github.com/AstrBotDevs/AstrBot/pull/6048))。 +- 在多语言 README 中补充官方开发群号,并改进配置元数据中的正则说明。 +- 更新编辑链接模式并移除过时仓库引用。 + +--- + +## What's Changed (EN) + +### New Features + +- Added Russian translation support ([#6081](https://github.com/AstrBotDevs/AstrBot/pull/6081)). +- Added file, voice, and video message support for QQ Official Bot (including WebSocket mode) ([#6063](https://github.com/AstrBotDevs/AstrBot/pull/6063)). + +### Improvements + +- Improved streaming message delivery reliability and proactive media sending for QQ Official API ([#6131](https://github.com/AstrBotDevs/AstrBot/pull/6131)). +- Optimized booter selection logic in edge cases and message sending tooling ([#6064](https://github.com/AstrBotDevs/AstrBot/pull/6064)). + +### Bug Fixes + +- Fixed broken README dialog anchor navigation in the Dashboard ([#6083](https://github.com/AstrBotDevs/AstrBot/pull/6083)). +- Preferred named weekday cron examples to reduce ambiguity ([#6091](https://github.com/AstrBotDevs/AstrBot/pull/6091)). +- Fixed plugin market install-state refresh after installation ([#6124](https://github.com/AstrBotDevs/AstrBot/pull/6124)). +- Fixed plugin dependency installation logic to install only missing packages ([#6088](https://github.com/AstrBotDevs/AstrBot/pull/6088)). +- Removed deprecated `normalize_whitespace` parameter in the Telegram adapter ([#6044](https://github.com/AstrBotDevs/AstrBot/pull/6044)). +- Fixed local skill file reading issues on Windows ([#6028](https://github.com/AstrBotDevs/AstrBot/pull/6028)). +- Fixed Discord pre-ack emoji config not being persisted across restarts ([#6031](https://github.com/AstrBotDevs/AstrBot/pull/6031)). +- Unified WebUI search input clear behavior ([#6017](https://github.com/AstrBotDevs/AstrBot/pull/6017)). +- Improved plugin dependency auto-install flow and Dashboard installation experience ([#5954](https://github.com/AstrBotDevs/AstrBot/pull/5954)). + +### Documentation + +- Added Astrbook and Jiupa Miao community links ([#6135](https://github.com/AstrBotDevs/AstrBot/pull/6135)). +- Fixed typos in `docker.md` and `napcat.md` ([#6048](https://github.com/AstrBotDevs/AstrBot/pull/6048)). +- Added official developer group IDs to multilingual READMEs and improved regex description in config metadata. +- Updated edit-link patterns and removed obsolete repository references. diff --git a/compose-with-shipyard.yml b/compose-with-shipyard.yml index 338f7b86f..24ced5a95 100644 --- a/compose-with-shipyard.yml +++ b/compose-with-shipyard.yml @@ -37,6 +37,7 @@ services: - DEFAULT_SHIP_MEMORY=512m volumes: - ${PWD}/data/shipyard/bay_data:/app/data + - ${PWD}/data/temp:/AstrBot/data/temp # Bind the local temp directory to the sandbox so that the uploaded file can be accessed in the sandbox - /var/run/docker.sock:/var/run/docker.sock:ro networks: - astrbot_network diff --git a/dashboard/.gitignore b/dashboard/.gitignore index 6e03962af..f17c69129 100644 --- a/dashboard/.gitignore +++ b/dashboard/.gitignore @@ -1,3 +1,5 @@ node_modules/ .DS_Store -dist/ \ No newline at end of file +dist/ +bun.lock +pmpm-lock.yaml diff --git a/dashboard/env.d.ts b/dashboard/env.d.ts index b4b350830..a90bd47be 100644 --- a/dashboard/env.d.ts +++ b/dashboard/env.d.ts @@ -7,3 +7,9 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/dashboard/package.json b/dashboard/package.json index 7b4a7f071..cfd0bd727 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -17,17 +17,17 @@ "@tiptap/starter-kit": "2.1.7", "@tiptap/vue-3": "2.1.7", "apexcharts": "3.42.0", - "axios": ">=1.6.2 <1.10.0 || >1.10.0 <2.0.0", + "axios": "1.13.5", "axios-mock-adapter": "^1.22.0", "chance": "1.1.11", "date-fns": "2.30.0", - "dompurify": "^3.3.1", + "dompurify": "^3.3.2", "event-source-polyfill": "^1.0.31", "highlight.js": "^11.11.1", "js-md5": "^0.8.3", "katex": "^0.16.27", - "lodash": "4.17.21", - "markdown-it": "^14.1.0", + "lodash": "4.17.23", + "markdown-it": "^14.1.1", "markstream-vue": "^0.0.6", "mermaid": "^11.12.2", "monaco-editor": "^0.52.2", @@ -38,7 +38,7 @@ "stream-markdown": "^0.0.13", "stream-monaco": "^0.0.17", "vee-validate": "4.11.3", - "vite-plugin-vuetify": "1.0.2", + "vite-plugin-vuetify": "2.1.3", "vue": "3.3.4", "vue-i18n": "^11.1.5", "vue-router": "4.2.4", @@ -54,7 +54,7 @@ "@types/dompurify": "^3.0.5", "@types/markdown-it": "^14.1.2", "@types/node": "^20.5.7", - "@vitejs/plugin-vue": "4.3.3", + "@vitejs/plugin-vue": "5.2.4", "@vue/eslint-config-prettier": "8.0.0", "@vue/eslint-config-typescript": "11.0.3", "@vue/tsconfig": "^0.4.0", @@ -64,9 +64,15 @@ "sass": "1.66.1", "sass-loader": "13.3.2", "typescript": "5.1.6", - "vite": "4.4.9", + "vite": "5.4.1", "vue-cli-plugin-vuetify": "2.5.8", "vue-tsc": "1.8.8", "vuetify-loader": "^2.0.0-alpha.9" + }, + "pnpm": { + "overrides": { + "immutable": "4.3.8", + "lodash-es": "4.17.23" + } } } diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index ea8636c61..af45aa6f0 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -4,6 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + immutable: 4.3.8 + lodash-es: 4.17.23 + importers: .: @@ -21,11 +25,11 @@ importers: specifier: 3.42.0 version: 3.42.0 axios: - specifier: '>=1.6.2 <1.10.0 || >1.10.0 <2.0.0' - version: 1.13.4 + specifier: 1.13.5 + version: 1.13.5 axios-mock-adapter: specifier: ^1.22.0 - version: 1.22.0(axios@1.13.4) + version: 1.22.0(axios@1.13.5) chance: specifier: 1.1.11 version: 1.1.11 @@ -33,8 +37,8 @@ importers: specifier: 2.30.0 version: 2.30.0 dompurify: - specifier: ^3.3.1 - version: 3.3.1 + specifier: ^3.3.2 + version: 3.3.2 event-source-polyfill: specifier: ^1.0.31 version: 1.0.31 @@ -48,11 +52,11 @@ importers: specifier: ^0.16.27 version: 0.16.28 lodash: - specifier: 4.17.21 - version: 4.17.21 + specifier: 4.17.23 + version: 4.17.23 markdown-it: - specifier: ^14.1.0 - version: 14.1.0 + specifier: ^14.1.1 + version: 14.1.1 markstream-vue: specifier: ^0.0.6 version: 0.0.6(katex@0.16.28)(mermaid@11.12.2)(shiki@3.22.0)(stream-markdown@0.0.13(shiki@3.22.0))(stream-monaco@0.0.17(monaco-editor@0.52.2))(vue-i18n@11.2.8(vue@3.3.4))(vue@3.3.4) @@ -84,8 +88,8 @@ importers: specifier: 4.11.3 version: 4.11.3(vue@3.3.4) vite-plugin-vuetify: - specifier: 1.0.2 - version: 1.0.2(vite@4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11) + specifier: 2.1.3 + version: 2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11) vue: specifier: 3.3.4 version: 3.3.4 @@ -103,7 +107,7 @@ importers: version: 0.1.4 vuetify: specifier: 3.7.11 - version: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@1.0.2)(vue@3.3.4) + version: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4) yup: specifier: 1.2.0 version: 1.2.0 @@ -127,8 +131,8 @@ importers: specifier: ^20.5.7 version: 20.19.32 '@vitejs/plugin-vue': - specifier: 4.3.3 - version: 4.3.3(vite@4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4) + specifier: 5.2.4 + version: 5.2.4(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4) '@vue/eslint-config-prettier': specifier: 8.0.0 version: 8.0.0(@types/eslint@9.6.1)(eslint@8.48.0)(prettier@3.0.2) @@ -157,8 +161,8 @@ importers: specifier: 5.1.6 version: 5.1.6 vite: - specifier: 4.4.9 - version: 4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) + specifier: 6.4.1 + version: 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) @@ -213,135 +217,159 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -460,6 +488,144 @@ packages: '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + 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==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + '@rushstack/eslint-patch@1.3.3': resolution: {integrity: sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==} @@ -745,6 +911,9 @@ packages: '@types/node@20.19.32': resolution: {integrity: sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==} + '@types/node@20.19.37': + resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -815,11 +984,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-vue@4.3.3': - resolution: {integrity: sha512-ssxyhIAZqB0TrpUg6R0cBpCuMk9jTIlO1GNSKKQD6S8VjnXi6JXKfUXjSsxey9IwQiaRGsO1WnW9Rkl1L6AJVw==} - engines: {node: ^14.18.0 || >=16.0.0} + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: - vite: ^4.0.0 + vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 '@volar/language-core@1.10.10': @@ -915,6 +1084,12 @@ packages: vue: ^3.0.0 vuetify: ^3.0.0-beta.4 + '@vuetify/loader-shared@2.1.2': + resolution: {integrity: sha512-X+1jBLmXHkpQEnC0vyOb4rtX2QSkBiFhaFXz8yhQqN2A4vQ6k2nChxN4Ol7VAY5KoqMdFoRMnmNdp/1qYXDQig==} + peerDependencies: + vue: ^3.0.0 + vuetify: '>=3' + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -985,6 +1160,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1006,8 +1186,8 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} alien-signals@2.0.8: resolution: {integrity: sha512-844G1VLkk0Pe2SJjY0J8vp8ADI73IM4KliNu2OGlYzWpO28NexEUvjHTcFjFX3VXoiUtwTbHxLNI9ImkcoBqzA==} @@ -1042,14 +1222,15 @@ packages: peerDependencies: axios: '>= 0.17.0' - axios@1.13.4: - resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.9.19: - resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} hasBin: true big.js@5.2.2: @@ -1091,8 +1272,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001769: - resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + caniuse-lite@1.0.30001778: + resolution: {integrity: sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1384,22 +1565,23 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dompurify@3.3.1: - resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dompurify@3.3.2: + resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} + engines: {node: '>=20'} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.286: - resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} emojis-list@3.0.0: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -1429,9 +1611,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} hasBin: true escalade@3.2.0: @@ -1542,6 +1724,15 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1684,8 +1875,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - immutable@4.3.7: - resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + immutable@4.3.8: + resolution: {integrity: sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -1818,17 +2009,14 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1862,8 +2050,8 @@ packages: resolution: {integrity: sha512-nZpRTJj4S6bN0I5wsNBtgzDKx+HYBBSsvKjGdYw7/tPdrzfo3gUTt3ZbeAjPGeZaC6a4LFi4JdhTVeLm3F6TIQ==} engines: {node: '>=18'} - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true marked@16.4.2: @@ -1978,8 +2166,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -2072,6 +2260,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pinia@2.1.6: resolution: {integrity: sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==} peerDependencies: @@ -2127,8 +2319,8 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - prosemirror-changeset@2.3.1: - resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} + prosemirror-changeset@2.4.0: + resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} prosemirror-collab@1.3.1: resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} @@ -2139,8 +2331,8 @@ packages: prosemirror-dropcursor@1.8.2: resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} - prosemirror-gapcursor@1.4.0: - resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} prosemirror-history@1.5.0: resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} @@ -2154,8 +2346,8 @@ packages: prosemirror-markdown@1.13.4: resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} - prosemirror-menu@1.2.5: - resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} + prosemirror-menu@1.3.0: + resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==} prosemirror-model@1.25.4: resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} @@ -2204,9 +2396,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -2252,9 +2441,9 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rollup@3.29.5: - resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true rope-sequence@1.3.4: @@ -2269,9 +2458,6 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2316,9 +2502,6 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2435,8 +2618,8 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -2466,6 +2649,10 @@ packages: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} @@ -2568,41 +2755,53 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-plugin-vuetify@1.0.2: - resolution: {integrity: sha512-MubIcKD33O8wtgQXlbEXE7ccTEpHZ8nPpe77y9Wy3my2MWw/PgehP9VqTp92BLqr0R1dSL970Lynvisx3UxBFw==} - engines: {node: '>=12'} + vite-plugin-vuetify@2.1.3: + resolution: {integrity: sha512-Q4SC/4TqbNvaZIFb9YsfBqkGlYHbJJJ6uU3CnRBZqLUF3s5eCMVZAaV4GkTbehIH/bhSj42lMXztOwc71u6rVw==} + engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: - vite: ^2.7.0 || ^3.0.0 || ^4.0.0 + vite: '>=5' vue: ^3.0.0 - vuetify: ^3.0.0-beta.4 + vuetify: '>=3' - vite@4.4.9: - resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} - engines: {node: ^14.18.0 || >=16.0.0} + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': '>= 14' + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' less: '*' lightningcss: ^1.21.0 sass: '*' + sass-embedded: '*' stylus: '*' sugarss: '*' - terser: ^5.4.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true + jiti: + optional: true less: optional: true lightningcss: optional: true sass: optional: true + sass-embedded: + optional: true stylus: optional: true sugarss: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} @@ -2718,8 +2917,8 @@ packages: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} webpack@5.105.0: @@ -2786,12 +2985,12 @@ snapshots: dependencies: '@chevrotain/gast': 11.0.3 '@chevrotain/types': 11.0.3 - lodash-es: 4.17.21 + lodash-es: 4.17.23 '@chevrotain/gast@11.0.3': dependencies: '@chevrotain/types': 11.0.3 - lodash-es: 4.17.21 + lodash-es: 4.17.23 '@chevrotain/regexp-to-ast@11.0.3': {} @@ -2799,70 +2998,82 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@esbuild/android-arm64@0.18.20': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm@0.18.20': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-x64@0.18.20': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/darwin-arm64@0.18.20': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.18.20': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.18.20': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.18.20': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.18.20': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm@0.18.20': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-ia32@0.18.20': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-loong64@0.18.20': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-mips64el@0.18.20': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.18.20': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-riscv64@0.18.20': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-s390x@0.18.20': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-x64@0.18.20': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/netbsd-x64@0.18.20': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.18.20': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.18.20': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.18.20': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.18.20': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.18.20': + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@8.48.0)': @@ -2985,6 +3196,81 @@ snapshots: '@remirror/core-constants@3.0.0': {} + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + '@rushstack/eslint-patch@1.3.3': {} '@shikijs/core@3.22.0': @@ -3121,16 +3407,16 @@ snapshots: '@tiptap/pm@2.27.2': dependencies: - prosemirror-changeset: 2.3.1 + prosemirror-changeset: 2.4.0 prosemirror-collab: 1.3.1 prosemirror-commands: 1.7.1 prosemirror-dropcursor: 1.8.2 - prosemirror-gapcursor: 1.4.0 + prosemirror-gapcursor: 1.4.1 prosemirror-history: 1.5.0 prosemirror-inputrules: 1.5.1 prosemirror-keymap: 1.2.3 prosemirror-markdown: 1.13.4 - prosemirror-menu: 1.2.5 + prosemirror-menu: 1.3.0 prosemirror-model: 1.25.4 prosemirror-schema-basic: 1.2.4 prosemirror-schema-list: 1.5.1 @@ -3293,7 +3579,7 @@ snapshots: '@types/dompurify@3.2.0': dependencies: - dompurify: 3.3.1 + dompurify: 3.3.2 '@types/eslint-scope@3.7.7': dependencies: @@ -3332,6 +3618,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@20.19.37': + dependencies: + undici-types: 6.21.0 + '@types/semver@7.7.1': {} '@types/trusted-types@2.0.7': @@ -3425,9 +3715,9 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-vue@4.3.3(vite@4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)': + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)': dependencies: - vite: 4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) + vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) vue: 3.3.4 '@volar/language-core@1.10.10': @@ -3573,7 +3863,13 @@ snapshots: find-cache-dir: 3.3.2 upath: 2.0.1 vue: 3.3.4 - vuetify: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@1.0.2)(vue@3.3.4) + vuetify: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4) + + '@vuetify/loader-shared@2.1.2(vue@3.3.4)(vuetify@3.7.11)': + dependencies: + upath: 2.0.1 + vue: 3.3.4 + vuetify: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4) '@webassemblyjs/ast@1.14.1': dependencies: @@ -3657,9 +3953,9 @@ snapshots: '@yr/monotone-cubic-spline@1.0.3': {} - acorn-import-phases@1.0.4(acorn@8.15.0): + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: @@ -3667,17 +3963,19 @@ snapshots: acorn@8.15.0: {} - ajv-formats@2.1.1(ajv@8.17.1): + acorn@8.16.0: {} + + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 - ajv-keywords@5.1.0(ajv@8.17.1): + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: - ajv: 8.17.1 + ajv: 8.18.0 fast-deep-equal: 3.1.3 ajv@6.12.6: @@ -3687,7 +3985,7 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 @@ -3723,13 +4021,13 @@ snapshots: asynckit@0.4.0: {} - axios-mock-adapter@1.22.0(axios@1.13.4): + axios-mock-adapter@1.22.0(axios@1.13.5): dependencies: - axios: 1.13.4 + axios: 1.13.5 fast-deep-equal: 3.1.3 is-buffer: 2.0.5 - axios@1.13.4: + axios@1.13.5: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 @@ -3739,7 +4037,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.9.19: {} + baseline-browser-mapping@2.10.0: {} big.js@5.2.2: {} @@ -3762,10 +4060,10 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001769 - electron-to-chromium: 1.5.286 - node-releases: 2.0.27 + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001778 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) buffer-from@1.1.2: {} @@ -3779,7 +4077,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001769: {} + caniuse-lite@1.0.30001778: {} ccount@2.0.1: {} @@ -3806,7 +4104,7 @@ snapshots: '@chevrotain/regexp-to-ast': 11.0.3 '@chevrotain/types': 11.0.3 '@chevrotain/utils': 11.0.3 - lodash-es: 4.17.21 + lodash-es: 4.17.23 chokidar@3.6.0: dependencies: @@ -4088,7 +4386,7 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.3.1: + dompurify@3.3.2: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -4098,11 +4396,11 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.286: {} + electron-to-chromium@1.5.307: {} emojis-list@3.0.0: {} - enhanced-resolve@5.19.0: + enhanced-resolve@5.20.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -4128,30 +4426,34 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - esbuild@0.18.20: + esbuild@0.25.12: optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escalade@3.2.0: {} @@ -4286,6 +4588,10 @@ snapshots: dependencies: reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -4441,7 +4747,7 @@ snapshots: ignore@5.3.2: {} - immutable@4.3.7: {} + immutable@4.3.8: {} import-fresh@3.3.1: dependencies: @@ -4487,7 +4793,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.19.32 + '@types/node': 20.19.37 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -4556,13 +4862,11 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.21: {} - lodash-es@4.17.23: {} lodash.merge@4.6.2: {} - lodash@4.17.21: {} + lodash@4.17.23: {} magic-string@0.30.21: dependencies: @@ -4594,7 +4898,7 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 - markdown-it@14.1.0: + markdown-it@14.1.1: dependencies: argparse: 2.0.1 entities: 4.5.0 @@ -4651,7 +4955,7 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.13 dayjs: 1.11.19 - dompurify: 3.3.1 + dompurify: 3.3.2 katex: 0.16.28 khroma: 2.1.0 lodash-es: 4.17.23 @@ -4718,7 +5022,7 @@ snapshots: neo-async@2.6.2: {} - node-releases@2.0.27: {} + node-releases@2.0.36: {} normalize-path@3.0.0: {} @@ -4799,6 +5103,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.3: {} + pinia@2.1.6(typescript@5.1.6)(vue@3.3.4): dependencies: '@vue/devtools-api': 6.6.4 @@ -4849,7 +5155,7 @@ snapshots: property-information@7.1.0: {} - prosemirror-changeset@2.3.1: + prosemirror-changeset@2.4.0: dependencies: prosemirror-transform: 1.11.0 @@ -4869,7 +5175,7 @@ snapshots: prosemirror-transform: 1.11.0 prosemirror-view: 1.41.6 - prosemirror-gapcursor@1.4.0: + prosemirror-gapcursor@1.4.1: dependencies: prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.4 @@ -4896,10 +5202,10 @@ snapshots: prosemirror-markdown@1.13.4: dependencies: '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 + markdown-it: 14.1.1 prosemirror-model: 1.25.4 - prosemirror-menu@1.2.5: + prosemirror-menu@1.3.0: dependencies: crelt: 1.0.6 prosemirror-commands: 1.7.1 @@ -4962,10 +5268,6 @@ snapshots: queue-microtask@1.2.3: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -5004,8 +5306,35 @@ snapshots: robust-predicates@3.0.2: {} - rollup@3.29.5: + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 rope-sequence@1.3.4: {} @@ -5023,8 +5352,6 @@ snapshots: rw@1.3.3: {} - safe-buffer@5.2.1: {} - safer-buffer@2.1.2: {} sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0): @@ -5037,7 +5364,7 @@ snapshots: sass@1.66.1: dependencies: chokidar: 3.6.0 - immutable: 4.3.7 + immutable: 4.3.8 source-map-js: 1.2.1 schema-utils@3.3.0: @@ -5049,18 +5376,14 @@ snapshots: schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) semver@6.3.1: {} semver@7.7.4: {} - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5181,19 +5504,18 @@ snapshots: tapable@2.3.0: {} - terser-webpack-plugin@5.3.16(webpack@5.105.0): + terser-webpack-plugin@5.4.0(webpack@5.105.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - serialize-javascript: 6.0.2 terser: 5.46.0 webpack: 5.105.0 terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -5203,6 +5525,11 @@ snapshots: tinyexec@1.0.2: {} + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tippy.js@6.3.7: dependencies: '@popperjs/core': 2.11.8 @@ -5297,22 +5624,25 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-vuetify@1.0.2(vite@4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11): + vite-plugin-vuetify@2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11): dependencies: - '@vuetify/loader-shared': 1.7.1(vue@3.3.4)(vuetify@3.7.11) + '@vuetify/loader-shared': 2.1.2(vue@3.3.4)(vuetify@3.7.11) debug: 4.4.3 upath: 2.0.1 - vite: 4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) + vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) vue: 3.3.4 - vuetify: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@1.0.2)(vue@3.3.4) + vuetify: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4) transitivePeerDependencies: - supports-color - vite@4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0): + vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0): dependencies: - esbuild: 0.18.20 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 3.29.5 + rollup: 4.59.0 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 20.19.32 fsevents: 2.3.3 @@ -5359,7 +5689,7 @@ snapshots: eslint-visitor-keys: 3.4.3 espree: 9.6.1 esquery: 1.7.0 - lodash: 4.17.21 + lodash: 4.17.23 semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -5416,17 +5746,17 @@ snapshots: null-loader: 4.0.1(webpack@5.105.0) querystring: 0.2.1 upath: 2.0.1 - vuetify: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@1.0.2)(vue@3.3.4) + vuetify: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4) webpack: 5.105.0 transitivePeerDependencies: - vue - vuetify@3.7.11(typescript@5.1.6)(vite-plugin-vuetify@1.0.2)(vue@3.3.4): + vuetify@3.7.11(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4): dependencies: vue: 3.3.4 optionalDependencies: typescript: 5.1.6 - vite-plugin-vuetify: 1.0.2(vite@4.4.9(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11) + vite-plugin-vuetify: 2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11) w3c-keyname@2.2.8: {} @@ -5435,7 +5765,7 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 - webpack-sources@3.3.3: {} + webpack-sources@3.3.4: {} webpack@5.105.0: dependencies: @@ -5445,11 +5775,11 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) + 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.19.0 + enhanced-resolve: 5.20.0 es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -5461,9 +5791,9 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.105.0) + terser-webpack-plugin: 5.4.0(webpack@5.105.0) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.3.4 transitivePeerDependencies: - '@swc/core' - esbuild diff --git a/dashboard/public/config.json b/dashboard/public/config.json new file mode 100644 index 000000000..0d7e84a8a --- /dev/null +++ b/dashboard/public/config.json @@ -0,0 +1,13 @@ +{ + "apiBaseUrl": "", + "presets": [ + { + "name": "Default (Auto)", + "url": "" + }, + { + "name": "Localhost", + "url": "http://localhost:6185" + } + ] +} diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index 7c25e1bc3..3bb98f136 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -11,6 +11,7 @@ :currSessionId="currSessionId" :selectedProjectId="selectedProjectId" :transportMode="transportMode" + :sendShortcut="sendShortcut" :isDark="isDark" :chatboxMode="chatboxMode" :isMobile="isMobile" @@ -20,6 +21,7 @@ @selectConversation="handleSelectConversation" @editTitle="showEditTitleDialog" @deleteConversation="handleDeleteConversation" + @batchDeleteConversations="handleBatchDeleteConversations" @closeMobileSidebar="closeMobileSidebar" @toggleTheme="toggleTheme" @toggleFullscreen="toggleFullscreen" @@ -28,6 +30,7 @@ @editProject="showEditProjectDialog" @deleteProject="handleDeleteProject" @updateTransportMode="setTransportMode" + @updateSendShortcut="setSendShortcut" /> @@ -78,6 +81,7 @@ :session-id="currSessionId || null" :current-session="getCurrentSession" :replyTo="replyTo" + :send-shortcut="sendShortcut" @send="handleSendMessage" @stop="handleStopMessage" @toggleStreaming="toggleStreaming" @@ -109,6 +113,7 @@ :session-id="currSessionId || null" :current-session="getCurrentSession" :replyTo="replyTo" + :send-shortcut="sendShortcut" @send="handleSendMessage" @stop="handleStopMessage" @toggleStreaming="toggleStreaming" @@ -139,6 +144,7 @@ :session-id="currSessionId || null" :current-session="getCurrentSession" :replyTo="replyTo" + :send-shortcut="sendShortcut" @send="handleSendMessage" @stop="handleStopMessage" @toggleStreaming="toggleStreaming" @@ -220,10 +226,13 @@ import { useMediaHandling } from '@/composables/useMediaHandling'; import { useProjects } from '@/composables/useProjects'; import type { Project } from '@/components/chat/ProjectList.vue'; import { useRecording } from '@/composables/useRecording'; +import { useToast } from '@/utils/toast'; interface Props { chatboxMode?: boolean; } +type SendShortcut = 'enter' | 'shift_enter'; +const SEND_SHORTCUT_STORAGE_KEY = 'chat_send_shortcut'; const props = withDefaults(defineProps(), { chatboxMode: false @@ -233,6 +242,7 @@ const router = useRouter(); const route = useRoute(); const { t } = useI18n(); const { tm } = useModuleI18n('features/chat'); +const { warning: toastWarning } = useToast(); const theme = useTheme(); const customizer = useCustomizerStore(); @@ -257,6 +267,7 @@ const { getSessions, newSession, deleteSession: deleteSessionFn, + batchDeleteSessions, showEditTitleDialog, saveTitle, updateSessionTitle, @@ -330,6 +341,12 @@ interface ReplyInfo { const replyTo = ref(null); const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark'); +const sendShortcut = ref('shift_enter'); + +function setSendShortcut(mode: SendShortcut) { + sendShortcut.value = mode; + localStorage.setItem(SEND_SHORTCUT_STORAGE_KEY, mode); +} // 检测是否为手机端 function checkMobile() { @@ -510,6 +527,33 @@ async function handleDeleteConversation(sessionId: string) { } } +async function handleBatchDeleteConversations(sessionIds: string[]) { + try { + const result = await batchDeleteSessions(sessionIds); + + // 仅在当前会话成功删除时清除信息 + if (result.currentSessionDeleted) { + messages.value = []; + } + + // 失败处理 + if (result.failed_count > 0) { + toastWarning( + tm('batch.partialFailure', { failed: result.failed_count, total: sessionIds.length }) + ); + } + + // 如果在项目视图中,刷新项目会话列表 + if (selectedProjectId.value) { + const sessions = await getProjectSessions(selectedProjectId.value); + projectSessions.value = sessions; + } + } catch (err) { + console.error('Batch delete sessions failed:', err); + toastWarning(tm('batch.requestFailed')); + } +} + async function handleSelectProject(projectId: string) { selectedProjectId.value = projectId; const sessions = await getProjectSessions(projectId); @@ -694,6 +738,10 @@ watch(sessions, (newSessions) => { }); onMounted(() => { + const storedShortcut = localStorage.getItem(SEND_SHORTCUT_STORAGE_KEY); + if (storedShortcut === 'enter' || storedShortcut === 'shift_enter') { + sendShortcut.value = storedShortcut; + } checkMobile(); window.addEventListener('resize', checkMobile); getSessions(); diff --git a/dashboard/src/components/chat/ChatInput.vue b/dashboard/src/components/chat/ChatInput.vue index 87cc82c66..ee48120f7 100644 --- a/dashboard/src/components/chat/ChatInput.vue +++ b/dashboard/src/components/chat/ChatInput.vue @@ -15,7 +15,7 @@
- mdi-cloud-upload + mdi-cloud-upload {{ tm('input.dropToUpload') }}
@@ -41,7 +41,7 @@ @@ -87,7 +87,7 @@ {{ tm('voice.liveMode') }} --> - @@ -95,13 +95,13 @@ {{ isRecording ? tm('voice.speaking') : tm('voice.startRecording') }} - + {{ tm('input.stopGenerating') }} - @@ -117,7 +117,7 @@
- + {{ tm('voice.recording') }} @@ -126,7 +126,7 @@
- + {{ file.original_name }} @@ -173,6 +173,7 @@ interface Props { currentSession?: Session | null; configId?: string | null; replyTo?: ReplyInfo | null; + sendShortcut?: 'enter' | 'shift_enter'; } const props = withDefaults(defineProps(), { @@ -180,7 +181,8 @@ const props = withDefaults(defineProps(), { currentSession: null, configId: null, stagedFiles: () => [], - replyTo: null + replyTo: null, + sendShortcut: 'shift_enter' }); const emit = defineEmits<{ @@ -253,9 +255,29 @@ watch(localPrompt, () => { }); function handleKeyDown(e: KeyboardEvent) { - // Enter 插入换行(桌面和手机端均如此,发送通过右下角发送按鈕) - // Shift+Enter 发送(Ctrl+Enter / Cmd+Enter 也保留) - if (e.keyCode === 13 && (e.shiftKey || e.ctrlKey || e.metaKey)) { + const isEnter = e.key === 'Enter'; + if (!isEnter) { + // Ctrl+B 录音 + if (e.ctrlKey && e.keyCode === 66) { + e.preventDefault(); + if (ctrlKeyDown.value) return; + + ctrlKeyDown.value = true; + ctrlKeyTimer.value = window.setTimeout(() => { + if (ctrlKeyDown.value && !props.isRecording) { + emit('startRecording'); + } + }, ctrlKeyLongPressThreshold); + } + return; + } + + const isSendHotkey = + e.ctrlKey || + e.metaKey || + (props.sendShortcut === 'enter' ? !e.shiftKey : e.shiftKey); + + if (isSendHotkey) { e.preventDefault(); if (localPrompt.value.trim() === '/astr_live_dev') { emit('openLiveMode'); @@ -267,19 +289,6 @@ function handleKeyDown(e: KeyboardEvent) { } return; } - - // Ctrl+B 录音 - if (e.ctrlKey && e.keyCode === 66) { - e.preventDefault(); - if (ctrlKeyDown.value) return; - - ctrlKeyDown.value = true; - ctrlKeyTimer.value = window.setTimeout(() => { - if (ctrlKeyDown.value && !props.isRecording) { - emit('startRecording'); - } - }, ctrlKeyLongPressThreshold); - } } function handleKeyUp(e: KeyboardEvent) { @@ -399,8 +408,8 @@ defineExpose({ left: 0; right: 0; bottom: 0; - background-color: rgba(103, 58, 183, 0.15); - border: 2px dashed rgba(103, 58, 183, 0.5); + background-color: rgba(var(--v-theme-primary), 0.12); + border: 2px dashed rgba(var(--v-theme-primary), 0.45); border-radius: 24px; display: flex; align-items: center; @@ -419,7 +428,7 @@ defineExpose({ .drop-text { font-size: 16px; font-weight: 500; - color: #673ab7; + color: rgb(var(--v-theme-primary)); } /* Fade transition for drop overlay */ @@ -439,7 +448,7 @@ defineExpose({ justify-content: space-between; padding: 8px 16px; margin: 8px 8px 0 8px; - background-color: rgba(103, 58, 183, 0.06); + background-color: rgba(var(--v-theme-primary), 0.06); border-radius: 12px; gap: 8px; max-height: 500px; diff --git a/dashboard/src/components/chat/ConversationSidebar.vue b/dashboard/src/components/chat/ConversationSidebar.vue index 01a5fccfb..8045fd8d1 100644 --- a/dashboard/src/components/chat/ConversationSidebar.vue +++ b/dashboard/src/components/chat/ConversationSidebar.vue @@ -5,7 +5,7 @@ 'mobile-sidebar-open': isMobile && mobileMenuOpen, 'mobile-sidebar': isMobile }" - :style="{ 'background-color': isDark ? sidebarCollapsed ? '#1e1e1e' : '#2d2d2d' : sidebarCollapsed ? '#ffffff' : '#f1f4f9' }"> + :style="{ backgroundColor: sidebarCollapsed && !isMobile ? 'rgb(var(--v-theme-surface))' : 'rgb(var(--v-theme-mcpCardBg))' }">
- {{ tm('actions.newChat') }} - + {{ tm('actions.newChat') }} + + mdi-checkbox-multiple-marked-outline + +
+
+ +
+ + {{ isAllSelected ? tm('batch.deselectAll') : tm('batch.selectAll') }} + + {{ tm('batch.selected', { count: batchSelected.length }) }} + + + {{ tm('batch.delete') }} + +
+ + style="background-color: transparent;" :selected="batchMode ? [] : selectedSessions" + @update:selected="handleListSelect"> + rounded="lg" class="conversation-item" active-color="secondary" + @click="batchMode ? toggleBatchItem(item.session_id) : undefined"> + + + + :style="{ color: 'rgb(var(--v-theme-primaryText))' }"> {{ item.display_name || tm('conversation.newConversation') }} -