diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 70bb8f30c..341581157 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,7 +21,23 @@ -- [ ] 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc. -- [ ] 👀 我的更改经过了良好的测试,**并已在上方提供了“验证步骤”和“运行截图”**。/ My changes have been well-tested, **and "Verification Steps" and "Screenshots" have been provided above**. -- [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt` 和 `pyproject.toml` 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in `requirements.txt` and `pyproject.toml`. -- [ ] 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code. +- [ ] 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。 + / If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc. + +- [ ] 👀 我的更改经过了良好的测试,**并已在上方提供了“验证步骤”和“运行截图”**。 + / My changes have been well-tested, **and "Verification Steps" and "Screenshots" have been provided above**. + +- [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 `requirements.txt` 和 `pyproject.toml` 文件相应位置。 + / I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in `requirements.txt` and `pyproject.toml`. + +- [ ] 😮 我的更改没有引入恶意代码。 + / My changes do not introduce malicious code. + +- [ ] ⚠️ 我已认真阅读并理解以上所有内容,确保本次提交符合规范。 + / I have read and understood all the above and confirm this PR follows the rules. + +- [ ] 🚀 我确保本次开发**基于 dev 分支**,并将代码合并至**开发分支**(除非极其紧急,才允许合并到主分支)。 + / I confirm that this development is **based on the dev branch** and will be merged into the **development branch**, unless it is extremely urgent to merge into the main branch. + +- [ ] ⚠️ 我**没有**认真阅读以上内容,直接提交。 + / I **did not** read the above carefully before submitting. 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/pr-checklist-check.yml b/.github/workflows/pr-checklist-check.yml new file mode 100644 index 000000000..f93eac126 --- /dev/null +++ b/.github/workflows/pr-checklist-check.yml @@ -0,0 +1,45 @@ +name: PR Checklist Check + +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + +jobs: + check: + runs-on: ubuntu-latest + + permissions: + pull-requests: write + issues: write + + steps: + - name: Check checklist + id: check + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.pull_request.body || ""; + const regex = /-\s*\[\s*x\s*\].*没有.*认真阅读/i; + const bad = regex.test(body); + core.setOutput("bad", bad); + + - name: Close PR + if: steps.check.outputs.bad == 'true' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: `检测到你勾选了“我没有认真阅读”,PR 已关闭。` + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + state: "closed" + }); \ No newline at end of file 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/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/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index f18b49a43..e4b5e681c 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -778,9 +778,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/astr_main_agent_resources.py b/astrbot/core/astr_main_agent_resources.py index b8eaf41d7..d0ef33b81 100644 --- a/astrbot/core/astr_main_agent_resources.py +++ b/astrbot/core/astr_main_agent_resources.py @@ -188,7 +188,12 @@ class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): @dataclass class SendMessageToUserTool(FunctionTool[AstrAgentContext]): name: str = "send_message_to_user" - description: str = "Directly send message to the user. Only use this tool when you need to proactively message the user. Otherwise you can directly output the reply in the conversation." + description: str = ( + "Send message to the user. " + "Supports various message types including `plain`, `image`, `record`, `video`, `file`, and `mention_user`. " + "Use this tool to send media files (`image`, `record`, `video`, `file`), " + "or when you need to proactively message the user(such as cron job). For normal text replies, you can output directly." + ) parameters: dict = Field( default_factory=lambda: { diff --git a/astrbot/core/computer/tools/neo_skills.py b/astrbot/core/computer/tools/neo_skills.py index 492b6e45e..e60648144 100644 --- a/astrbot/core/computer/tools/neo_skills.py +++ b/astrbot/core/computer/tools/neo_skills.py @@ -164,7 +164,10 @@ class CreateSkillPayloadTool(NeoSkillToolBase): "type": "object", "properties": { "payload": { - "anyOf": [{"type": "object"}, {"type": "array"}], + "anyOf": [ + {"type": "object"}, + {"type": "array", "items": {"type": "object"}}, + ], "description": ( "Skill payload JSON. Typical schema: {skill_markdown, inputs, outputs, meta}. " "This only stores content and returns payload_ref; it does not create a candidate or release." diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 41cc4510c..dd9383654 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 = [ @@ -1133,6 +1133,18 @@ CONFIG_METADATA_2 = { "proxy": "", "custom_headers": {}, }, + "MiniMax": { + "id": "minimax", + "provider": "minimax", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "api_base": "https://api.minimaxi.com/v1", + "timeout": 120, + "proxy": "", + "custom_headers": {}, + }, "xAI": { "id": "xai", "provider": "xai", diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index d12878be3..25a3a219c 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -332,9 +332,9 @@ class CronJobManager: cron_job=cron_job_str ) req.prompt = ( - "You are now responding to a scheduled task" + "You are now responding to a scheduled task. " "Proceed according to your system instructions. " - "Output using same language as previous conversation." + "Output using same language as previous conversation. " "After completing your task, summarize and output your actions and results." ) if not req.func_tool: 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..7e31536a1 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 @@ -385,6 +391,47 @@ class QQOfficialPlatformAdapter(Platform): else: msg.append(File(name=filename, file=url, url=url)) + @staticmethod + def _parse_face_message(content: str) -> str: + """Parse QQ official face message format and convert to readable text. + + QQ official face message format: + + + The ext field contains base64-encoded JSON with a 'text' field + describing the emoji (e.g., '[满头问号]'). + + Args: + content: The message content that may contain face tags. + + Returns: + Content with face tags replaced by readable emoji descriptions. + """ + import base64 + import json + import re + + def replace_face(match): + face_tag = match.group(0) + # Extract ext field from the face tag + ext_match = re.search(r'ext="([^"]*)"', face_tag) + if ext_match: + try: + ext_encoded = ext_match.group(1) + # Decode base64 and parse JSON + ext_decoded = base64.b64decode(ext_encoded).decode("utf-8") + ext_data = json.loads(ext_decoded) + emoji_text = ext_data.get("text", "") + if emoji_text: + return f"[表情:{emoji_text}]" + except Exception: + pass + # Fallback if parsing fails + return "[表情]" + + # Match face tags: + return re.sub(r"]*>", replace_face, content) + @staticmethod def _parse_from_qqofficial( message: botpy.message.Message @@ -410,7 +457,10 @@ class QQOfficialPlatformAdapter(Platform): abm.group_id = message.group_openid else: abm.sender = MessageMember(message.author.user_openid, "") - abm.message_str = message.content.strip() + # Parse face messages to readable text + abm.message_str = QQOfficialPlatformAdapter._parse_face_message( + message.content.strip() + ) abm.self_id = "unknown_selfid" msg.append(At(qq="qq_official")) msg.append(Plain(abm.message_str)) @@ -426,10 +476,12 @@ class QQOfficialPlatformAdapter(Platform): else: abm.self_id = "" - plain_content = message.content.replace( - "<@!" + str(abm.self_id) + ">", - "", - ).strip() + plain_content = QQOfficialPlatformAdapter._parse_face_message( + message.content.replace( + "<@!" + str(abm.self_id) + ">", + "", + ).strip() + ) QQOfficialPlatformAdapter._append_attachments(msg, message.attachments) abm.message = msg diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py index bcd05faf1..7af066020 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -1,5 +1,6 @@ import asyncio import logging +import time from typing import cast import quart @@ -39,6 +40,9 @@ class QQOfficialWebhook: self.client = botpy_client self.event_queue = event_queue self.shutdown_event = asyncio.Event() + # Deduplication cache for webhook retry callbacks. + self._seen_event_ids: dict[str, float] = {} + self._dedup_ttl: int = 60 # seconds async def initialize(self) -> None: logger.info("正在登录到 QQ 官方机器人...") @@ -106,6 +110,22 @@ class QQOfficialWebhook: print(signed) return signed + event_id = msg.get("id") + if event_id: + now = time.monotonic() + # Lazily evict expired entries to prevent unbounded growth. + expired = [ + k + for k, ts in self._seen_event_ids.items() + if now - ts > self._dedup_ttl + ] + for k in expired: + del self._seen_event_ids[k] + if event_id in self._seen_event_ids: + logger.debug(f"Duplicate webhook event {event_id!r}, skipping.") + return {"opcode": 12} + self._seen_event_ids[event_id] = now + if event and opcode == BotWebSocket.WS_DISPATCH_EVENT: event = msg["t"].lower() try: 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/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py index e75fb9214..f963969b7 100644 --- a/astrbot/core/platform/sources/telegram/tg_event.py +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -25,6 +25,16 @@ from astrbot.api.platform import AstrBotMessage, MessageType, PlatformMetadata from astrbot.core.utils.metrics import Metric +def _is_gif(path: str) -> bool: + if path.lower().endswith(".gif"): + return True + try: + with open(path, "rb") as f: + return f.read(6) in (b"GIF87a", b"GIF89a") + except OSError: + return False + + class TelegramPlatformEvent(AstrMessageEvent): # Telegram 的最大消息长度限制 MAX_MESSAGE_LENGTH = 4096 @@ -291,7 +301,13 @@ class TelegramPlatformEvent(AstrMessageEvent): await client.send_message(text=chunk, **cast(Any, payload)) elif isinstance(i, Image): image_path = await i.convert_to_file_path() - await client.send_photo(photo=image_path, **cast(Any, payload)) + if _is_gif(image_path): + send_coro = client.send_animation + media_kwarg = {"animation": image_path} + else: + send_coro = client.send_photo + media_kwarg = {"photo": image_path} + await send_coro(**media_kwarg, **cast(Any, payload)) elif isinstance(i, File): path = await i.get_file() name = i.name or os.path.basename(path) @@ -406,12 +422,20 @@ class TelegramPlatformEvent(AstrMessageEvent): on_text(i.text) elif isinstance(i, Image): image_path = await i.convert_to_file_path() + if _is_gif(image_path): + action = ChatAction.UPLOAD_VIDEO + send_coro = self.client.send_animation + media_kwarg = {"animation": image_path} + else: + action = ChatAction.UPLOAD_PHOTO + send_coro = self.client.send_photo + media_kwarg = {"photo": image_path} await self._send_media_with_action( self.client, - ChatAction.UPLOAD_PHOTO, - self.client.send_photo, + action, + send_coro, user_name=user_name, - photo=image_path, + **media_kwarg, **cast(Any, payload), ) elif isinstance(i, File): diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py index 62f236b57..79fe6f8ed 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py @@ -440,9 +440,16 @@ class WecomAIBotAdapter(Platform): ) def _extract_session_id(self, message_data: dict[str, Any]) -> str: - """从消息数据中提取会话ID""" - user_id = message_data.get("from", {}).get("userid", "default_user") - return format_session_id("wecomai", user_id) + """从消息数据中提取会话ID + 群聊使用 chatid,单聊使用 userid + """ + chattype = message_data.get("chattype", "single") + if chattype == "group": + chat_id = message_data.get("chatid", "default_group") + return format_session_id("wecomai", chat_id) + else: + user_id = message_data.get("from", {}).get("userid", "default_user") + return format_session_id("wecomai", user_id) async def _enqueue_message( self, diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 520b36cd4..0df9f791a 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -808,6 +808,8 @@ class ProviderManager: config.save_config() # load instance await self.load_provider(new_config) + # sync in-memory config for API queries (e.g., embedding provider list) + self.providers_config = astrbot_config["provider"] async def terminate(self) -> None: if self._mcp_init_task and not self._mcp_init_task.done(): diff --git a/astrbot/core/provider/sources/groq_source.py b/astrbot/core/provider/sources/groq_source.py index fcc8f238f..af4029f67 100644 --- a/astrbot/core/provider/sources/groq_source.py +++ b/astrbot/core/provider/sources/groq_source.py @@ -13,3 +13,11 @@ class ProviderGroq(ProviderOpenAIOfficial): ) -> None: super().__init__(provider_config, provider_settings) self.reasoning_key = "reasoning" + + def _finally_convert_payload(self, payloads: dict) -> None: + """Groq rejects assistant history items that include reasoning_content.""" + super()._finally_convert_payload(payloads) + for message in payloads.get("messages", []): + if message.get("role") == "assistant": + message.pop("reasoning_content", None) + message.pop("reasoning", None) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index adee24073..2fae94e1a 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -311,7 +311,7 @@ class ProviderOpenAIOfficial(Provider): state.handle_chunk(chunk) except Exception as e: logger.warning("Saving chunk state error: " + str(e)) - if len(chunk.choices) == 0: + if not chunk.choices: continue delta = chunk.choices[0].delta # logger.debug(f"chunk delta: {delta}") @@ -322,7 +322,7 @@ class ProviderOpenAIOfficial(Provider): if reasoning: llm_response.reasoning_content = reasoning _y = True - if delta.content: + if delta and delta.content: # Don't strip streaming chunks to preserve spaces between words completion_text = self._normalize_content(delta.content, strip=False) llm_response.result_chain = MessageChain( @@ -345,7 +345,7 @@ class ProviderOpenAIOfficial(Provider): ) -> str: """Extract reasoning content from OpenAI ChatCompletion if available.""" reasoning_text = "" - if len(completion.choices) == 0: + if not completion.choices: return reasoning_text if isinstance(completion, ChatCompletion): choice = completion.choices[0] @@ -468,7 +468,7 @@ class ProviderOpenAIOfficial(Provider): """Parse OpenAI ChatCompletion into LLMResponse""" llm_response = LLMResponse("assistant") - if len(completion.choices) == 0: + if not completion.choices: raise Exception("API 返回的 completion 为空。") choice = completion.choices[0] @@ -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 d504f128a..b939b53fa 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/umop_config_router.py b/astrbot/core/umop_config_router.py index d8b010d50..c2588e6c2 100644 --- a/astrbot/core/umop_config_router.py +++ b/astrbot/core/umop_config_router.py @@ -25,12 +25,22 @@ class UmopConfigRouter: ) self.umop_to_conf_id = sp_data + @staticmethod + def _split_umo(umo: str) -> tuple[str, str, str] | None: + """将 UMO 拆分为 3 个部分,同时保留 session_id 中的 ':'""" + if not isinstance(umo, str): + return None + parts = umo.split(":", 2) + if len(parts) != 3: + return None + return parts[0], parts[1], parts[2] + def _is_umo_match(self, p1: str, p2: str) -> bool: """判断 p2 umo 是否逻辑包含于 p1 umo""" - p1_ls = p1.split(":") - p2_ls = p2.split(":") + p1_ls = self._split_umo(p1) + p2_ls = self._split_umo(p2) - if len(p1_ls) != 3 or len(p2_ls) != 3: + if p1_ls is None or p2_ls is None: return False # 非法格式 return all(p == "" or fnmatch.fnmatchcase(t, p) for p, t in zip(p1_ls, p2_ls)) @@ -62,7 +72,7 @@ class UmopConfigRouter: """ for part in new_routing: - if not isinstance(part, str) or len(part.split(":")) != 3: + if self._split_umo(part) is None: raise ValueError( "umop keys must be strings in the format [platform_id]:[message_type]:[session_id], with optional wildcards * or empty for all", ) @@ -81,7 +91,7 @@ class UmopConfigRouter: ValueError: 如果 umo 格式不正确 """ - if not isinstance(umo, str) or len(umo.split(":")) != 3: + if self._split_umo(umo) is None: raise ValueError( "umop must be a string in the format [platform_id]:[message_type]:[session_id], with optional wildcards * or empty for all", ) @@ -99,7 +109,7 @@ class UmopConfigRouter: ValueError: 当 umo 格式不正确时抛出 """ - if not isinstance(umo, str) or len(umo.split(":")) != 3: + if self._split_umo(umo) is None: raise ValueError( "umop must be a string in the format [platform_id]:[message_type]:[session_id], with optional wildcards * or empty for all", ) 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/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..c79ad1e35 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -36,6 +36,20 @@ async def track_conversation(convs: dict, conv_id: str): convs.pop(conv_id, None) +async def _poll_webchat_stream_result(back_queue, username: str): + try: + result = await asyncio.wait_for(back_queue.get(), timeout=1) + except asyncio.TimeoutError: + return None, False + except asyncio.CancelledError: + logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") + return None, True + except Exception as e: + logger.error(f"WebChat stream error: {e}") + return None, False + return result, False + + class ChatRoute(Route): def __init__( self, @@ -51,6 +65,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, @@ -342,16 +357,12 @@ class ChatRoute(Route): async with track_conversation(self.running_convs, webchat_conv_id): while True: - try: - result = await asyncio.wait_for(back_queue.get(), timeout=1) - except asyncio.TimeoutError: - continue - except asyncio.CancelledError: - logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") + result, should_break = await _poll_webchat_stream_result( + back_queue, username + ) + if should_break: client_disconnected = True - except Exception as e: - logger.error(f"WebChat stream error: {e}") - + break if not result: continue @@ -578,19 +589,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 +633,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/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/package.json b/dashboard/package.json index 7b4a7f071..9d6b7f866 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", @@ -36,9 +36,8 @@ "remixicon": "3.5.0", "shiki": "^3.20.0", "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 +53,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 +63,15 @@ "sass": "1.66.1", "sass-loader": "13.3.2", "typescript": "5.1.6", - "vite": "4.4.9", + "vite": "6.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..74f3eb9b3 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) @@ -77,15 +81,12 @@ importers: stream-markdown: specifier: ^0.0.13 version: 0.0.13(shiki@3.22.0) - stream-monaco: - specifier: ^0.0.17 - version: 0.0.17(monaco-editor@0.52.2) vee-validate: 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 +104,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 +128,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 +158,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 +214,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 +485,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 +908,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 +981,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 +1081,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 +1157,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 +1183,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 +1219,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 +1269,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 +1562,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 +1608,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 +1721,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 +1872,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 +2006,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 +2047,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 +2163,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 +2257,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 +2316,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 +2328,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 +2343,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 +2393,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 +2438,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 +2455,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 +2499,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 +2615,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 +2646,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 +2752,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 +2914,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 +2982,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 +2995,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 +3193,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': @@ -3014,6 +3297,7 @@ snapshots: '@shikijs/core': 3.22.0 '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 + optional: true '@shikijs/themes@3.22.0': dependencies: @@ -3121,16 +3405,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 +3577,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 +3616,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 +3713,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 +3861,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 +3951,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 +3961,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,14 +3983,15 @@ 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 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alien-signals@2.0.8: {} + alien-signals@2.0.8: + optional: true ansi-regex@5.0.1: {} @@ -3723,13 +4020,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 +4036,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 +4059,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 +4076,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001769: {} + caniuse-lite@1.0.30001778: {} ccount@2.0.1: {} @@ -3806,7 +4103,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 +4385,7 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.3.1: + dompurify@3.3.2: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -4098,11 +4395,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 +4425,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 +4587,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 +4746,7 @@ snapshots: ignore@5.3.2: {} - immutable@4.3.7: {} + immutable@4.3.8: {} import-fresh@3.3.1: dependencies: @@ -4487,7 +4792,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 +4861,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 +4897,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 +4954,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 +5021,7 @@ snapshots: neo-async@2.6.2: {} - node-releases@2.0.27: {} + node-releases@2.0.36: {} normalize-path@3.0.0: {} @@ -4799,6 +5102,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 +5154,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 +5174,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 +5201,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 +5267,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 +5305,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 +5351,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 +5363,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 +5375,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 @@ -5120,6 +5442,7 @@ snapshots: alien-signals: 2.0.8 monaco-editor: 0.52.2 shiki: 3.22.0 + optional: true stringify-entities@4.0.4: dependencies: @@ -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/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index 7c25e1bc3..631b9d600 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" /> @@ -71,13 +74,14 @@ :stagedImagesUrl="stagedImagesUrl" :stagedAudioUrl="stagedAudioUrl" :stagedFiles="stagedNonImageFiles" - :disabled="isStreaming" + :disabled="false" :is-running="isStreaming || isConvRunning" :enableStreaming="enableStreaming" :isRecording="isRecording" :session-id="currSessionId || null" :current-session="getCurrentSession" :replyTo="replyTo" + :send-shortcut="sendShortcut" @send="handleSendMessage" @stop="handleStopMessage" @toggleStreaming="toggleStreaming" @@ -102,13 +106,14 @@ :stagedImagesUrl="stagedImagesUrl" :stagedAudioUrl="stagedAudioUrl" :stagedFiles="stagedNonImageFiles" - :disabled="isStreaming" + :disabled="false" :is-running="isStreaming || isConvRunning" :enableStreaming="enableStreaming" :isRecording="isRecording" :session-id="currSessionId || null" :current-session="getCurrentSession" :replyTo="replyTo" + :send-shortcut="sendShortcut" @send="handleSendMessage" @stop="handleStopMessage" @toggleStreaming="toggleStreaming" @@ -132,13 +137,14 @@ :stagedImagesUrl="stagedImagesUrl" :stagedAudioUrl="stagedAudioUrl" :stagedFiles="stagedNonImageFiles" - :disabled="isStreaming" + :disabled="false" :is-running="isStreaming || isConvRunning" :enableStreaming="enableStreaming" :isRecording="isRecording" :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,18 @@ 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 focusChatInput() { + nextTick(() => { + chatInputRef.value?.focusInput?.(); + }); +} // 检测是否为手机端 function checkMobile() { @@ -488,6 +511,7 @@ async function handleSelectConversation(sessionIds: string[]) { nextTick(() => { messageList.value?.scrollToBottom(); }); + focusChatInput(); } function handleNewChat() { @@ -497,6 +521,7 @@ function handleNewChat() { // 退出项目视图 selectedProjectId.value = null; projectSessions.value = []; + focusChatInput(); } async function handleDeleteConversation(sessionId: string) { @@ -510,6 +535,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); @@ -627,6 +679,11 @@ async function handleSendMessage() { const selectedProviderId = selection?.providerId || ''; const selectedModelName = selection?.modelName || ''; + // 点击发送后立即将消息区滚到底部,确保用户看到最新消息 + nextTick(() => { + messageList.value?.scrollToBottom(); + }); + await sendMsg( promptToSend, filesToSend, @@ -636,6 +693,11 @@ async function handleSendMessage() { replyToSend ); + // 发送流程结束后再兜底一次,处理异步渲染场景 + nextTick(() => { + messageList.value?.scrollToBottom(); + }); + // 如果在项目中创建了新会话,将其添加到项目 if (isCreatingNewSession && currentProjectId && currSessionId.value) { await addSessionToProject(currSessionId.value, currentProjectId); @@ -694,6 +756,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..aff404958 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) { @@ -364,6 +373,11 @@ function getCurrentSelection() { return providerModelMenuRef.value?.getCurrentSelection(); } +function focusInput() { + if (!inputField.value) return; + inputField.value.focus(); +} + onMounted(() => { if (inputField.value) { inputField.value.addEventListener('paste', handlePaste); @@ -379,7 +393,8 @@ onBeforeUnmount(() => { }); defineExpose({ - getCurrentSelection + getCurrentSelection, + focusInput }); @@ -399,8 +414,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 +434,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 +454,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') }} -