diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index ed25cd058..54cf0779f 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -13,7 +13,7 @@ jobs: - name: checkout uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.7 with: version: 10.28.2 - name: Setup Node.js diff --git a/.github/workflows/dashboard_ci.yml b/.github/workflows/dashboard_ci.yml index 521edcc41..09b2f5bed 100644 --- a/.github/workflows/dashboard_ci.yml +++ b/.github/workflows/dashboard_ci.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.7 with: version: 10.28.2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8304e277f..c04543519 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.7 with: version: 10.28.2 diff --git a/AGENTS.md b/AGENTS.md index 695c338de..7de7832ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -248,3 +248,10 @@ chore: maintenance tasks 1. Create in `astrbot/builtin_stars/` or as plugin 2. Extend `Star` class 3. Use decorators: `@star.on_command()`, `@star.on_schedule()`, etc. + +## Release versions + +1. Replace current version name to specific version name. +2. Write changelog in `changelogs/`, you can refer to the full commit messages between the latest tag to the latest commit. +3. Make and push a commit into master branch with message format like: `chore: bump version to 4.25.0` +4. Create a tag and push the tag. For example: `git tag v4.25.0 && git push origin v4.25.0` \ No newline at end of file diff --git a/README.md b/README.md index b566b454d..3a6b56554 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@
-Soulter%2FAstrBot | Trendshift +AstrBotDevs%2FAstrBot | Trendshift Featured|HelloGitHub
diff --git a/README_fr.md b/README_fr.md index 4b9e978d4..a77c65721 100644 --- a/README_fr.md +++ b/README_fr.md @@ -11,7 +11,7 @@
-Soulter%2FAstrBot | Trendshift +AstrBotDevs%2FAstrBot | Trendshift Featured|HelloGitHub
diff --git a/README_ja.md b/README_ja.md index 849fab695..c78b57106 100644 --- a/README_ja.md +++ b/README_ja.md @@ -11,7 +11,7 @@
-Soulter%2FAstrBot | Trendshift +AstrBotDevs%2FAstrBot | Trendshift Featured|HelloGitHub
diff --git a/README_ru.md b/README_ru.md index 044120320..476ff6d7c 100644 --- a/README_ru.md +++ b/README_ru.md @@ -11,7 +11,7 @@
-Soulter%2FAstrBot | Trendshift +AstrBotDevs%2FAstrBot | Trendshift Featured|HelloGitHub
diff --git a/README_zh-TW.md b/README_zh-TW.md index 2aaedfabb..fdeedfbc8 100644 --- a/README_zh-TW.md +++ b/README_zh-TW.md @@ -11,7 +11,7 @@
-Soulter%2FAstrBot | Trendshift +AstrBotDevs%2FAstrBot | Trendshift Featured|HelloGitHub
diff --git a/README_zh.md b/README_zh.md index 1b55b171a..f07972637 100644 --- a/README_zh.md +++ b/README_zh.md @@ -9,7 +9,7 @@ Русский
-Soulter%2FAstrBot | Trendshift +AstrBotDevs%2FAstrBot | Trendshift Featured|HelloGitHub
diff --git a/astrbot/builtin_stars/astrbot/main.py b/astrbot/builtin_stars/astrbot/main.py index 3390fdeca..06108636b 100644 --- a/astrbot/builtin_stars/astrbot/main.py +++ b/astrbot/builtin_stars/astrbot/main.py @@ -1,5 +1,6 @@ import copy import traceback +from collections.abc import Iterable from sys import maxsize import astrbot.api.message_components as Comp @@ -19,6 +20,13 @@ from astrbot.core.utils.session_waiter import ( from .long_term_memory import LongTermMemory +def _iter_message_components(event: AstrMessageEvent): + messages = getattr(getattr(event, "message_obj", None), "message", None) + if not isinstance(messages, Iterable) or isinstance(messages, (str, bytes)): + return () + return tuple(messages) + + class Main(star.Star): def __init__(self, context: star.Context) -> None: self.context = context @@ -134,8 +142,9 @@ class Main(star.Star): @filter.platform_adapter_type(filter.PlatformAdapterType.ALL) async def on_message(self, event: AstrMessageEvent): """群聊记忆增强""" + message_components = _iter_message_components(event) has_image_or_plain = False - for comp in event.message_obj.message: + for comp in message_components: if isinstance(comp, Plain) or isinstance(comp, Image): has_image_or_plain = True break @@ -177,6 +186,13 @@ class Main(star.Star): ) prompt = event.message_str + image_urls = [] + for comp in message_components: + if isinstance(comp, Image): + try: + image_urls.append(await comp.convert_to_file_path()) + except Exception: + logger.exception("主动回复处理图片失败") if not conv: logger.error("未找到对话,无法主动回复") @@ -185,6 +201,7 @@ class Main(star.Star): yield event.request_llm( prompt=prompt, session_id=event.session_id, + image_urls=image_urls, conversation=conv, ) except BaseException as e: diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index 0aa205bf0..3a4247e39 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -73,6 +73,7 @@ async def _clear_third_party_agent_runner_state( context: 星尘上下文。 session_id: 会话 ID (unified_msg_origin)。 provider_type: 提供商类型 (如 deerflow)。 + """ provider_config = context.provider_manager.get_provider_config_by_id( provider_type, @@ -349,7 +350,7 @@ class ConversationCommands: if not cid: message.set_result( MessageEventResult().message( - "❌ You are not in a conversation. Use /new to create one." + "❌ You are not in a conversation. Use /new to create one.", ), ) return @@ -373,14 +374,14 @@ class ConversationCommands: ).where( col(ProviderStat.agent_type) == "internal", col(ProviderStat.conversation_id) == cid, - ) + ), ) stats = result.one() if stats.record_count == 0: message.set_result( MessageEventResult().message( - "📊 No stats available for this conversation yet." + "📊 No stats available for this conversation yet.", ), ) return diff --git a/astrbot/cli/__init__.py b/astrbot/cli/__init__.py index 28ee08a40..bf4d023cf 100644 --- a/astrbot/cli/__init__.py +++ b/astrbot/cli/__init__.py @@ -1 +1 @@ -__version__ = "4.24.2" +__version__ = "4.25.1" diff --git a/astrbot/cli/commands/cmd_conf.py b/astrbot/cli/commands/cmd_conf.py index e2cd725e7..6d50d19af 100644 --- a/astrbot/cli/commands/cmd_conf.py +++ b/astrbot/cli/commands/cmd_conf.py @@ -23,6 +23,7 @@ from astrbot.core.utils.auth_password import ( _is_argon2_hash, _is_pbkdf2_hash, hash_dashboard_password, + hash_legacy_dashboard_password, is_legacy_dashboard_password, validate_dashboard_password, ) @@ -71,8 +72,8 @@ def _validate_dashboard_password(value: str) -> str: validate_dashboard_password(value) except ValueError as e: raise click.ClickException(str(e)) from e - # Return the canonical stored representation. - return hash_dashboard_password(value) + # Return the plaintext value; callers hash it before storage. + return value def _validate_timezone(value: str) -> str: @@ -200,10 +201,16 @@ def set_dashboard_credentials( "Please provide the plaintext password (it will be hashed securely), " "or provide an Argon2-encoded hash string.", ) + validated = _validate_dashboard_password(password_hash) + _set_nested_item( + config, + "dashboard.pbkdf2_password", + hash_dashboard_password(validated), + ) _set_nested_item( config, "dashboard.password", - _validate_dashboard_password(password_hash), + hash_legacy_dashboard_password(validated), ) @@ -237,7 +244,19 @@ def set_config(key: str, value: str) -> None: old_value = "" validated_value = CONFIG_VALIDATORS[key](value) - _set_nested_item(config, key, validated_value) + if key == "dashboard.password": + _set_nested_item( + config, + "dashboard.pbkdf2_password", + hash_dashboard_password(validated_value), + ) + _set_nested_item( + config, + "dashboard.password", + hash_legacy_dashboard_password(validated_value), + ) + else: + _set_nested_item(config, key, validated_value) _save_config(config) click.echo(f"Config updated: {key}") diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index bffb5f99b..4d4e51270 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -13,6 +13,18 @@ from astrbot.core.utils.astrbot_path import astrbot_paths from .cmd_conf import ensure_config_file, set_dashboard_credentials +DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD" + + +def _initialize_config_from_env(astrbot_root: Path) -> None: + if DASHBOARD_INITIAL_PASSWORD_ENV not in os.environ: + return + + from astrbot.core.config.astrbot_config import AstrBotConfig + + AstrBotConfig(config_path=str(astrbot_root / "data" / "cmd_config.json")) + click.echo("Initialized data/cmd_config.json with dashboard initial password.") + async def initialize_astrbot( astrbot_root: Path, @@ -51,6 +63,9 @@ async def initialize_astrbot( path.mkdir(parents=True, exist_ok=True) status = "Created" if not path.exists() else "Exists" click.echo(f" [{status}] {name.title()}: {path}") + + _initialize_config_from_env(astrbot_root) + config_path = astrbot_root / "data" / "cmd_config.json" if not config_path.exists(): config_path.write_text( diff --git a/astrbot/core/agent/message.py b/astrbot/core/agent/message.py index e8bf20cdb..985b4f235 100644 --- a/astrbot/core/agent/message.py +++ b/astrbot/core/agent/message.py @@ -38,7 +38,9 @@ class ContentPart(BaseModel): @classmethod def __get_pydantic_core_schema__( - cls, source_type: Any, handler: GetCoreSchemaHandler + cls, + source_type: Any, + handler: GetCoreSchemaHandler, ) -> core_schema.CoreSchema: # If we're dealing with the base ContentPart class, use custom validation if cls.__name__ == "ContentPart": @@ -50,12 +52,12 @@ class ContentPart(BaseModel): # if it's a dict with a type field, dispatch to the appropriate subclass if isinstance(value, dict) and "type" in value: - type_value: Any | None = cast(dict[str, Any], value).get("type") + type_value: Any | None = cast("dict[str, Any]", value).get("type") if not isinstance(type_value, str): raise ValueError(f"Cannot validate {value} as ContentPart") target_class = cls.__content_part_registry[type_value] part = target_class.model_validate(value) - if cast(dict[str, Any], value).get("_no_save"): + if cast("dict[str, Any]", value).get("_no_save"): part._no_save = True return part @@ -79,8 +81,7 @@ class ContentPart(BaseModel): class TextPart(ContentPart): - """ - >>> TextPart(text="Hello, world!").model_dump() + """>>> TextPart(text="Hello, world!").model_dump() {'type': 'text', 'text': 'Hello, world!'} """ @@ -89,8 +90,7 @@ class TextPart(ContentPart): class ThinkPart(ContentPart): - """ - >>> ThinkPart(think="I think I need to think about this.").model_dump() + """>>> ThinkPart(think="I think I need to think about this.").model_dump() {'type': 'think', 'think': 'I think I need to think about this.', 'encrypted': None} """ @@ -111,8 +111,7 @@ class ThinkPart(ContentPart): class ImageURLPart(ContentPart): - """ - >>> ImageURLPart(image_url="http://example.com/image.jpg").model_dump() + """>>> ImageURLPart(image_url="http://example.com/image.jpg").model_dump() {'type': 'image_url', 'image_url': 'http://example.com/image.jpg'} """ @@ -127,8 +126,7 @@ class ImageURLPart(ContentPart): class AudioURLPart(ContentPart): - """ - >>> AudioURLPart(audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")).model_dump() + """>>> AudioURLPart(audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")).model_dump() {'type': 'audio_url', 'audio_url': {'url': 'https://example.com/audio.mp3', 'id': None}} """ @@ -143,8 +141,7 @@ class AudioURLPart(ContentPart): class ToolCall(BaseModel): - """ - A tool call requested by the assistant. + """A tool call requested by the assistant. >>> ToolCall( ... id="123", @@ -233,7 +230,7 @@ class Message(BaseModel): # other all cases: content is required if self.content is None: raise ValueError( - "content is required unless role='assistant' and tool_calls is not None" + "content is required unless role='assistant' and tool_calls is not None", ) return self @@ -357,6 +354,8 @@ def dump_messages_with_checkpoints(messages: list[Message]) -> list[dict]: dumped.append(message_data) if message._checkpoint_after is not None: dumped.append( - CheckpointMessageSegment(content=message._checkpoint_after).model_dump() + CheckpointMessageSegment( + content=message._checkpoint_after, + ).model_dump(), ) return dumped diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 132ca8192..544443377 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -83,7 +83,8 @@ class _HandleFunctionToolsResult: @classmethod def from_tool_call_result_blocks( - cls, blocks: list[ToolCallMessageSegment] + cls, + blocks: list[ToolCallMessageSegment], ) -> "_HandleFunctionToolsResult": return cls(kind="tool_call_result_blocks", tool_call_result_blocks=blocks) @@ -188,7 +189,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) @@ -400,7 +401,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): return content estimated_tokens = self._tool_result_token_counter.count_tokens( - [Message(role="tool", content=content, tool_call_id=tool_call_id)] + [Message(role="tool", content=content, tool_call_id=tool_call_id)], ) if estimated_tokens <= self.TOOL_RESULT_MAX_ESTIMATED_TOKENS: return content @@ -445,7 +446,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): preview = content while preview: estimated_tokens = self._tool_result_token_counter.count_tokens( - [Message(role="tool", content=preview, tool_call_id=tool_call_id)] + [Message(role="tool", content=preview, tool_call_id=tool_call_id)], ) if estimated_tokens <= self.TOOL_RESULT_PREVIEW_MAX_ESTIMATED_TOKENS: return preview @@ -456,7 +457,9 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): return preview async def _iter_llm_responses( - self, *, include_model: bool = True + self, + *, + include_model: bool = True, ) -> T.AsyncGenerator[LLMResponse, None]: """Yields chunks *and* a final LLMResponse.""" payload = { @@ -512,7 +515,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): with attempt: try: async for resp in self._iter_llm_responses( - include_model=idx == 0 + include_model=idx == 0, ): if resp.is_chunk: has_stream_output = True @@ -708,7 +711,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): token_usage = self.req.conversation.token_usage if self.req.conversation else 0 self._simple_print_message_role("[BefCompact]") self.run_context.messages = await self.context_manager.process( - self.run_context.messages, trusted_token_usage=token_usage + self.run_context.messages, + trusted_token_usage=token_usage, ) self._simple_print_message_role("[AftCompact]") @@ -811,7 +815,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): llm_resp, _ = await self._resolve_tool_exec(llm_resp) if not llm_resp.tools_call_name: logger.warning( - "skills_like tool re-query returned no tool calls; fallback to assistant response." + "skills_like tool re-query returned no tool calls; fallback to assistant response.", ) if llm_resp.result_chain: yield AgentResponse( @@ -863,7 +867,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) @@ -878,7 +882,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ) # record the assistant message with tool calls self.run_context.messages.extend( - tool_calls_result.to_openai_messages_model() + tool_calls_result.to_openai_messages_model(), ) # If there are cached images and the model supports image input, @@ -891,35 +895,37 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): image_parts = [] for cached_img in cached_images: img_data = tool_image_cache.get_image_base64_by_path( - cached_img.file_path, cached_img.mime_type + cached_img.file_path, + cached_img.mime_type, ) if img_data: base64_data, mime_type = img_data image_parts.append( TextPart( - text=f"[Image from tool '{cached_img.tool_name}', path='{cached_img.file_path}']" - ) + text=f"[Image from tool '{cached_img.tool_name}', path='{cached_img.file_path}']", + ), ) image_parts.append( ImageURLPart( image_url=ImageURLPart.ImageURL( url=f"data:{mime_type};base64,{base64_data}", id=cached_img.file_path, - ) - ) + ), + ), ) if image_parts: self.run_context.messages.append( - Message(role="user", content=image_parts) + Message(role="user", content=image_parts), ) logger.debug( - f"Appended {len(cached_images)} cached image(s) to context for LLM review" + f"Appended {len(cached_images)} cached image(s) to context for LLM review", ) self.req.append_tool_calls_result(tool_calls_result) async def step_until_done( - self, max_step: int + self, + max_step: int, ) -> T.AsyncGenerator[AgentResponse, None]: """Process steps until the agent is done.""" step_count = 0 @@ -931,7 +937,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): # 如果循环结束了但是 agent 还没有完成,说明是达到了 max_step if not self.done(): logger.warning( - f"Agent reached max steps ({max_step}), forcing a final response." + f"Agent reached max steps ({max_step}), forcing a final response.", ) # 拔掉所有工具 if self.req: @@ -941,7 +947,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): Message( role="user", content=self.MAX_STEPS_REACHED_PROMPT, - ) + ), ) # 再执行最后一步 async for resp in self.step(): @@ -982,10 +988,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): "name": func_tool_name, "args": func_tool_args, "ts": time.time(), - } - ) + }, + ), ], - ) + ), ) try: if not req.func_tool: @@ -1003,6 +1009,9 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool = req.func_tool.get_tool(func_tool_name) available_tools = req.func_tool.names() + # Some API may return None for tools with no parameters + if func_tool_args is None: + func_tool_args = {} logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}") if not func_tool: @@ -1084,11 +1093,11 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): result_parts.append( f"Image returned and cached at path='{cached_img.file_path}'. " f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." + f"with type='image' and path='{cached_img.file_path}'.", ) # Yield image info for LLM visibility (will be handled in step()) yield _HandleFunctionToolsResult.from_cached_image( - cached_img + cached_img, ) elif isinstance(content_item, EmbeddedResource): resource = content_item.resource @@ -1110,15 +1119,15 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): result_parts.append( f"Image returned and cached at path='{cached_img.file_path}'. " f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." + f"with type='image' and path='{cached_img.file_path}'.", ) # Yield image info for LLM visibility yield _HandleFunctionToolsResult.from_cached_image( - cached_img + cached_img, ) else: result_parts.append( - "The tool has returned a data type that is not supported." + "The tool has returned a data type that is not supported.", ) if result_parts: inline_result = "\n\n".join(result_parts) @@ -1130,7 +1139,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, inline_result + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, + tool_call_streak, ), ) @@ -1139,7 +1149,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): # 这里我们将直接结束 Agent Loop # 发送消息逻辑在 ToolExecutor 中处理了 logger.warning( - f"{func_tool_name} 没有返回值,或者已将结果直接发送给用户。" + f"{func_tool_name} 没有返回值,或者已将结果直接发送给用户。", ) self._transition_state(AgentState.DONE) self.stats.end_time = time.time() @@ -1147,7 +1157,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, "The tool has no return value, or has sent the result directly to the user." + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, + tool_call_streak, ), ) else: @@ -1159,7 +1170,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, "*The tool has returned an unsupported type. Please tell the user to check the definition and implementation of this tool.*" + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, + tool_call_streak, ), ) @@ -1180,7 +1192,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, f"error: {e!s}" + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, + tool_call_streak, ), ) @@ -1196,17 +1209,17 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): "id": func_tool_id, "ts": time.time(), "result": last_tcr_content, - } - ) + }, + ), ], - ) + ), ) logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}") # 处理函数调用响应 if tool_call_result_blocks: yield _HandleFunctionToolsResult.from_tool_call_result_blocks( - tool_call_result_blocks + tool_call_result_blocks, ) def _build_tool_requery_context( @@ -1222,7 +1235,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): elif isinstance(msg, dict): contexts.append(copy.deepcopy(msg)) instruction = self.SKILLS_LIKE_REQUERY_INSTRUCTION_TEMPLATE.format( - tool_names=", ".join(tool_names) + tool_names=", ".join(tool_names), ) if extra_instruction: instruction = f"{instruction}\n{extra_instruction}" @@ -1265,7 +1278,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): if isinstance(self._tool_schema_param_set, ToolSet): param_subset = self._build_tool_subset( - self._tool_schema_param_set, tool_names + self._tool_schema_param_set, + tool_names, ) if param_subset.tools and tool_names: contexts = self._build_tool_requery_context(tool_names) @@ -1289,7 +1303,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): and not self._has_meaningful_assistant_reply(llm_resp) ): logger.warning( - "skills_like tool re-query returned no tool calls and no explanation; retrying with stronger instruction." + "skills_like tool re-query returned no tool calls and no explanation; retrying with stronger instruction.", ) repair_contexts = self._build_tool_requery_context( tool_names, @@ -1348,7 +1362,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) @@ -1381,7 +1395,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): if self._is_stop_requested(): await self._close_executor(executor) raise _ToolExecutionInterrupted( - "Tool execution interrupted before reading the next tool result." + "Tool execution interrupted before reading the next tool result.", ) next_result_task = asyncio.create_task(anext(executor)) @@ -1401,7 +1415,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): await self._close_executor(executor) raise _ToolExecutionInterrupted( - "Tool execution interrupted by a stop request." + "Tool execution interrupted by a stop request.", ) try: diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 6c37f1a83..a7ad4b153 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -237,6 +237,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): Returns: Dict mapping tool name to FunctionTool instance. + """ from astrbot.core.computer.computer_tool_provider import ( ComputerToolProvider, diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index da590f5be..b91ec3a23 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -112,7 +112,8 @@ from astrbot.core.utils.string_utils import normalize_and_dedupe_strings @dataclass(slots=True) class MainAgentBuildConfig: """The main agent build configuration. - Most of the configs can be found in the cmd_config.json""" + Most of the configs can be found in the cmd_config.json + """ tool_call_timeout: int """The timeout (in seconds) for a tool call. @@ -178,7 +179,8 @@ class MainAgentBuildResult: def _select_provider( - event: AstrMessageEvent, plugin_context: Context + event: AstrMessageEvent, + plugin_context: Context, ) -> Provider | None: """Select chat provider for the event.""" sel_provider = event.get_extra("selected_provider") @@ -188,7 +190,8 @@ def _select_provider( logger.error("未找到指定的提供商: %s。", sel_provider) if not isinstance(provider, Provider): logger.error( - "选择的提供商类型无效(%s),跳过 LLM 请求处理。", type(provider) + "选择的提供商类型无效(%s),跳过 LLM 请求处理。", + type(provider), ) return None return provider @@ -200,7 +203,8 @@ def _select_provider( async def _get_session_conv( - event: AstrMessageEvent, plugin_context: Context + event: AstrMessageEvent, + plugin_context: Context, ) -> Conversation: conv_mgr = plugin_context.conversation_manager umo = event.unified_msg_origin @@ -223,7 +227,7 @@ async def _apply_kb( config: MainAgentBuildConfig, ) -> None: if not config.kb_agentic_mode: - if req.prompt is None: + if req.prompt is None or not req.prompt.strip(): return try: kb_result = await retrieve_knowledge_base( @@ -244,8 +248,8 @@ async def _apply_kb( req.func_tool = ToolSet() req.func_tool.add_tool( plugin_context.get_llm_tool_manager().get_builtin_tool( - KnowledgeBaseQueryTool - ) + KnowledgeBaseQueryTool, + ), ) @@ -280,7 +284,7 @@ async def _apply_file_extract( config.file_extract_msh_api_key, ) for file_path in file_paths - ] + ], ) else: logger.error("Unsupported file extract provider: %s", config.file_extract_prov) @@ -429,7 +433,8 @@ async def _ensure_persona_and_skills( ) set_persona_custom_error_message_on_event( - event, extract_persona_custom_error_message_from_persona(persona) + event, + extract_persona_custom_error_message_from_persona(persona), ) # Ensure system_prompt is a string before any += @@ -515,7 +520,7 @@ async def _ensure_persona_and_skills( tool.name for tool in tmgr.func_list if not isinstance(tool, HandoffTool) - ] + ], ) continue if not isinstance(tools, list): @@ -607,7 +612,7 @@ async def _ensure_img_caption( ) if caption: req.extra_user_content_parts.append( - TextPart(text=f"{caption}") + TextPart(text=f"{caption}"), ) req.image_urls = [] except Exception as exc: # noqa: BLE001 @@ -619,19 +624,19 @@ async def _ensure_img_caption( def _append_quoted_image_attachment(req: ProviderRequest, image_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Image Attachment in quoted message: path {image_path}]") + TextPart(text=f"[Image Attachment in quoted message: path {image_path}]"), ) def _append_audio_attachment(req: ProviderRequest, audio_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Audio Attachment: path {audio_path}]") + TextPart(text=f"[Audio Attachment: path {audio_path}]"), ) def _append_quoted_audio_attachment(req: ProviderRequest, audio_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Audio Attachment in quoted message: path {audio_path}]") + TextPart(text=f"[Audio Attachment in quoted message: path {audio_path}]"), ) @@ -787,7 +792,7 @@ async def _process_quote_message( ) if llm_resp.completion_text: content_parts.append( - f"[Image Caption in quoted message]: {llm_resp.completion_text}" + f"[Image Caption in quoted message]: {llm_resp.completion_text}", ) else: logger.warning("No provider found for image captioning in quote.") @@ -798,7 +803,7 @@ async def _process_quote_message( compress_path and compress_path != path and os.path.exists(compress_path) - ): # noqa: PNT120 + ): try: os.remove(compress_path) except Exception as exc: # noqa: BLE001 @@ -860,7 +865,7 @@ async def _decorate_llm_request( config: MainAgentBuildConfig, ) -> None: cfg = config.provider_settings or plugin_context.get_config( - umo=event.unified_msg_origin + umo=event.unified_msg_origin, ).get("provider_settings", {}) _apply_prompt_prefix(req, cfg) @@ -926,7 +931,9 @@ def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None: async def _handle_webchat( - event: AstrMessageEvent, req: ProviderRequest, prov: Provider + event: AstrMessageEvent, + req: ProviderRequest, + prov: Provider, ) -> None: from astrbot.core import db_helper @@ -961,7 +968,9 @@ async def _handle_webchat( if not title or "" in title: return logger.info( - "Generated chatui title for session %s: %s", chatui_session_id, title + "Generated chatui title for session %s: %s", + chatui_session_id, + title, ) await db_helper.update_platform_session( session_id=chatui_session_id, @@ -1098,7 +1107,8 @@ async def _apply_web_search_tools( def _get_compress_provider( - config: MainAgentBuildConfig, plugin_context: Context + config: MainAgentBuildConfig, + plugin_context: Context, ) -> Provider | None: if not config.llm_compress_provider_id: return None @@ -1121,12 +1131,14 @@ def _get_compress_provider( def _get_fallback_chat_providers( - provider: Provider, plugin_context: Context, provider_settings: dict + provider: Provider, + plugin_context: Context, + provider_settings: dict, ) -> list[Provider]: fallback_ids = provider_settings.get("fallback_chat_models", []) if not isinstance(fallback_ids, list): logger.warning( - "fallback_chat_models setting is not a list, skip fallback providers." + "fallback_chat_models setting is not a list, skip fallback providers.", ) return [] @@ -1189,7 +1201,7 @@ async def build_main_agent( if sel_model := event.get_extra("selected_model"): req.model = sel_model if config.provider_wake_prefix and not event.message_str.startswith( - config.provider_wake_prefix + config.provider_wake_prefix, ): return None @@ -1207,7 +1219,7 @@ async def build_main_agent( event.track_temporary_local_file(image_path) req.image_urls.append(image_path) req.extra_user_content_parts.append( - TextPart(text=f"[Image Attachment: path {image_path}]") + TextPart(text=f"[Image Attachment: path {image_path}]"), ) elif isinstance(comp, Record): audio_path = await comp.convert_to_file_path() @@ -1218,8 +1230,8 @@ async def build_main_agent( file_name = comp.name or os.path.basename(file_path) req.extra_user_content_parts.append( TextPart( - text=f"[File Attachment: name {file_name}, path {file_path}]" - ) + text=f"[File Attachment: name {file_name}, path {file_path}]", + ), ) elif isinstance(comp, Video): await _append_video_attachment(req, comp) @@ -1228,7 +1240,7 @@ async def build_main_agent( comp for comp in event.message_obj.message if isinstance(comp, Reply) ] quoted_message_settings = _get_quoted_message_parser_settings( - config.provider_settings + config.provider_settings, ) fallback_quoted_image_count = 0 for comp in reply_comps: @@ -1258,8 +1270,8 @@ async def build_main_agent( text=( f"[File Attachment in quoted message: " f"name {file_name}, path {file_path}]" - ) - ) + ), + ), ) elif isinstance(reply_comp, Video): await _append_video_attachment(req, reply_comp, quoted=True) @@ -1273,7 +1285,7 @@ async def build_main_agent( event, comp, settings=quoted_message_settings, - ) + ), ) remaining_limit = max( config.max_quoted_fallback_images @@ -1326,8 +1338,8 @@ async def build_main_agent( "The user is asking in a side thread about this selected " "excerpt from the previous assistant answer:\n" f"{thread_selected_text.strip()}" - ) - ) + ), + ), ) req.image_urls = normalize_and_dedupe_strings(req.image_urls) req.audio_urls = normalize_and_dedupe_strings(req.audio_urls) @@ -1376,8 +1388,8 @@ async def build_main_agent( req.func_tool = ToolSet() req.func_tool.add_tool( plugin_context.get_llm_tool_manager().get_builtin_tool( - "send_message_to_user" - ) + "send_message_to_user", + ), ) if provider.provider_config.get("max_context_tokens", 0) <= 0: @@ -1433,7 +1445,9 @@ async def build_main_agent( enforce_max_turns=config.max_context_length, tool_schema_mode=config.tool_schema_mode, fallback_providers=_get_fallback_chat_providers( - provider, plugin_context, config.provider_settings + provider, + plugin_context, + config.provider_settings, ), tool_result_overflow_dir=( get_astrbot_system_tmp_path() diff --git a/astrbot/core/computer/booters/base.py b/astrbot/core/computer/booters/base.py index d77892152..9dbed2823 100644 --- a/astrbot/core/computer/booters/base.py +++ b/astrbot/core/computer/booters/base.py @@ -58,7 +58,6 @@ class ComputerBooter(abc.ABC): ShipyardNeoBooter). The default implementation ignores them. """ - ... async def upload_file(self, path: str, file_name: str) -> dict: """Upload file to the computer. diff --git a/astrbot/core/computer/booters/bwrap.py b/astrbot/core/computer/booters/bwrap.py index 968e7b396..5e00a00e1 100644 --- a/astrbot/core/computer/booters/bwrap.py +++ b/astrbot/core/computer/booters/bwrap.py @@ -297,7 +297,10 @@ class HostBackedFileSystemComponent(FileSystemComponent): cmd = ["grep", "-r", pattern, p] result = await asyncio.to_thread( - subprocess.run, cmd, capture_output=True, text=True + subprocess.run, + cmd, + capture_output=True, + text=True, ) return { "success": True, diff --git a/astrbot/core/computer/booters/cua.py b/astrbot/core/computer/booters/cua.py index 151b4c0e0..9c989a06b 100644 --- a/astrbot/core/computer/booters/cua.py +++ b/astrbot/core/computer/booters/cua.py @@ -58,7 +58,7 @@ async def _write_base64_via_shell( "pathlib.Path(sys.argv[1]).write_bytes(base64.b64decode(sys.stdin.read()))" ) return await shell.exec( - f"python3 -c {shlex.quote(decoder)} {shlex.quote(path)} <<'EOF'\n{encoded}\nEOF" + f"python3 -c {shlex.quote(decoder)} {shlex.quote(path)} <<'EOF'\n{encoded}\nEOF", ) @@ -223,7 +223,7 @@ def _missing_component_method_error( candidates = ", ".join(f"{component_name}.{name}" for name in names) return RuntimeError( f"CUA sandbox does not provide any of: {candidates}. " - "Please check the installed CUA SDK version and sandbox backend." + "Please check the installed CUA SDK version and sandbox backend.", ) @@ -344,7 +344,9 @@ class CuaPythonComponent(PythonComponent): self._python_exec = None if python is not None: self._python_exec = getattr(python, "exec", None) or getattr( - python, "run", None + python, + "run", + None, ) async def exec( @@ -394,7 +396,9 @@ def _write_result(path: str, result: dict[str, Any]) -> dict[str, Any]: class CuaFileSystemComponent(FileSystemComponent): def __init__( - self, sandbox: Any, os_type: str = CUA_DEFAULT_CONFIG["os_type"] + self, + sandbox: Any, + os_type: str = CUA_DEFAULT_CONFIG["os_type"], ) -> None: self._shell = CuaShellComponent(sandbox, os_type=os_type) self._fs_components = _resolve_files_components(sandbox) @@ -420,19 +424,21 @@ class CuaFileSystemComponent(FileSystemComponent): limit: int | None = None, ) -> dict[str, Any]: read_file = _resolve_files_method( - self._fs_components, ("read_file", "read_text") + self._fs_components, + ("read_file", "read_text"), ) if read_file is None: return await self._fallback.read_file(path, encoding, offset, limit) - else: - content = await _maybe_await(read_file(path)) + content = await _maybe_await(read_file(path)) if isinstance(content, bytes): content = content.decode(encoding, errors="replace") return { "success": True, "path": path, "content": _slice_content_by_lines( - str(content), offset=offset, limit=limit + str(content), + offset=offset, + limit=limit, ), } @@ -445,22 +451,22 @@ class CuaFileSystemComponent(FileSystemComponent): ) -> dict[str, Any]: _ = mode write_file = _resolve_files_method( - self._fs_components, ("write_file", "write_text") + self._fs_components, + ("write_file", "write_text"), ) if write_file is None: return await self._fallback.write_file(path, content, mode, encoding) - else: - await _maybe_await(write_file(path, content)) + await _maybe_await(write_file(path, content)) return {"success": True, "path": path} async def delete_file(self, path: str) -> dict[str, Any]: delete = _resolve_files_method( - self._fs_components, ("delete", "delete_file", "remove") + self._fs_components, + ("delete", "delete_file", "remove"), ) if delete is None: return await self._fallback.delete_file(path) - else: - await _maybe_await(delete(path)) + await _maybe_await(delete(path)) return {"success": True, "path": path} async def list_dir( @@ -547,7 +553,9 @@ class _PosixShellFileSystem(FileSystemComponent): "success": True, "path": path, "content": _slice_content_by_lines( - str(result.get("stdout", "")), offset=offset, limit=limit + str(result.get("stdout", "")), + offset=offset, + limit=limit, ), } @@ -562,7 +570,9 @@ class _PosixShellFileSystem(FileSystemComponent): if error := self._ensure_posix(path): return error result = await _write_base64_via_shell( - self._shell, path, content.encode(encoding) + self._shell, + path, + content.encode(encoding), ) return _write_result(path, result) @@ -628,7 +638,8 @@ class CuaGUIComponent(GUIComponent): self._click = _resolve_component_method(mouse, "click") self._type_text = _resolve_component_method(keyboard, "type") self._press_key = _resolve_component_method( - keyboard, ("press", "key_press", "press_key") + keyboard, + ("press", "key_press", "press_key"), ) async def screenshot(self, path: str | None = None) -> dict[str, Any]: @@ -661,7 +672,8 @@ class CuaGUIComponent(GUIComponent): async def press_key(self, key: str) -> dict[str, Any]: if self._press_key is None: raise _missing_component_method_error( - "keyboard", ("press", "key_press", "press_key") + "keyboard", + ("press", "key_press", "press_key"), ) result = await _maybe_await(self._press_key(key)) payload = _maybe_model_dump(result) @@ -733,7 +745,7 @@ class CuaBooter(ComputerBooter): except ImportError as exc: raise RuntimeError( "CUA sandbox support requires the optional `cua` package. " - "Install it with `pip install cua` in the AstrBot environment." + "Install it with `pip install cua` in the AstrBot environment.", ) from exc image_obj = self._build_image(Image) @@ -839,7 +851,7 @@ class CuaBooter(ComputerBooter): sandbox = None if self._runtime is None else self._runtime.sandbox if sandbox is not None and hasattr(sandbox, "upload_file"): return _maybe_model_dump( - await sandbox.upload_file(str(local_path), file_name) + await sandbox.upload_file(str(local_path), file_name), ) files_components = () if sandbox is None else _resolve_files_components(sandbox) upload = _resolve_files_method(files_components, "upload") @@ -853,7 +865,9 @@ class CuaBooter(ComputerBooter): if not _is_posix_os_type(self.os_type): return _non_posix_filesystem_result(file_name, self.os_type) result = await _write_base64_via_shell( - self.shell, file_name, local_path.read_bytes() + self.shell, + file_name, + local_path.read_bytes(), ) return { "success": not bool(result.get("stderr")), diff --git a/astrbot/core/computer/booters/cua_defaults.py b/astrbot/core/computer/booters/cua_defaults.py index 4c506154a..a36c6e654 100644 --- a/astrbot/core/computer/booters/cua_defaults.py +++ b/astrbot/core/computer/booters/cua_defaults.py @@ -2,6 +2,7 @@ CUA_DEFAULT_CONFIG = { "image": "linux", "os_type": "linux", "ttl": 3600, + "idle_timeout": 0, "telemetry_enabled": False, "local": True, "api_key": "", diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index f0c6935cb..495cd6ac5 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -107,13 +107,18 @@ class ShipyardShellWrapper: class ShipyardFileSystemWrapper: def __init__( - self, _shipyard_fs: FileSystemComponent, _shipyard_shell: ShellComponent + self, + _shipyard_fs: FileSystemComponent, + _shipyard_shell: ShellComponent, ): self._fs = _shipyard_fs self._shell = _shipyard_shell async def create_file( - self, path: str, content: str = "", mode: int = 420 + self, + path: str, + content: str = "", + mode: int = 420, ) -> dict[str, Any]: return await self._fs.create_file(path=path, content=content, mode=mode) @@ -125,18 +130,30 @@ class ShipyardFileSystemWrapper: limit: int | None = None, ) -> dict[str, Any]: return await self._fs.read_file( - path=path, encoding=encoding, offset=offset, limit=limit + path=path, + encoding=encoding, + offset=offset, + limit=limit, ) async def write_file( - self, path: str, content: str, mode: str = "w", encoding: str = "utf-8" + self, + path: str, + content: str, + mode: str = "w", + encoding: str = "utf-8", ) -> dict[str, Any]: return await self._fs.write_file( - path=path, content=content, mode=mode, encoding=encoding + path=path, + content=content, + mode=mode, + encoding=encoding, ) async def list_dir( - self, path: str = ".", show_hidden: bool = False + self, + path: str = ".", + show_hidden: bool = False, ) -> dict[str, Any]: return await self._fs.list_dir(path=path, show_hidden=show_hidden) diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index b8cd2deb3..f73937d5c 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -27,7 +27,7 @@ try: from shipyard_neo.sandbox import Sandbox except ImportError: logger.warning( - "shipyard_neo_sdk is not installed. ShipyardNeoBooter will not work without it." + "shipyard_neo_sdk is not installed. ShipyardNeoBooter will not work without it.", ) @@ -278,12 +278,12 @@ class ShipyardNeoBooter(ComputerBooter): self, endpoint_url: str, access_token: str, - profile: str = DEFAULT_PROFILE, + profile: str = "", ttl: int = 3600, ) -> None: self._endpoint_url = endpoint_url self._access_token = access_token - self._profile = profile + self._profile = profile.strip() if profile else "" self._ttl = ttl self._client: Any = None self._sandbox: Any = None @@ -350,6 +350,10 @@ class ShipyardNeoBooter(ComputerBooter): access_token=self._access_token, ) await self._client.__aenter__() + + # Resolve profile: user-specified > smart selection > default. + # An empty profile means auto-select; any non-empty profile must be + # honoured as an explicit choice, including "python-default". resolved_profile = await self._resolve_profile(self._client) self._sandbox = await self._client.create_sandbox( profile=resolved_profile, @@ -414,7 +418,7 @@ class ShipyardNeoBooter(ComputerBooter): del_err, ) raise RuntimeError( - f"Sandbox {sandbox_id} is in terminal state: {status}" + f"Sandbox {sandbox_id} is in terminal state: {status}", ) remaining = deadline - asyncio.get_running_loop().time() @@ -436,7 +440,7 @@ class ShipyardNeoBooter(ComputerBooter): ) raise TimeoutError( f"Sandbox {sandbox_id} did not become ready within " - f"{READINESS_TIMEOUT}s (last status: {status})" + f"{READINESS_TIMEOUT}s (last status: {status})", ) logger.debug( @@ -450,7 +454,7 @@ class ShipyardNeoBooter(ComputerBooter): """Pick the best profile for this session. Resolution order: - 1. User-specified profile (non-empty, non-default) → use as-is. + 1. User-specified profile (non-empty) → use as-is. 2. Query ``GET /v1/profiles`` and pick the profile with the most capabilities, preferring profiles that include ``"browser"``. 3. Fall back to :attr:`DEFAULT_PROFILE`. @@ -459,11 +463,9 @@ class ShipyardNeoBooter(ComputerBooter): misconfigured token, and silently falling back would just delay the real failure to ``create_sandbox``. """ - if self._profile and self._profile != self.DEFAULT_PROFILE: - logger.info( - "[Computer] profile_selected mode=user profile=%s", - self._profile, - ) + # User explicitly set a profile → honour it. + if self._profile: + logger.info("[Computer] Using user-specified profile: %s", self._profile) return self._profile from shipyard_neo.errors import ForbiddenError, UnauthorizedError @@ -509,11 +511,13 @@ class ShipyardNeoBooter(ComputerBooter): if delete_sandbox and self._sandbox is not None: try: logger.info( - "[Computer] Deleting Shipyard Neo sandbox: id=%s", sandbox_id + "[Computer] Deleting Shipyard Neo sandbox: id=%s", + sandbox_id, ) await self._sandbox.delete() logger.info( - "[Computer] Shipyard Neo sandbox deleted: id=%s", sandbox_id + "[Computer] Shipyard Neo sandbox deleted: id=%s", + sandbox_id, ) except Exception as e: logger.warning( diff --git a/astrbot/core/computer/computer_client.py b/astrbot/core/computer/computer_client.py index 72323163a..c28668f90 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -1,9 +1,12 @@ from __future__ import annotations +import asyncio import json import os import shutil +import time import uuid +from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING @@ -27,6 +30,70 @@ local_booter: ComputerBooter | None = None _MANAGED_SKILLS_FILE = ".astrbot_managed_skills.json" +@dataclass(slots=True) +class _CUAIdleState: + expires_at: float + task: asyncio.Task + + +cua_idle_state: dict[str, _CUAIdleState] = {} + + +def _get_cua_idle_timeout(config: dict) -> float: + sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {}) + value = sandbox_cfg.get("cua_idle_timeout", 0) + try: + timeout = float(value) + except (TypeError, ValueError): + return 0.0 + return max(timeout, 0.0) + + +def _clear_cua_idle_state(session_id: str) -> None: + state = cua_idle_state.pop(session_id, None) + if state is not None and not state.task.done(): + state.task.cancel() + + +def _schedule_cua_idle_cleanup(session_id: str, timeout: float) -> None: + _clear_cua_idle_state(session_id) + if timeout <= 0: + return + expires_at = time.monotonic() + timeout + + async def _expire_when_idle() -> None: + try: + remaining = expires_at - time.monotonic() + if remaining > 0: + await asyncio.sleep(remaining) + + state = cua_idle_state.get(session_id) + if state is None or state.expires_at != expires_at: + return + + booter = session_booter.get(session_id) + if booter is not None: + try: + await booter.shutdown() + except Exception as shutdown_err: + logger.warning( + "[Computer] Failed to shutdown idle CUA sandbox for session %s: %s", + session_id, + shutdown_err, + ) + finally: + session_booter.pop(session_id, None) + except asyncio.CancelledError: + raise + finally: + state = cua_idle_state.get(session_id) + if state is not None and state.expires_at == expires_at: + cua_idle_state.pop(session_id, None) + + task = asyncio.create_task(_expire_when_idle()) + cua_idle_state[session_id] = _CUAIdleState(expires_at=expires_at, task=task) + + def _list_local_skill_dirs(skills_root: Path) -> list[Path]: skills: list[Path] = [] for entry in sorted(skills_root.iterdir()): @@ -388,7 +455,7 @@ async def _apply_skills_to_sandbox(booter: ComputerBooter) -> None: """ logger.info("[Computer] sandbox_sync phase=apply status=start") apply_result = _normalize_shell_exec_result( - await booter.shell.exec(_build_apply_sync_command()) + await booter.shell.exec(_build_apply_sync_command()), ) if not _shell_exec_succeeded(apply_result): detail = _format_exec_error_detail(apply_result) @@ -404,7 +471,7 @@ async def _scan_sandbox_skills(booter: ComputerBooter) -> dict | None: """Scan sandbox skills and return normalized payload for cache update.""" logger.info("[Computer] sandbox_sync phase=scan status=start") scan_result = _normalize_shell_exec_result( - await booter.shell.exec(_build_scan_command()) + await booter.shell.exec(_build_scan_command()), ) if not _shell_exec_succeeded(scan_result): detail = _format_exec_error_detail(scan_result) @@ -509,6 +576,7 @@ async def get_booter( sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {}) booter_type = sandbox_cfg.get("booter", "shipyard_neo") + cua_idle_timeout = _get_cua_idle_timeout(config) if booter_type == "cua" else 0.0 if session_id in session_booter: booter = session_booter[session_id] @@ -529,6 +597,7 @@ async def get_booter( session_id, shutdown_err, ) + _clear_cua_idle_state(session_id) session_booter.pop(session_id, None) if session_id not in session_booter: uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex @@ -578,7 +647,7 @@ async def get_booter( cua_kwargs = build_cua_booter_kwargs(sandbox_cfg) logger.info( f"[Computer] CUA config: image={cua_kwargs['image']}, " - f"os_type={cua_kwargs['os_type']}, ttl={cua_kwargs['ttl']}" + f"os_type={cua_kwargs['os_type']}, ttl={cua_kwargs['ttl']}", ) client = CuaBooter(**cua_kwargs) elif booter_type == "boxlite": @@ -609,9 +678,12 @@ async def get_booter( session_id, shutdown_error, ) + _clear_cua_idle_state(session_id) raise e session_booter[session_id] = client + if booter_type == "cua": + _schedule_cua_idle_cleanup(session_id, cua_idle_timeout) return session_booter[session_id] diff --git a/astrbot/core/computer/olayer/__init__.py b/astrbot/core/computer/olayer/__init__.py index 270ce2ffb..c153d92f9 100644 --- a/astrbot/core/computer/olayer/__init__.py +++ b/astrbot/core/computer/olayer/__init__.py @@ -5,11 +5,11 @@ from .python import PythonComponent from .shell import ShellComponent __all__ = [ + "BrowserComponent", "BrowserComponent", "FileSystemComponent", + "FileSystemComponent", + "GUIComponent", "PythonComponent", "ShellComponent", - "FileSystemComponent", - "BrowserComponent", - "GUIComponent", ] diff --git a/astrbot/core/computer/olayer/gui.py b/astrbot/core/computer/olayer/gui.py index cc23b9d7a..ad131f446 100644 --- a/astrbot/core/computer/olayer/gui.py +++ b/astrbot/core/computer/olayer/gui.py @@ -1,6 +1,4 @@ -""" -GUI automation component. -""" +"""GUI automation component.""" from typing import Any, Protocol diff --git a/astrbot/core/computer/shell_session.py b/astrbot/core/computer/shell_session.py index 02326a66c..9d705f366 100644 --- a/astrbot/core/computer/shell_session.py +++ b/astrbot/core/computer/shell_session.py @@ -92,6 +92,7 @@ class PersistentShellSession: ------- dict with keys ``stdout``, ``stderr``, ``exit_code`` and (when background) ``background_task``. + """ await self._ensure_running() diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 66b25565c..4ba6cdcb1 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -6,12 +6,16 @@ from typing import Any from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.auth_password import ( - normalize_dashboard_password_hash, + generate_dashboard_password, + hash_dashboard_password, + hash_legacy_dashboard_password, + validate_dashboard_password, ) from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP ASTRBOT_CONFIG_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json") +DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD" logger = logging.getLogger("astrbot") @@ -59,21 +63,64 @@ class AstrBotConfig(dict): # Handle UTF-8 BOM if present conf_str = conf_str.removeprefix("\ufeff") conf = json.loads(conf_str) if conf_str.strip() else {} - + dashboard_conf = conf.get("dashboard") + legacy_dashboard_password_change_required = bool( + isinstance(dashboard_conf, dict) + and dashboard_conf.get("password_change_required", False), + ) + if legacy_dashboard_password_change_required: + object.__setattr__( + self, + "_dashboard_password_change_required_from_config", + True, + ) # 检查配置完整性,并插入 has_new = self.check_config_integrity(default_config, conf) if ( "dashboard" in conf and isinstance(conf["dashboard"], dict) + and not conf["dashboard"].get("pbkdf2_password") and not conf["dashboard"].get("password") + ) or ( + "dashboard" in conf + and isinstance(conf["dashboard"], dict) + and legacy_dashboard_password_change_required + and conf["dashboard"].get("pbkdf2_password") ): - conf["dashboard"]["password"] = normalize_dashboard_password_hash("") + self._reset_generated_dashboard_password(conf) has_new = True self.update(conf) if has_new: self.save_config() - self.update(conf) + def _reset_generated_dashboard_password(self, conf: dict) -> None: + generated_password = self._resolve_initial_dashboard_password() + conf["dashboard"]["pbkdf2_password"] = hash_dashboard_password( + generated_password, + ) + conf["dashboard"]["password"] = hash_legacy_dashboard_password( + generated_password, + ) + conf["dashboard"]["password_storage_upgraded"] = True + conf["dashboard"]["password_change_required"] = True + object.__setattr__( + self, + "_generated_dashboard_password", + generated_password, + ) + object.__setattr__( + self, + "_generated_dashboard_password_change_required", + True, + ) + + @staticmethod + def _resolve_initial_dashboard_password() -> str: + env_password = os.environ.get(DASHBOARD_INITIAL_PASSWORD_ENV) + if env_password is None: + return generate_dashboard_password() + validate_dashboard_password(env_password) + return env_password def _config_schema_to_default_config(self, schema: dict) -> dict: """将 Schema 转换成 Config""" diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index eb02f61c1..2e1c65f4b 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -9,7 +9,7 @@ from astrbot.builtin_stars.web_searcher.provider_constants import ( from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "4.24.2" +VERSION = "4.25.1" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") PERSONAL_WECHAT_CONFIG_METADATA = { "weixin_oc_base_url": { @@ -181,7 +181,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "shipyard_neo_ttl": 3600, "cua_image": CUA_DEFAULT_CONFIG["image"], "cua_os_type": CUA_DEFAULT_CONFIG["os_type"], - "cua_ttl": CUA_DEFAULT_CONFIG["ttl"], + "cua_idle_timeout": CUA_DEFAULT_CONFIG["idle_timeout"], "cua_telemetry_enabled": CUA_DEFAULT_CONFIG["telemetry_enabled"], "cua_local": CUA_DEFAULT_CONFIG["local"], "cua_api_key": CUA_DEFAULT_CONFIG["api_key"], @@ -248,6 +248,9 @@ DEFAULT_CONFIG: dict[str, Any] = { "enable": True, "username": "astrbot", "password": "", + "pbkdf2_password": "", + "password_storage_upgraded": False, + "password_change_required": False, "jwt_secret": "", "host": "0.0.0.0", "port": 6185, @@ -318,7 +321,7 @@ CONFIG_METADATA_2: Any = { "QQ 官方机器人(WebSocket)": { "id": "default", "type": "qq_official", - "enable": False, + "enable": True, "appid": "", "secret": "", "enable_group_c2c": True, @@ -327,7 +330,7 @@ CONFIG_METADATA_2: Any = { "QQ 官方机器人(Webhook)": { "id": "default", "type": "qq_official_webhook", - "enable": False, + "enable": True, "appid": "", "secret": "", "is_sandbox": False, @@ -339,7 +342,7 @@ CONFIG_METADATA_2: Any = { "OneBot v11": { "id": "default", "type": "aiocqhttp", - "enable": False, + "enable": True, "ws_reverse_host": "0.0.0.0", "ws_reverse_port": 6199, "ws_reverse_token": "", @@ -347,7 +350,7 @@ CONFIG_METADATA_2: Any = { "微信公众平台": { "id": "weixin_official_account", "type": "weixin_official_account", - "enable": False, + "enable": True, "appid": "", "secret": "", "token": "", @@ -362,7 +365,7 @@ CONFIG_METADATA_2: Any = { "企业微信(含微信客服)": { "id": "wecom", "type": "wecom", - "enable": False, + "enable": True, "corpid": "", "secret": "", "token": "", @@ -399,7 +402,7 @@ CONFIG_METADATA_2: Any = { "个人微信": { "id": "weixin_personal", "type": "weixin_oc", - "enable": False, + "enable": True, "weixin_oc_base_url": "https://ilinkai.weixin.qq.com", "weixin_oc_bot_type": "3", "weixin_oc_qr_poll_interval": 1, @@ -409,8 +412,7 @@ CONFIG_METADATA_2: Any = { "飞书(Lark)": { "id": "lark", "type": "lark", - "enable": False, - "lark_bot_name": "", + "enable": True, "app_id": "", "app_secret": "", "domain": "https://open.feishu.cn", @@ -422,7 +424,7 @@ CONFIG_METADATA_2: Any = { "钉钉(DingTalk)": { "id": "dingtalk", "type": "dingtalk", - "enable": False, + "enable": True, "client_id": "", "client_secret": "", "card_template_id": "", @@ -430,7 +432,7 @@ CONFIG_METADATA_2: Any = { "Telegram": { "id": "telegram", "type": "telegram", - "enable": False, + "enable": True, "telegram_token": "your_bot_token", "start_message": "Hello, I'm AstrBot!", "telegram_api_base_url": "https://api.telegram.org/bot", @@ -443,7 +445,7 @@ CONFIG_METADATA_2: Any = { "Discord": { "id": "discord", "type": "discord", - "enable": False, + "enable": True, "discord_token": "", "discord_proxy": "", "discord_command_register": True, @@ -452,7 +454,7 @@ CONFIG_METADATA_2: Any = { "Misskey": { "id": "misskey", "type": "misskey", - "enable": False, + "enable": True, "misskey_instance_url": "https://misskey.example", "misskey_token": "", "misskey_default_visibility": "public", @@ -470,7 +472,7 @@ CONFIG_METADATA_2: Any = { "Slack": { "id": "slack", "type": "slack", - "enable": False, + "enable": True, "bot_token": "", "app_token": "", "signing_secret": "", @@ -484,7 +486,7 @@ CONFIG_METADATA_2: Any = { "Line": { "id": "line", "type": "line", - "enable": False, + "enable": True, "channel_access_token": "", "channel_secret": "", "unified_webhook_mode": True, @@ -493,7 +495,7 @@ CONFIG_METADATA_2: Any = { "Satori": { "id": "satori", "type": "satori", - "enable": False, + "enable": True, "satori_api_base_url": "http://localhost:5140/satori/v1", "satori_endpoint": "ws://localhost:5140/satori/v1/events", "satori_token": "", @@ -504,7 +506,7 @@ CONFIG_METADATA_2: Any = { "kook": { "id": "kook", "type": "kook", - "enable": False, + "enable": True, "kook_bot_token": "", "kook_reconnect_delay": 1, "kook_max_reconnect_delay": 60, @@ -514,6 +516,14 @@ CONFIG_METADATA_2: Any = { "kook_max_heartbeat_failures": 3, "kook_max_consecutive_failures": 5, }, + "Mattermost": { + "id": "mattermost", + "type": "mattermost", + "enable": True, + "mattermost_url": "https://chat.example.com", + "mattermost_bot_token": "", + "mattermost_reconnect_delay": 5.0, + }, # "WebChat": { # "id": "webchat", # "type": "webchat", @@ -865,11 +875,6 @@ CONFIG_METADATA_2: Any = { "wecom_ai_bot_connection_mode": "long_connection", }, }, - "lark_bot_name": { - "description": "飞书机器人的名字", - "type": "string", - "hint": "请务必填写正确,否则 @ 机器人将无法唤醒,只能通过前缀唤醒。", - }, "discord_token": { "description": "Discord Bot Token", "type": "string", @@ -1770,6 +1775,34 @@ CONFIG_METADATA_2: Any = { "timeout": 20, "proxy": "", }, + "NVIDIA Embedding": { + "id": "nvidia_embedding", + "type": "nvidia_embedding", + "provider": "nvidia", + "provider_type": "embedding", + "hint": "provider_group.provider.nvidia_embedding.hint", + "enable": True, + "embedding_api_key": "", + "embedding_api_base": "https://integrate.api.nvidia.com/v1", + "embedding_model": "nvidia/llama-nemotron-embed-1b-v2", + "input_type": "passage", + "embedding_dimensions": 1024, + "timeout": 20, + "proxy": "", + }, + "Ollama Embedding": { + "id": "ollama_embedding", + "type": "ollama_embedding", + "provider": "ollama", + "provider_type": "embedding", + "hint": "provider_group.provider.ollama_embedding.hint", + "enable": True, + "embedding_api_base": "http://localhost:11434", + "embedding_model": "nomic-embed-text", + "embedding_dimensions": 768, + "timeout": 60, + "proxy": "", + }, "vLLM Rerank": { "id": "vllm_rerank", "type": "vllm_rerank", @@ -3276,7 +3309,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_neo_profile": { "description": "Shipyard Neo Profile", "type": "string", - "hint": "Shipyard Neo 沙箱 profile,如 python-default。", + "hint": "Shipyard Neo 沙箱 profile,如 python-default。留空时自动选择能力更完整的 profile。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard_neo", @@ -3311,10 +3344,10 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.booter": "cua", }, }, - "provider_settings.sandbox.cua_ttl": { - "description": "CUA Sandbox TTL", + "provider_settings.sandbox.cua_idle_timeout": { + "description": "CUA Idle Timeout", "type": "int", - "hint": "CUA 沙箱生存时间(秒)。当前作为会话配置保存,具体生效取决于 CUA SDK。", + "hint": "Idle timeout for CUA sandbox sessions in seconds. When greater than 0, AstrBot proactively shuts down an idle CUA sandbox after that amount of inactivity; 0 disables it.", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "cua", diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index 275ce4355..fea32bc55 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -76,12 +76,12 @@ class SQLiteDatabase(BaseDatabase): if "folder_id" not in columns: await conn.execute( text( - "ALTER TABLE personas ADD COLUMN folder_id VARCHAR(36) DEFAULT NULL" - ) + "ALTER TABLE personas ADD COLUMN folder_id VARCHAR(36) DEFAULT NULL", + ), ) if "sort_order" not in columns: await conn.execute( - text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0") + text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0"), ) async def _ensure_persona_skills_column(self, conn) -> None: @@ -103,7 +103,7 @@ class SQLiteDatabase(BaseDatabase): if "custom_error_message" not in columns: await conn.execute( - text("ALTER TABLE personas ADD COLUMN custom_error_message TEXT") + text("ALTER TABLE personas ADD COLUMN custom_error_message TEXT"), ) async def _ensure_platform_message_history_checkpoint_column(self, conn) -> None: @@ -115,15 +115,15 @@ class SQLiteDatabase(BaseDatabase): await conn.execute( text( "ALTER TABLE platform_message_history " - "ADD COLUMN llm_checkpoint_id VARCHAR DEFAULT NULL" - ) + "ADD COLUMN llm_checkpoint_id VARCHAR DEFAULT NULL", + ), ) await conn.execute( text( "CREATE INDEX IF NOT EXISTS " "ix_platform_message_history_llm_checkpoint_id " - "ON platform_message_history (llm_checkpoint_id)" - ) + "ON platform_message_history (llm_checkpoint_id)", + ), ) # ==== @@ -409,7 +409,7 @@ class SQLiteDatabase(BaseDatabase): async with session.begin(): await session.execute( delete(ConversationV2).where( - col(ConversationV2.user_id) == user_id + col(ConversationV2.user_id) == user_id, ), ) @@ -567,7 +567,7 @@ class SQLiteDatabase(BaseDatabase): await session.execute( update(PlatformMessageHistory) .where(PlatformMessageHistory.id == message_id) - .values(**values) + .values(**values), ) async def delete_platform_message_history_by_id(self, message_id: int) -> None: @@ -577,8 +577,8 @@ class SQLiteDatabase(BaseDatabase): async with session.begin(): await session.execute( delete(PlatformMessageHistory).where( - PlatformMessageHistory.id == message_id - ) + PlatformMessageHistory.id == message_id, + ), ) async def delete_platform_message_offset( @@ -624,13 +624,14 @@ class SQLiteDatabase(BaseDatabase): return result.scalars().all() async def get_platform_message_history_by_id( - self, message_id: int + self, + message_id: int, ) -> PlatformMessageHistory | None: """Get a platform message history record by its ID.""" async with self.get_db() as session: session: AsyncSession query = select(PlatformMessageHistory).where( - PlatformMessageHistory.id == message_id + PlatformMessageHistory.id == message_id, ) result = await session.execute(query) return result.scalar_one_or_none() @@ -667,7 +668,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession result = await session.execute( - select(WebChatThread).where(WebChatThread.thread_id == thread_id) + select(WebChatThread).where(WebChatThread.thread_id == thread_id), ) return result.scalar_one_or_none() @@ -680,7 +681,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession query = select(WebChatThread).where( - WebChatThread.parent_session_id == parent_session_id + WebChatThread.parent_session_id == parent_session_id, ) if creator is not None: query = query.where(WebChatThread.creator == creator) @@ -714,7 +715,7 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): await session.execute( - delete(WebChatThread).where(WebChatThread.thread_id == thread_id) + delete(WebChatThread).where(WebChatThread.thread_id == thread_id), ) async def delete_webchat_threads_by_parent_session( @@ -731,8 +732,8 @@ class SQLiteDatabase(BaseDatabase): async with session.begin(): await session.execute( delete(WebChatThread).where( - col(WebChatThread.thread_id).in_(thread_ids) - ) + col(WebChatThread.thread_id).in_(thread_ids), + ), ) return thread_ids @@ -750,7 +751,7 @@ class SQLiteDatabase(BaseDatabase): select(WebChatThread.thread_id).where( WebChatThread.parent_session_id == parent_session_id, col(WebChatThread.parent_message_id).in_(parent_message_ids), - ) + ), ) thread_ids = list(result.scalars().all()) if not thread_ids: @@ -760,8 +761,8 @@ class SQLiteDatabase(BaseDatabase): async with session.begin(): await session.execute( delete(WebChatThread).where( - col(WebChatThread.thread_id).in_(thread_ids) - ) + col(WebChatThread.thread_id).in_(thread_ids), + ), ) return thread_ids @@ -793,7 +794,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession query = select(Attachment).where( - col(Attachment.attachment_id).in_(attachment_ids) + col(Attachment.attachment_id).in_(attachment_ids), ) result = await session.execute(query) return list(result.scalars().all()) @@ -807,9 +808,9 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): query = delete(Attachment).where( - col(Attachment.attachment_id) == attachment_id + col(Attachment.attachment_id) == attachment_id, ) - result = T.cast(CursorResult, await session.execute(query)) + result = T.cast("CursorResult", await session.execute(query)) return result.rowcount > 0 async def delete_attachments(self, attachment_ids: list[str]) -> int: @@ -823,9 +824,9 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): query = delete(Attachment).where( - col(Attachment.attachment_id).in_(attachment_ids) + col(Attachment.attachment_id).in_(attachment_ids), ) - result = T.cast(CursorResult, await session.execute(query)) + result = T.cast("CursorResult", await session.execute(query)) return result.rowcount async def create_api_key( @@ -859,7 +860,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession result = await session.execute( - select(ApiKey).order_by(desc(ApiKey.created_at)) + select(ApiKey).order_by(desc(ApiKey.created_at)), ) return list(result.scalars().all()) @@ -868,7 +869,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession result = await session.execute( - select(ApiKey).where(ApiKey.key_id == key_id) + select(ApiKey).where(ApiKey.key_id == key_id), ) return result.scalar_one_or_none() @@ -906,7 +907,7 @@ class SQLiteDatabase(BaseDatabase): .where(col(ApiKey.key_id) == key_id) .values(revoked_at=datetime.now(timezone.utc)) ) - result = T.cast(CursorResult, await session.execute(query)) + result = T.cast("CursorResult", await session.execute(query)) return result.rowcount > 0 async def delete_api_key(self, key_id: str) -> bool: @@ -915,9 +916,9 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): result = T.cast( - CursorResult, + "CursorResult", await session.execute( - delete(ApiKey).where(col(ApiKey.key_id) == key_id) + delete(ApiKey).where(col(ApiKey.key_id) == key_id), ), ) return result.rowcount > 0 @@ -1043,13 +1044,15 @@ class SQLiteDatabase(BaseDatabase): return result.scalar_one_or_none() async def get_persona_folders( - self, parent_id: str | None = None + self, + parent_id: str | None = None, ) -> list[PersonaFolder]: """Get all persona folders, optionally filtered by parent_id. Args: parent_id: If None, returns root folders only. If specified, returns children of that folder. + """ async with self.get_db() as session: session: AsyncSession @@ -1074,7 +1077,8 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession query = select(PersonaFolder).order_by( - col(PersonaFolder.sort_order), col(PersonaFolder.name) + col(PersonaFolder.sort_order), + col(PersonaFolder.name), ) result = await session.execute(query) return list(result.scalars().all()) @@ -1092,7 +1096,7 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): query = update(PersonaFolder).where( - col(PersonaFolder.folder_id) == folder_id + col(PersonaFolder.folder_id) == folder_id, ) values: dict[str, T.Any] = {} if name is not None: @@ -1122,17 +1126,19 @@ class SQLiteDatabase(BaseDatabase): await session.execute( update(Persona) .where(col(Persona.folder_id) == folder_id) - .values(folder_id=None) + .values(folder_id=None), ) # Delete the folder await session.execute( delete(PersonaFolder).where( - col(PersonaFolder.folder_id) == folder_id + col(PersonaFolder.folder_id) == folder_id, ), ) async def move_persona_to_folder( - self, persona_id: str, folder_id: str | None + self, + persona_id: str, + folder_id: str | None, ) -> Persona | None: """Move a persona to a folder (or root if folder_id is None).""" async with self.get_db() as session: @@ -1141,17 +1147,19 @@ class SQLiteDatabase(BaseDatabase): await session.execute( update(Persona) .where(col(Persona.persona_id) == persona_id) - .values(folder_id=folder_id) + .values(folder_id=folder_id), ) return await self.get_persona_by_id(persona_id) async def get_personas_by_folder( - self, folder_id: str | None = None + self, + folder_id: str | None = None, ) -> list[Persona]: """Get all personas in a specific folder. Args: folder_id: If None, returns personas in root directory. + """ async with self.get_db() as session: session: AsyncSession @@ -1181,6 +1189,7 @@ class SQLiteDatabase(BaseDatabase): - id: The persona_id or folder_id - type: Either "persona" or "folder" - sort_order: The new sort_order value + """ if not items: return @@ -1200,13 +1209,13 @@ class SQLiteDatabase(BaseDatabase): await session.execute( update(Persona) .where(col(Persona.persona_id) == item_id) - .values(sort_order=sort_order) + .values(sort_order=sort_order), ) elif item_type == "folder": await session.execute( update(PersonaFolder) .where(col(PersonaFolder.folder_id) == item_id) - .values(sort_order=sort_order) + .values(sort_order=sort_order), ) async def insert_preference_or_update(self, scope, scope_id, key, value): @@ -1649,7 +1658,8 @@ class SQLiteDatabase(BaseDatabase): return new_session async def get_platform_session_by_id( - self, session_id: str + self, + session_id: str, ) -> PlatformSession | None: """Get a Platform session by its ID.""" async with self.get_db() as session: @@ -1661,7 +1671,8 @@ class SQLiteDatabase(BaseDatabase): return result.scalar_one_or_none() async def get_platform_sessions_by_ids( - self, session_ids: list[str] + self, + session_ids: list[str], ) -> list[PlatformSession]: """Get platform sessions by IDs.""" if not session_ids: @@ -1670,7 +1681,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession query = select(PlatformSession).where( - col(PlatformSession.session_id).in_(session_ids) + col(PlatformSession.session_id).in_(session_ids), ) result = await session.execute(query) return list(result.scalars().all()) @@ -1769,7 +1780,7 @@ class SQLiteDatabase(BaseDatabase): ) total_result = await session.execute( - select(func.count()).select_from(base_query.subquery()) + select(func.count()).select_from(base_query.subquery()), ) total = int(total_result.scalar_one() or 0) @@ -1973,7 +1984,9 @@ class SQLiteDatabase(BaseDatabase): return list(result.scalars().all()) async def get_project_by_session( - self, session_id: str, creator: str + self, + session_id: str, + creator: str, ) -> ChatUIProject | None: """Get the project that a session belongs to.""" async with self.get_db() as session: @@ -2080,7 +2093,7 @@ class SQLiteDatabase(BaseDatabase): ) await session.execute(stmt) result = await session.execute( - select(CronJob).where(col(CronJob.job_id) == job_id) + select(CronJob).where(col(CronJob.job_id) == job_id), ) return result.scalar_one_or_none() @@ -2089,14 +2102,14 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): await session.execute( - delete(CronJob).where(col(CronJob.job_id) == job_id) + delete(CronJob).where(col(CronJob.job_id) == job_id), ) async def get_cron_job(self, job_id: str) -> CronJob | None: async with self.get_db() as session: session: AsyncSession result = await session.execute( - select(CronJob).where(col(CronJob.job_id) == job_id) + select(CronJob).where(col(CronJob.job_id) == job_id), ) return result.scalar_one_or_none() @@ -2130,7 +2143,7 @@ class SQLiteDatabase(BaseDatabase): .where( col(PlatformMessageHistory.platform_id) == platform_id, col(PlatformMessageHistory.user_id) == user_id, - ) + ), ) total = count_result.scalar() return records, total @@ -2149,7 +2162,7 @@ class SQLiteDatabase(BaseDatabase): col(PlatformMessageHistory.platform_id) == platform_id, col(PlatformMessageHistory.user_id) == user_id, col(PlatformMessageHistory.created_at) < before, - ) + ), ) return result.rowcount @@ -2167,7 +2180,7 @@ class SQLiteDatabase(BaseDatabase): col(PlatformMessageHistory.platform_id) == platform_id, col(PlatformMessageHistory.user_id) == user_id, col(PlatformMessageHistory.created_at) > after, - ) + ), ) return result.rowcount @@ -2183,7 +2196,7 @@ class SQLiteDatabase(BaseDatabase): delete(PlatformMessageHistory).where( col(PlatformMessageHistory.platform_id) == platform_id, col(PlatformMessageHistory.user_id) == user_id, - ) + ), ) return result.rowcount @@ -2200,7 +2213,7 @@ class SQLiteDatabase(BaseDatabase): col(PlatformMessageHistory.platform_id) == platform_id, col(PlatformMessageHistory.user_id) == user_id, col(PlatformMessageHistory.idempotency_key) == idempotency_key, - ) + ), ) return result.scalar_one_or_none() diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index 6cc8c3884..64df07dfc 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py @@ -86,19 +86,23 @@ class InternalAgentSubStage(Stage): self.file_extract_enabled: bool = file_extract_conf.get("enable", False) self.file_extract_prov: str = file_extract_conf.get("provider", "moonshotai") self.file_extract_msh_api_key: str = file_extract_conf.get( - "moonshotai_api_key", "" + "moonshotai_api_key", + "", ) # 上下文管理相关 self.context_limit_reached_strategy: str = settings.get( - "context_limit_reached_strategy", "truncate_by_turns" + "context_limit_reached_strategy", + "truncate_by_turns", ) self.llm_compress_instruction: str = settings.get( - "llm_compress_instruction", "" + "llm_compress_instruction", + "", ) self.llm_compress_keep_recent: int = settings.get("llm_compress_keep_recent", 4) self.llm_compress_provider_id: str = settings.get( - "llm_compress_provider_id", "" + "llm_compress_provider_id", + "", ) self.max_context_length = settings["max_context_length"] # int self.dequeue_context_length: int = min( @@ -108,12 +112,14 @@ class InternalAgentSubStage(Stage): if self.dequeue_context_length <= 0: self.dequeue_context_length = 1 self.fallback_max_context_tokens: int = settings.get( - "fallback_max_context_tokens", 128000 + "fallback_max_context_tokens", + 128000, ) self.llm_safety_mode = settings.get("llm_safety_mode", True) self.safety_mode_strategy = settings.get( - "safety_mode_strategy", "system_prompt" + "safety_mode_strategy", + "system_prompt", ) self.computer_use_runtime = settings.get("computer_use_runtime") @@ -271,13 +277,13 @@ class InternalAgentSubStage(Stage): # 获取 TTS Provider tts_provider = ( self.ctx.plugin_manager.context.get_using_tts_provider( - event.unified_msg_origin + event.unified_msg_origin, ) ) if not tts_provider: logger.warning( - "[Live Mode] TTS Provider 未配置,将使用普通流式模式" + "[Live Mode] TTS Provider 未配置,将使用普通流式模式", ) # 使用 run_live_agent,总是使用流式响应 @@ -372,7 +378,7 @@ class InternalAgentSubStage(Stage): req, agent_runner, final_resp, - ) + ), ) # 检查事件是否被停止,如果被停止则不保存历史记录 @@ -400,7 +406,7 @@ class InternalAgentSubStage(Stage): except Exception as e: logger.error(f"Error occurred while processing agent: {e}", exc_info=True) custom_error_message = extract_persona_custom_error_message_from_event( - event + event, ) error_text = custom_error_message or ( f"Error occurred while processing agent request: {e}" @@ -468,7 +474,7 @@ class InternalAgentSubStage(Stage): message_to_save.append( CheckpointMessageSegment( content=CheckpointData(id=checkpoint_id), - ).model_dump() + ).model_dump(), ) # if user_aborted: diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 4067f8869..8e47e2883 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -294,7 +294,8 @@ class ResultDecorateStage(Stage): ) else: result.chain.insert( - 0, Plain(f"🤔 思考: {reasoning_content}\n\n────\n") + 0, + Plain(f"🤔 思考: {reasoning_content}\n\n────\n"), ) if should_tts and tts_provider: diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index d23b46524..b90cd4fe1 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -64,7 +64,7 @@ class AstrMessageEvent(abc.ABC): except (ValueError, TypeError, AttributeError): logger.warning( f"Failed to convert message type {message_obj.type!r} to MessageType. " - f"Falling back to FRIEND_MESSAGE." + f"Falling back to FRIEND_MESSAGE.", ) message_type = MessageType.FRIEND_MESSAGE self.session = MessageSession( diff --git a/astrbot/core/platform/platform.py b/astrbot/core/platform/platform.py index 3ee113d0d..a6b7103c9 100644 --- a/astrbot/core/platform/platform.py +++ b/astrbot/core/platform/platform.py @@ -141,7 +141,7 @@ class Platform(abc.ABC): 异步方法。 """ asyncio.create_task( - Metric.upload(msg_event_tick=1, adapter_name=self.meta().name) + Metric.upload(msg_event_tick=1, adapter_name=self.meta().name), ) def commit_event(self, event: AstrMessageEvent) -> None: diff --git a/astrbot/core/platform/sources/dingtalk/app_registration.py b/astrbot/core/platform/sources/dingtalk/app_registration.py new file mode 100644 index 000000000..c177d30ae --- /dev/null +++ b/astrbot/core/platform/sources/dingtalk/app_registration.py @@ -0,0 +1,146 @@ +import os +from dataclasses import dataclass +from typing import Any + +import aiohttp + +DEFAULT_DINGTALK_REGISTRATION_BASE_URL = "https://oapi.dingtalk.com" +DEFAULT_DINGTALK_REGISTRATION_SOURCE = "DING_DWS_CLAW" + + +@dataclass +class DingtalkAppRegistration: + device_code: str + user_code: str + verification_uri: str + verification_uri_complete: str + expires_in: int + interval: int + + +def dingtalk_registration_base_url() -> str: + return ( + os.getenv("DINGTALK_REGISTRATION_BASE_URL", "").strip() + or DEFAULT_DINGTALK_REGISTRATION_BASE_URL + ).rstrip("/") + + +def dingtalk_registration_source() -> str: + return ( + os.getenv("DINGTALK_REGISTRATION_SOURCE", "").strip() + or DEFAULT_DINGTALK_REGISTRATION_SOURCE + ) + + +def _string_field(data: dict[str, Any], key: str) -> str: + value = data.get(key) + if isinstance(value, str): + return value.strip() + return "" + + +def _int_field(data: dict[str, Any], key: str, default: int) -> int: + value = data.get(key) + if isinstance(value, int): + return value + if isinstance(value, float): + return int(value) + return default + + +async def _post_registration( + path: str, + payload: dict[str, str], +) -> tuple[int, dict[str, Any]]: + timeout = aiohttp.ClientTimeout(total=15) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.post( + f"{dingtalk_registration_base_url()}{path}", + json=payload, + ) as response: + status = response.status + data = await response.json(content_type=None) + if not isinstance(data, dict): + raise RuntimeError("DingTalk registration response format is invalid") + return status, data + + +def _raise_dingtalk_registration_error( + status: int, + raw: dict[str, Any], + action: str, +) -> None: + errcode = raw.get("errcode", 0) + if status < 400 and int(errcode or 0) == 0: + return + errmsg = _string_field(raw, "errmsg") or "unknown error" + raise RuntimeError(f"[{action}] {errmsg} (errcode={errcode})") + + +async def request_dingtalk_app_registration() -> DingtalkAppRegistration: + status, init_raw = await _post_registration( + "/app/registration/init", + {"source": dingtalk_registration_source()}, + ) + _raise_dingtalk_registration_error(status, init_raw, "init") + nonce = _string_field(init_raw, "nonce") + if not nonce: + raise RuntimeError("[init] missing nonce") + + status, begin_raw = await _post_registration( + "/app/registration/begin", + {"nonce": nonce}, + ) + _raise_dingtalk_registration_error(status, begin_raw, "begin") + + device_code = _string_field(begin_raw, "device_code") + verification_uri_complete = _string_field(begin_raw, "verification_uri_complete") + if not device_code: + raise RuntimeError("[begin] missing device_code") + if not verification_uri_complete: + raise RuntimeError("[begin] missing verification_uri_complete") + + return DingtalkAppRegistration( + device_code=device_code, + user_code=_string_field(begin_raw, "user_code"), + verification_uri=_string_field(begin_raw, "verification_uri"), + verification_uri_complete=verification_uri_complete, + expires_in=max(_int_field(begin_raw, "expires_in", 7200), 60), + interval=max(_int_field(begin_raw, "interval", 3), 1), + ) + + +async def poll_dingtalk_app_registration_once(device_code: str) -> dict[str, Any]: + status, raw = await _post_registration( + "/app/registration/poll", + {"device_code": device_code}, + ) + _raise_dingtalk_registration_error(status, raw, "poll") + return dingtalk_registration_poll_result(raw) + + +def dingtalk_registration_poll_result(raw: dict[str, Any]) -> dict[str, Any]: + status_raw = _string_field(raw, "status").upper() + if status_raw == "WAITING": + return {"status": "pending"} + if status_raw == "SUCCESS": + client_id = _string_field(raw, "client_id") + client_secret = _string_field(raw, "client_secret") + if not client_id or not client_secret: + return {"status": "error", "message": "扫码成功但未获取到钉钉应用凭证"} + return { + "status": "created", + "client_id": client_id, + "client_secret": client_secret, + } + if status_raw == "FAIL": + return { + "status": "error", + "message": _string_field(raw, "fail_reason") or "钉钉扫码创建失败", + } + if status_raw == "EXPIRED": + return {"status": "expired", "message": "钉钉扫码已过期,请重新创建"} + return { + "status": "error", + "message": f"钉钉扫码创建返回未知状态: {status_raw or 'UNKNOWN'}", + } diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index b1a8156a4..92189781c 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -48,7 +48,9 @@ class MyEventHandler(dingtalk_stream.EventHandler): @register_platform_adapter( - "dingtalk", "钉钉机器人官方 API 适配器", support_streaming_message=True + "dingtalk", + "钉钉机器人官方 API 适配器", + support_streaming_message=True, ) class DingtalkPlatformAdapter(Platform): def __init__( @@ -141,7 +143,7 @@ class DingtalkPlatformAdapter(Platform): return PlatformMetadata( name="dingtalk", description="钉钉机器人官方 API 适配器", - id=cast(str, self.config.get("id")), + id=cast("str", self.config.get("id")), support_streaming_message=True, support_proactive_message=True, ) @@ -153,7 +155,7 @@ class DingtalkPlatformAdapter(Platform): abm = AstrBotMessage() abm.message = [] abm.message_str = "" - abm.timestamp = int(cast(int, message.create_at) / 1000) + abm.timestamp = int(cast("int", message.create_at) / 1000) abm.type = ( MessageType.GROUP_MESSAGE if message.conversation_type == "2" @@ -164,7 +166,7 @@ class DingtalkPlatformAdapter(Platform): nickname=message.sender_nick, ) abm.self_id = self._id_to_sid(message.chatbot_user_id) - abm.message_id = cast(str, message.message_id) + abm.message_id = cast("str", message.message_id) abm.raw_message = message if abm.type == MessageType.GROUP_MESSAGE: @@ -178,9 +180,9 @@ class DingtalkPlatformAdapter(Platform): else: abm.session_id = abm.sender.user_id - message_type: str = cast(str, message.message_type) - robot_code = cast(str, message.robot_code or "") - raw_content = cast(dict, message.extensions.get("content") or {}) + message_type: str = cast("str", message.message_type) + robot_code = cast("str", message.robot_code or "") + raw_content = cast("dict", message.extensions.get("content") or {}) if not isinstance(raw_content, dict): raw_content = {} match message_type: @@ -193,11 +195,12 @@ class DingtalkPlatformAdapter(Platform): await self._remember_sender_binding(message, abm) return abm image_content = cast( - dingtalk_stream.ImageContent | None, + "dingtalk_stream.ImageContent | None", message.image_content, ) download_code = cast( - str, (image_content.download_code if image_content else "") or "" + "str", + (image_content.download_code if image_content else "") or "", ) if not download_code: logger.warning("钉钉图片消息缺少 downloadCode,已跳过") @@ -213,26 +216,27 @@ class DingtalkPlatformAdapter(Platform): logger.warning("钉钉图片消息下载失败,无法解析为图片") case "richText": rtc: dingtalk_stream.RichTextContent = cast( - dingtalk_stream.RichTextContent, message.rich_text_content + "dingtalk_stream.RichTextContent", + message.rich_text_content, ) - contents: list[dict] = cast(list[dict], rtc.rich_text_list) + contents: list[dict] = cast("list[dict]", rtc.rich_text_list) plain_parts: list[str] = [] for content in contents: if "text" in content: - plain_text = cast(str, content.get("text") or "") + plain_text = cast("str", content.get("text") or "") if plain_text: plain_parts.append(plain_text) abm.message.append(Plain(plain_text)) elif "type" in content and content["type"] == "picture": - download_code = cast(str, content.get("downloadCode") or "") + download_code = cast("str", content.get("downloadCode") or "") if not download_code: logger.warning( - "钉钉富文本图片消息缺少 downloadCode,已跳过" + "钉钉富文本图片消息缺少 downloadCode,已跳过", ) continue if not robot_code: logger.error( - "钉钉富文本图片消息解析失败: 回调中缺少 robotCode" + "钉钉富文本图片消息解析失败: 回调中缺少 robotCode", ) continue f_path = await self.download_ding_file( @@ -244,13 +248,13 @@ class DingtalkPlatformAdapter(Platform): abm.message.append(Image.fromFileSystem(f_path)) abm.message_str = "".join(plain_parts).strip() case "audio" | "voice": - download_code = cast(str, raw_content.get("downloadCode") or "") + download_code = cast("str", raw_content.get("downloadCode") or "") if not download_code: logger.warning("钉钉语音消息缺少 downloadCode,已跳过") elif not robot_code: logger.error("钉钉语音消息解析失败: 回调中缺少 robotCode") else: - voice_ext = cast(str, raw_content.get("fileExtension") or "") + voice_ext = cast("str", raw_content.get("fileExtension") or "") if not voice_ext: voice_ext = "amr" voice_ext = voice_ext.lstrip(".") @@ -262,16 +266,16 @@ class DingtalkPlatformAdapter(Platform): if f_path: abm.message.append(Record.fromFileSystem(f_path)) case "file": - download_code = cast(str, raw_content.get("downloadCode") or "") + download_code = cast("str", raw_content.get("downloadCode") or "") if not download_code: logger.warning("钉钉文件消息缺少 downloadCode,已跳过") elif not robot_code: logger.error("钉钉文件消息解析失败: 回调中缺少 robotCode") else: - file_name = cast(str, raw_content.get("fileName") or "") + file_name = cast("str", raw_content.get("fileName") or "") file_ext = Path(file_name).suffix.lstrip(".") if file_name else "" if not file_ext: - file_ext = cast(str, raw_content.get("fileExtension") or "") + file_ext = cast("str", raw_content.get("fileExtension") or "") if not file_ext: file_ext = "file" f_path = await self.download_ding_file( @@ -295,14 +299,14 @@ class DingtalkPlatformAdapter(Platform): try: if abm.type == MessageType.FRIEND_MESSAGE: sender_id = abm.sender.user_id - sender_staff_id = cast(str, message.sender_staff_id or "") + sender_staff_id = cast("str", message.sender_staff_id or "") if sender_staff_id: umo = str( MessageSesion( platform_name=self.meta().id, message_type=abm.type, session_id=sender_id, - ) + ), ) await sp.put_async( "global", @@ -353,7 +357,7 @@ class DingtalkPlatformAdapter(Platform): return "" resp_data = await resp.json() download_url = cast( - str, + "str", ( resp_data.get("downloadUrl") or resp_data.get("data", {}).get("downloadUrl") @@ -389,7 +393,7 @@ class DingtalkPlatformAdapter(Platform): ) return "" data = await resp.json() - return cast(str, data.get("data", {}).get("accessToken", "")) + return cast("str", data.get("data", {}).get("accessToken", "")) async def _get_sender_staff_id(self, session: MessageSesion) -> str: try: @@ -399,7 +403,7 @@ class DingtalkPlatformAdapter(Platform): "dingtalk_staffid", "", ) - return cast(str, staff_id or "") + return cast("str", staff_id or "") except Exception as e: logger.warning(f"读取钉钉 staff_id 映射失败: {e}") return "" @@ -426,16 +430,18 @@ class DingtalkPlatformAdapter(Platform): "Content-Type": "application/json", "x-acs-dingtalk-access-token": access_token, } - async with aiohttp.ClientSession() as session: - async with session.post( + async with ( + aiohttp.ClientSession() as session, + session.post( "https://api.dingtalk.com/v1.0/robot/groupMessages/send", headers=headers, json=payload, - ) as resp: - if resp.status != 200: - logger.error( - f"钉钉群消息发送失败: {resp.status}, {await resp.text()}", - ) + ) as resp, + ): + if resp.status != 200: + logger.error( + f"钉钉群消息发送失败: {resp.status}, {await resp.text()}", + ) async def _send_private_message( self, @@ -459,16 +465,18 @@ class DingtalkPlatformAdapter(Platform): "Content-Type": "application/json", "x-acs-dingtalk-access-token": access_token, } - async with aiohttp.ClientSession() as session: - async with session.post( + async with ( + aiohttp.ClientSession() as session, + session.post( "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend", headers=headers, json=payload, - ) as resp: - if resp.status != 200: - logger.error( - f"钉钉私聊消息发送失败: {resp.status}, {await resp.text()}", - ) + ) as resp, + ): + if resp.status != 200: + logger.error( + f"钉钉私聊消息发送失败: {resp.status}, {await resp.text()}", + ) def _safe_remove_file(self, file_path: str | None) -> None: if not file_path: @@ -515,14 +523,14 @@ class DingtalkPlatformAdapter(Platform): ) as resp: if resp.status != 200: logger.error( - f"钉钉媒体上传失败: {resp.status}, {await resp.text()}" + f"钉钉媒体上传失败: {resp.status}, {await resp.text()}", ) return "" data = await resp.json() if data.get("errcode") != 0: logger.error(f"钉钉媒体上传失败: {data}") return "" - return cast(str, data.get("media_id", "")) + return cast("str", data.get("media_id", "")) async def upload_image(self, image: Image) -> str: image_file_path = await image.convert_to_file_path() @@ -694,8 +702,8 @@ class DingtalkPlatformAdapter(Platform): robot_code = self.client_id # at_list: list[str] = [] - sender_id = cast(str, incoming_message.sender_id or "") - sender_staff_id = cast(str, incoming_message.sender_staff_id or "") + sender_id = cast("str", incoming_message.sender_id or "") + sender_staff_id = cast("str", incoming_message.sender_staff_id or "") normalized_sender_id = self._id_to_sid(sender_id) # 现在用的发消息接口不支持 at # for segment in message_chain.chain: @@ -711,7 +719,7 @@ class DingtalkPlatformAdapter(Platform): if incoming_message.conversation_type == "2": await self.send_message_chain_to_group( - open_conversation_id=cast(str, incoming_message.conversation_id), + open_conversation_id=cast("str", incoming_message.conversation_id), robot_code=robot_code, message_chain=message_chain, # at_str=at_str, diff --git a/astrbot/core/platform/sources/lark/app_registration.py b/astrbot/core/platform/sources/lark/app_registration.py new file mode 100644 index 000000000..3420b426f --- /dev/null +++ b/astrbot/core/platform/sources/lark/app_registration.py @@ -0,0 +1,205 @@ +from dataclasses import dataclass +from typing import Any +from urllib.parse import urlencode + +import aiohttp + +DEFAULT_FEISHU_OPEN_DOMAIN = "https://open.feishu.cn" +DEFAULT_LARK_OPEN_DOMAIN = "https://open.larksuite.com" +APP_REGISTRATION_PATH = "/oauth/v1/app/registration" + + +@dataclass +class LarkAppRegistrationEndpoints: + accounts_base: str + open_base: str + registration: str + + +@dataclass +class LarkAppRegistration: + device_code: str + user_code: str + verification_uri: str + verification_uri_complete: str + expires_in: int + interval: int + + +def resolve_app_registration_endpoints( + domain: str, +) -> LarkAppRegistrationEndpoints: + normalized = (domain or DEFAULT_FEISHU_OPEN_DOMAIN).strip().rstrip("/") + if normalized in {"feishu", DEFAULT_FEISHU_OPEN_DOMAIN}: + accounts_base = "https://accounts.feishu.cn" + open_base = DEFAULT_FEISHU_OPEN_DOMAIN + elif normalized in {"lark", DEFAULT_LARK_OPEN_DOMAIN}: + accounts_base = "https://accounts.larksuite.com" + open_base = DEFAULT_LARK_OPEN_DOMAIN + else: + open_base = normalized + accounts_base = normalized.replace("://open.", "://accounts.", 1) + + return LarkAppRegistrationEndpoints( + accounts_base=accounts_base, + open_base=open_base, + registration=f"{accounts_base}{APP_REGISTRATION_PATH}", + ) + + +def _registration_data(raw: dict[str, Any]) -> dict[str, Any]: + data = raw.get("data") + if isinstance(data, dict): + return data + return raw + + +def _string_field(data: dict[str, Any], key: str) -> str: + value = data.get(key) + if isinstance(value, str): + return value + return "" + + +def _int_field(data: dict[str, Any], key: str, default: int) -> int: + value = data.get(key) + if isinstance(value, int): + return value + if isinstance(value, float): + return int(value) + return default + + +async def _post_registration( + endpoint: str, + form: dict[str, str], +) -> tuple[int, dict[str, Any]]: + timeout = aiohttp.ClientTimeout(total=15) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.post( + endpoint, + data=form, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) as response: + status = response.status + data = await response.json(content_type=None) + if not isinstance(data, dict): + raise RuntimeError("飞书应用创建响应格式异常") + return status, data + + +def _raise_registration_error(status: int, raw: dict[str, Any], fallback: str) -> None: + data = _registration_data(raw) + if status < 400 and not raw.get("error") and not data.get("error"): + return + message = ( + _string_field(raw, "error_description") + or _string_field(data, "error_description") + or _string_field(raw, "error") + or _string_field(data, "error") + or fallback + ) + raise RuntimeError(message) + + +async def request_app_registration(domain: str) -> LarkAppRegistration: + endpoints = resolve_app_registration_endpoints(domain) + status, raw = await _post_registration( + endpoints.registration, + { + "action": "begin", + "archetype": "PersonalAgent", + "auth_method": "client_secret", + "request_user_info": "open_id tenant_brand", + }, + ) + _raise_registration_error(status, raw, "发起扫码创建失败") + data = _registration_data(raw) + user_code = _string_field(data, "user_code") + verification_uri = _string_field(data, "verification_uri") + verification_uri_complete = _string_field(data, "verification_uri_complete") + if not verification_uri_complete and user_code: + verification_uri_complete = ( + f"{endpoints.open_base}/page/cli?{urlencode({'user_code': user_code})}" + ) + + return LarkAppRegistration( + device_code=_string_field(data, "device_code"), + user_code=user_code, + verification_uri=verification_uri, + verification_uri_complete=verification_uri_complete, + expires_in=_int_field(data, "expires_in", 300), + interval=_int_field(data, "interval", 5), + ) + + +def _tenant_brand(data: dict[str, Any]) -> str: + user_info = data.get("user_info") + if isinstance(user_info, dict): + return _string_field(user_info, "tenant_brand") + return _string_field(data, "tenant_brand") + + +async def poll_app_registration_once( + *, + domain: str, + device_code: str, +) -> dict[str, Any]: + endpoints = resolve_app_registration_endpoints(domain) + status, raw = await _post_registration( + endpoints.registration, + { + "action": "poll", + "device_code": device_code, + }, + ) + data = _registration_data(raw) + error = _string_field(raw, "error") or _string_field(data, "error") + client_id = _string_field(data, "client_id") + client_secret = _string_field(data, "client_secret") + tenant_brand = _tenant_brand(data) + + if status < 400 and not error and client_id: + if not client_secret and tenant_brand == "lark": + client_secret = await _poll_lark_secret(device_code) + if not client_secret: + return {"status": "error", "message": "应用创建成功但未获取到凭证"} + return { + "status": "created", + "app_id": client_id, + "app_secret": client_secret, + "tenant_brand": tenant_brand, + "domain": DEFAULT_LARK_OPEN_DOMAIN + if tenant_brand == "lark" + else DEFAULT_FEISHU_OPEN_DOMAIN, + } + if error == "authorization_pending": + return {"status": "pending"} + if error == "slow_down": + return {"status": "slow_down"} + if error == "access_denied": + return {"status": "denied", "message": "用户取消了扫码创建"} + if error in {"expired_token", "invalid_grant"}: + return {"status": "expired", "message": "扫码已过期,请再次创建"} + + message = ( + _string_field(raw, "error_description") + or _string_field(data, "error_description") + or error + or "获取扫码创建状态失败" + ) + return {"status": "error", "message": message} + + +async def _poll_lark_secret(device_code: str) -> str: + endpoints = resolve_app_registration_endpoints(DEFAULT_LARK_OPEN_DOMAIN) + status, raw = await _post_registration( + endpoints.registration, + { + "action": "poll", + "device_code": device_code, + }, + ) + if status >= 400 or raw.get("error"): + return "" + return _string_field(_registration_data(raw), "client_secret") diff --git a/astrbot/core/platform/sources/lark/bot_info.py b/astrbot/core/platform/sources/lark/bot_info.py new file mode 100644 index 000000000..d8db8141b --- /dev/null +++ b/astrbot/core/platform/sources/lark/bot_info.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass +from typing import Any + +import aiohttp + +from .app_registration import DEFAULT_FEISHU_OPEN_DOMAIN, DEFAULT_LARK_OPEN_DOMAIN + +TENANT_ACCESS_TOKEN_INTERNAL_PATH = "/open-apis/auth/v3/tenant_access_token/internal" +BOT_INFO_PATH = "/open-apis/bot/v3/info" + + +@dataclass +class LarkBotInfo: + app_name: str + open_id: str + + +def _open_base(domain: str) -> str: + normalized = (domain or DEFAULT_FEISHU_OPEN_DOMAIN).strip().rstrip("/") + if normalized in {"feishu", DEFAULT_FEISHU_OPEN_DOMAIN}: + return DEFAULT_FEISHU_OPEN_DOMAIN + if normalized in {"lark", DEFAULT_LARK_OPEN_DOMAIN}: + return DEFAULT_LARK_OPEN_DOMAIN + return normalized + + +def _string_field(data: dict[str, Any], key: str) -> str: + value = data.get(key) + return value if isinstance(value, str) else "" + + +async def _post_json( + endpoint: str, + payload: dict[str, str], +) -> dict[str, Any]: + timeout = aiohttp.ClientTimeout(total=15) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.post(endpoint, json=payload) as response: + data = await response.json(content_type=None) + if not isinstance(data, dict): + raise RuntimeError("飞书接口响应格式异常") + return data + + +async def _get_json( + endpoint: str, + *, + headers: dict[str, str], +) -> dict[str, Any]: + timeout = aiohttp.ClientTimeout(total=15) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get(endpoint, headers=headers) as response: + data = await response.json(content_type=None) + if not isinstance(data, dict): + raise RuntimeError("飞书接口响应格式异常") + return data + + +async def request_lark_bot_info( + *, + domain: str, + app_id: str, + app_secret: str, +) -> LarkBotInfo: + open_base = _open_base(domain) + token_data = await _post_json( + f"{open_base}{TENANT_ACCESS_TOKEN_INTERNAL_PATH}", + { + "app_id": app_id, + "app_secret": app_secret, + }, + ) + if token_data.get("code") != 0: + raise RuntimeError(_string_field(token_data, "msg") or "获取飞书访问令牌失败") + + tenant_access_token = _string_field(token_data, "tenant_access_token") + if not tenant_access_token: + raise RuntimeError("飞书访问令牌响应缺少 tenant_access_token") + + bot_data = await _get_json( + f"{open_base}{BOT_INFO_PATH}", + headers={"Authorization": f"Bearer {tenant_access_token}"}, + ) + if bot_data.get("code") != 0: + raise RuntimeError(_string_field(bot_data, "msg") or "获取飞书机器人信息失败") + + bot = bot_data.get("bot") + if not isinstance(bot, dict): + raise RuntimeError("飞书机器人信息响应缺少 bot 字段") + + return LarkBotInfo( + app_name=_string_field(bot, "app_name"), + open_id=_string_field(bot, "open_id"), + ) diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py index 352251155..f2a5d7d3f 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -27,6 +27,7 @@ from astrbot.core.platform.register import register_platform_adapter from astrbot.core.utils.astrbot_path import get_astrbot_temp_path from astrbot.core.utils.webhook_utils import log_webhook_info +from .bot_info import request_lark_bot_info from .lark_event import LarkMessageEvent from .server import LarkWebhookServer @@ -48,10 +49,14 @@ class LarkPlatformAdapter(Platform): self.appsecret = platform_config["app_secret"] self.domain = platform_config.get("domain", lark.FEISHU_DOMAIN) self.bot_name = platform_config.get("lark_bot_name", "astrbot") - self.connection_mode = platform_config.get("lark_connection_mode", "socket") + self.bot_open_id = "" if not self.bot_name: logger.warning("未设置飞书机器人名称,@ 机器人可能得不到回复。") + # socket or webhook + self.connection_mode = platform_config.get("lark_connection_mode", "socket") + + # 初始化 WebSocket 长连接相关配置 async def on_msg_event_recv(event: lark.im.v1.P2ImMessageReceiveV1) -> None: await self.convert_msg(event) @@ -470,7 +475,7 @@ class LarkPlatformAdapter(Platform): ) if message.chat_type == "group": abm.group_id = message.chat_id - abm.self_id = self.bot_name + abm.self_id = self.bot_open_id or self.bot_name abm.message_str = "" at_list = {} if message.parent_id: @@ -483,9 +488,11 @@ class LarkPlatformAdapter(Platform): continue open_id = m.id.open_id or "" at_list[m.key] = Comp.At(qq=open_id, name=m.name) - if m.name == self.bot_name: - if m.id.open_id is not None: - abm.self_id = m.id.open_id + + if (self.bot_open_id and open_id == self.bot_open_id) or ( + m.name == self.bot_name + ): + abm.self_id = open_id or self.bot_open_id or self.bot_name if message.content is None: logger.warning("[Lark] 消息内容为空") return @@ -562,6 +569,11 @@ class LarkPlatformAdapter(Platform): logger.error(f"[Lark Webhook] 处理事件失败: {e}", exc_info=True) async def run(self) -> None: + try: + await self._refresh_bot_info() + except Exception as e: + logger.error(f"[Lark] 启动时获取机器人信息失败: {e}", exc_info=True) + if self.connection_mode == "webhook": if self.webhook_server is None: logger.error("[Lark] Webhook 模式已启用,但 webhook_server 未初始化") @@ -580,6 +592,17 @@ class LarkPlatformAdapter(Platform): return ({"error": "Webhook server not initialized"}, 500) return await self.webhook_server.handle_callback(request) + async def _refresh_bot_info(self) -> None: + bot_info = await request_lark_bot_info( + domain=self.domain, + app_id=self.appid, + app_secret=self.appsecret, + ) + if bot_info.app_name: + self.bot_name = bot_info.app_name + if bot_info.open_id: + self.bot_open_id = bot_info.open_id + async def terminate(self) -> None: if self.connection_mode == "socket": await self.client._disconnect() diff --git a/astrbot/core/platform/sources/lark/lark_event.py b/astrbot/core/platform/sources/lark/lark_event.py index 13b7ddec9..280e7a069 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -74,6 +74,7 @@ class LarkMessageEvent(AstrMessageEvent): Returns: 是否发送成功 + """ if lark_client.im is None: logger.error("[Lark] API Client im 模块未初始化") @@ -89,7 +90,7 @@ class LarkMessageEvent(AstrMessageEvent): .msg_type(msg_type) .uuid(str(uuid.uuid4())) .reply_in_thread(False) - .build() + .build(), ) .build() ) @@ -115,7 +116,7 @@ class LarkMessageEvent(AstrMessageEvent): .content(content) .msg_type(msg_type) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -145,6 +146,7 @@ class LarkMessageEvent(AstrMessageEvent): Returns: 成功返回file_key,失败返回None + """ if not path or not os.path.exists(path): logger.error(f"[Lark] 文件不存在: {path}") @@ -174,7 +176,7 @@ class LarkMessageEvent(AstrMessageEvent): if not response.success(): logger.error( - f"[Lark] 无法上传文件({response.code}): {response.msg}" + f"[Lark] 无法上传文件({response.code}): {response.msg}", ) return None @@ -207,7 +209,7 @@ class LarkMessageEvent(AstrMessageEvent): file_path = comp.file.replace("file:///", "") elif comp.file and comp.file.startswith("http"): image_file_path = await download_image_by_url(comp.file) - file_path = image_file_path if image_file_path else "" + file_path = image_file_path or "" elif comp.file and comp.file.startswith("base64://"): base64_str = comp.file.removeprefix("base64://") image_data = base64.b64decode(base64_str) @@ -220,7 +222,7 @@ class LarkMessageEvent(AstrMessageEvent): with open(file_path, "wb") as f: f.write(BytesIO(image_data).getvalue()) else: - file_path = comp.file if comp.file else "" + file_path = comp.file or "" if image_file is None: if not file_path: @@ -307,7 +309,7 @@ class LarkMessageEvent(AstrMessageEvent): { "tag": "markdown", "content": reasoning_content, - } + }, ], } @@ -321,8 +323,8 @@ class LarkMessageEvent(AstrMessageEvent): reasoning_content=reasoning_content, title=title, expanded=False, - ) - ] + ), + ], }, } @@ -341,7 +343,7 @@ class LarkMessageEvent(AstrMessageEvent): reasoning_content=reasoning_content, title=str(comp.data.get("title", "💭 Thinking")), expanded=bool(comp.data.get("expanded", False)), - ) + ), ) elif isinstance(comp, Plain): if comp.text: @@ -379,9 +381,9 @@ class LarkMessageEvent(AstrMessageEvent): CreateCardRequestBody.builder() .type("card_json") .data(json.dumps(card_json, ensure_ascii=False)) - .build() + .build(), ) - .build() + .build(), ) except Exception as e: logger.error(f"[Lark] 创建卡片失败: {e}") @@ -446,6 +448,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型,如 'open_id', 'chat_id'(用于主动发送) + """ if lark_client.im is None: logger.error("[Lark] API Client im 模块未初始化") @@ -554,17 +557,29 @@ class LarkMessageEvent(AstrMessageEvent): # 发送附件 for file_comp in file_components: await LarkMessageEvent._send_file_message( - file_comp, lark_client, reply_message_id, receive_id, receive_id_type + file_comp, + lark_client, + reply_message_id, + receive_id, + receive_id_type, ) for audio_comp in audio_components: await LarkMessageEvent._send_audio_message( - audio_comp, lark_client, reply_message_id, receive_id, receive_id_type + audio_comp, + lark_client, + reply_message_id, + receive_id, + receive_id_type, ) for media_comp in media_components: await LarkMessageEvent._send_media_message( - media_comp, lark_client, reply_message_id, receive_id, receive_id_type + media_comp, + lark_client, + reply_message_id, + receive_id, + receive_id_type, ) async def send(self, message: MessageChain) -> None: @@ -592,10 +607,13 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ file_path = file_comp.file or "" file_key = await LarkMessageEvent._upload_lark_file( - lark_client, path=file_path, file_type="stream" + lark_client, + path=file_path, + file_type="stream", ) if not file_key: return @@ -626,6 +644,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ # 获取音频文件路径 try: @@ -699,6 +718,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ # 获取视频文件路径 try: @@ -803,8 +823,8 @@ class LarkMessageEvent(AstrMessageEvent): "tag": "markdown", "content": "", "element_id": "markdown_1", - } - ] + }, + ], }, } @@ -814,7 +834,7 @@ class LarkMessageEvent(AstrMessageEvent): CreateCardRequestBody.builder() .type("card_json") .data(json.dumps(card_json, ensure_ascii=False)) - .build() + .build(), ) .build() ) @@ -827,7 +847,7 @@ class LarkMessageEvent(AstrMessageEvent): if not response.success(): logger.error( - f"[Lark] 创建流式卡片实体失败({response.code}): {response.msg}" + f"[Lark] 创建流式卡片实体失败({response.code}): {response.msg}", ) return None @@ -880,7 +900,7 @@ class LarkMessageEvent(AstrMessageEvent): .content(content) .sequence(sequence) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -920,7 +940,7 @@ class LarkMessageEvent(AstrMessageEvent): .settings(settings_json) .sequence(sequence) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -950,7 +970,7 @@ class LarkMessageEvent(AstrMessageEvent): await self.send(buffer) asyncio.create_task( - Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name) + Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name), ) self._has_send_oper = True @@ -1003,7 +1023,7 @@ class LarkMessageEvent(AstrMessageEvent): buffer.squash_plain() await self.send(buffer) asyncio.create_task( - Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name) + Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name), ) self._has_send_oper = True @@ -1050,7 +1070,7 @@ class LarkMessageEvent(AstrMessageEvent): card_id = await self._create_streaming_card() if not card_id: logger.warning( - "[Lark] 无法创建流式卡片,回退到非流式发送" + "[Lark] 无法创建流式卡片,回退到非流式发送", ) await _consume_rest_and_fallback(generator, delta) return @@ -1061,7 +1081,7 @@ class LarkMessageEvent(AstrMessageEvent): ) if not sent: logger.error( - "[Lark] 发送流式卡片消息失败,回退到非流式发送" + "[Lark] 发送流式卡片消息失败,回退到非流式发送", ) await _consume_rest_and_fallback(generator, delta) return @@ -1081,8 +1101,9 @@ class LarkMessageEvent(AstrMessageEvent): if not fallback_used: asyncio.create_task( Metric.upload( - msg_event_tick=1, adapter_name=self.platform_meta.name - ) + msg_event_tick=1, + adapter_name=self.platform_meta.name, + ), ) self._has_send_oper = True return @@ -1091,6 +1112,6 @@ class LarkMessageEvent(AstrMessageEvent): # 内联父类 send_streaming 的副作用 asyncio.create_task( - Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name) + Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name), ) self._has_send_oper = True diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index b4d494b34..f5a7dfab8 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -89,7 +89,7 @@ class WebChatAdapter(Platform): ) -> None: conversation_id = _extract_conversation_id(session.session_id) active_request_ids = self._webchat_queue_mgr.list_back_request_ids( - conversation_id + conversation_id, ) stream_request_ids = [ req_id for req_id in active_request_ids if not req_id.startswith("ws_sub_") @@ -154,7 +154,8 @@ class WebChatAdapter(Platform): ) async def _get_message_history( - self, message_id: int + self, + message_id: int, ) -> PlatformMessageHistory | None: return await db_helper.get_platform_message_history_by_id(message_id) @@ -173,6 +174,7 @@ class WebChatAdapter(Platform): Returns: tuple[list, list[str]]: (消息组件列表, 纯文本列表) + """ async def get_reply_parts( @@ -247,12 +249,14 @@ class WebChatAdapter(Platform): message_event.set_extra("selected_provider", payload.get("selected_provider")) message_event.set_extra("selected_model", payload.get("selected_model")) message_event.set_extra( - "enable_streaming", payload.get("enable_streaming", True) + "enable_streaming", + payload.get("enable_streaming", True), ) message_event.set_extra("action_type", payload.get("action_type")) message_event.set_extra("llm_checkpoint_id", payload.get("llm_checkpoint_id")) message_event.set_extra( - "thread_selected_text", payload.get("thread_selected_text") + "thread_selected_text", + payload.get("thread_selected_text"), ) self.commit_event(message_event) diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index 31436ebf2..1b191bb56 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -66,7 +66,7 @@ def _extract_wecom_media_filename(disposition: str | None) -> str | None: class WecomServer: def __init__(self, event_queue: asyncio.Queue, config: dict) -> None: self.server = quart.Quart(__name__) - self.port = int(cast(str, config.get("port"))) + self.port = int(cast("str", config.get("port"))) self.callback_server_host = config.get("callback_server_host", "0.0.0.0") self.server.add_url_rule( "/callback/command", @@ -101,6 +101,7 @@ class WecomServer: Returns: 验证响应 + """ logger.info(f"验证请求有效性: {request.args}") args = request.args @@ -129,6 +130,7 @@ class WecomServer: Returns: 响应内容 + """ data = await request.get_data() msg_signature = request.args.get("msg_signature") @@ -140,7 +142,7 @@ class WecomServer: logger.error("解密失败,签名异常,请检查配置。") raise else: - msg = cast(BaseMessage, parse_message(xml)) + msg = cast("BaseMessage", parse_message(xml)) logger.info(f"解析成功: {msg}") if self.callback: @@ -355,8 +357,7 @@ class WecomPlatformAdapter(Platform): # 根据请求方法分发到不同的处理函数 if request.method == "GET": return await self.server.handle_verify(request) - else: - return await self.server.handle_callback(request) + return await self.server.handle_callback(request) async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None: abm = AstrBotMessage() @@ -366,11 +367,11 @@ class WecomPlatformAdapter(Platform): abm.message = [Plain(msg.content)] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) abm.message_id = str(msg.id) - abm.timestamp = int(cast(int | str, msg.time)) + abm.timestamp = int(cast("int | str", msg.time)) abm.session_id = abm.sender.user_id abm.raw_message = msg elif isinstance(msg, ImageMessage): @@ -379,11 +380,11 @@ class WecomPlatformAdapter(Platform): abm.message = [Image(file=msg.image, url=msg.image)] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) abm.message_id = str(msg.id) - abm.timestamp = int(cast(int | str, msg.time)) + abm.timestamp = int(cast("int | str", msg.time)) abm.session_id = abm.sender.user_id abm.raw_message = msg elif isinstance(msg, VoiceMessage): @@ -410,11 +411,11 @@ class WecomPlatformAdapter(Platform): abm.message = [Record(file=path_wav, url=path_wav)] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) abm.message_id = str(msg.id) - abm.timestamp = int(cast(int | str, msg.time)) + abm.timestamp = int(cast("int | str", msg.time)) abm.session_id = abm.sender.user_id abm.raw_message = msg else: @@ -427,7 +428,7 @@ class WecomPlatformAdapter(Platform): async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None: msgtype = msg.get("msgtype") - external_userid = cast(str, msg.get("external_userid")) + external_userid = cast("str", msg.get("external_userid")) abm = AstrBotMessage() abm.raw_message = msg abm.raw_message["_wechat_kf_flag"] = None # 方便处理 diff --git a/astrbot/core/platform/sources/weixin_oc/login_registration.py b/astrbot/core/platform/sources/weixin_oc/login_registration.py new file mode 100644 index 000000000..4310924d9 --- /dev/null +++ b/astrbot/core/platform/sources/weixin_oc/login_registration.py @@ -0,0 +1,167 @@ +from dataclasses import dataclass +from typing import Any + +from .weixin_oc_client import WeixinOCClient + +DEFAULT_WEIXIN_OC_BASE_URL = "https://ilinkai.weixin.qq.com" +DEFAULT_WEIXIN_OC_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c" +DEFAULT_WEIXIN_OC_BOT_TYPE = "3" +DEFAULT_WEIXIN_OC_QR_POLL_INTERVAL = 1 +DEFAULT_WEIXIN_OC_LONG_POLL_TIMEOUT_MS = 35_000 +DEFAULT_WEIXIN_OC_API_TIMEOUT_MS = 15_000 + + +@dataclass +class WeixinOCLoginRegistration: + qrcode: str + qrcode_img_content: str + interval: int + + +def normalize_weixin_oc_base_url(base_url: str | None) -> str: + return (base_url or DEFAULT_WEIXIN_OC_BASE_URL).strip().rstrip("/") + + +def _string_field(data: dict[str, Any], key: str) -> str: + value = data.get(key) + if isinstance(value, str): + return value.strip() + return "" + + +def _int_config(value: Any, default: int, minimum: int) -> int: + try: + parsed = int(value) + except (TypeError, ValueError): + parsed = default + return max(parsed, minimum) + + +def weixin_oc_login_result( + data: dict[str, Any], + *, + default_base_url: str, +) -> dict[str, Any]: + raw_status = _string_field(data, "status") or "wait" + if raw_status == "confirmed": + bot_token = _string_field(data, "bot_token") + if not bot_token: + return {"status": "error", "message": "登录成功但未返回 token"} + base_url = _string_field(data, "baseurl") or default_base_url + return { + "status": "created", + "qr_status": raw_status, + "weixin_oc_token": bot_token, + "weixin_oc_account_id": _string_field(data, "ilink_bot_id"), + "weixin_oc_base_url": normalize_weixin_oc_base_url(base_url), + "weixin_oc_user_id": _string_field(data, "ilink_user_id"), + } + if raw_status == "expired": + return {"status": "expired", "qr_status": raw_status, "message": "二维码已过期"} + if raw_status in {"cancel", "canceled", "denied"}: + return {"status": "denied", "qr_status": raw_status, "message": "用户取消登录"} + return {"status": "pending", "qr_status": raw_status} + + +def _client( + *, + adapter_id: str, + base_url: str, + api_timeout_ms: int, +) -> WeixinOCClient: + return WeixinOCClient( + adapter_id=adapter_id, + base_url=base_url, + cdn_base_url=DEFAULT_WEIXIN_OC_CDN_BASE_URL, + api_timeout_ms=api_timeout_ms, + ) + + +async def request_weixin_oc_login_qr( + platform_config: dict[str, Any], +) -> WeixinOCLoginRegistration: + base_url = normalize_weixin_oc_base_url( + _string_field(platform_config, "weixin_oc_base_url"), + ) + bot_type = _string_field(platform_config, "weixin_oc_bot_type") + if not bot_type: + bot_type = DEFAULT_WEIXIN_OC_BOT_TYPE + api_timeout_ms = _int_config( + platform_config.get("weixin_oc_api_timeout_ms"), + DEFAULT_WEIXIN_OC_API_TIMEOUT_MS, + 1_000, + ) + interval = _int_config( + platform_config.get("weixin_oc_qr_poll_interval"), + DEFAULT_WEIXIN_OC_QR_POLL_INTERVAL, + 1, + ) + + client = _client( + adapter_id=str(platform_config.get("id") or "weixin_oc"), + base_url=base_url, + api_timeout_ms=api_timeout_ms, + ) + try: + data = await client.request_json( + "GET", + "ilink/bot/get_bot_qrcode", + params={"bot_type": bot_type}, + token_required=False, + timeout_ms=15_000, + ) + finally: + await client.close() + + qrcode = _string_field(data, "qrcode") + qrcode_img_content = _string_field(data, "qrcode_img_content") + if not qrcode or not qrcode_img_content: + raise RuntimeError("个人微信二维码响应格式异常") + + return WeixinOCLoginRegistration( + qrcode=qrcode, + qrcode_img_content=qrcode_img_content, + interval=interval, + ) + + +async def poll_weixin_oc_login_once( + *, + platform_config: dict[str, Any], + qrcode: str, +) -> dict[str, Any]: + if not qrcode: + raise ValueError("Missing qrcode") + + base_url = normalize_weixin_oc_base_url( + _string_field(platform_config, "weixin_oc_base_url"), + ) + api_timeout_ms = _int_config( + platform_config.get("weixin_oc_api_timeout_ms"), + DEFAULT_WEIXIN_OC_API_TIMEOUT_MS, + 1_000, + ) + long_poll_timeout_ms = _int_config( + platform_config.get("weixin_oc_long_poll_timeout_ms"), + DEFAULT_WEIXIN_OC_LONG_POLL_TIMEOUT_MS, + 1_000, + ) + + client = _client( + adapter_id=str(platform_config.get("id") or "weixin_oc"), + base_url=base_url, + api_timeout_ms=api_timeout_ms, + ) + try: + data = await client.request_json( + "GET", + "ilink/bot/get_qrcode_status", + params={"qrcode": qrcode}, + token_required=False, + timeout_ms=long_poll_timeout_ms, + headers={"iLink-App-ClientVersion": "1"}, + ) + finally: + await client.close() + + return weixin_oc_login_result(data, default_base_url=base_url) diff --git a/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py b/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py index 55c41aa98..cbba01894 100644 --- a/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py +++ b/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py @@ -61,6 +61,7 @@ class TypingSessionState: @register_platform_adapter("weixin_oc", "个人微信", support_streaming_message=False) class WeixinOCAdapter(Platform): + SESSION_TIMEOUT_ERRCODE = -14 IMAGE_ITEM_TYPE = 2 VOICE_ITEM_TYPE = 3 FILE_ITEM_TYPE = 4 @@ -775,6 +776,50 @@ class WeixinOCAdapter(Platform): ) return True + def _build_cache_components_from_items( + self, + item_list: list[dict[str, Any]], + ) -> list[Any]: + components: list[Any] = [] + for item in item_list: + item_type = int(item.get("type") or 0) + if item_type != 1: + continue + text = str(item.get("text_item", {}).get("text", "")).strip() + if text: + components.append(Plain(text)) + return components + + @staticmethod + def _is_successful_api_payload(payload: dict[str, Any]) -> bool: + ret = payload.get("ret", 0) + errcode = payload.get("errcode", 0) + return int(ret or 0) == 0 and int(errcode or 0) == 0 + + @staticmethod + def _format_api_error(payload: dict[str, Any]) -> str: + ret = int(payload.get("ret") or 0) + errcode = int(payload.get("errcode") or 0) + errmsg = str(payload.get("errmsg", "")) + return f"ret={ret}, errcode={errcode}, errmsg={errmsg}" + + @staticmethod + def _api_errcode(payload: dict[str, Any]) -> int: + return int(payload.get("errcode") or 0) + + async def _handle_inbound_session_timeout(self) -> None: + logger.warning( + "weixin_oc(%s): session timed out, clearing login state and waiting for QR login.", + self.meta().id, + ) + self.token = None + self.account_id = None + self._sync_buf = "" + self._context_tokens = {} + self._context_tokens_dirty = False + self._login_session = None + await self._save_account_state() + async def _send_media_segment( self, user_id: str, @@ -816,7 +861,12 @@ class WeixinOCAdapter(Platform): file_name, ) except Exception as e: - logger.error("weixin_oc(%s): prepare media failed: %s", self.meta().id, e) + logger.error( + "weixin_oc(%s): prepare media failed: %s", + self.meta().id, + e, + exc_info=True, + ) return False if text: await self._send_items_to_session( @@ -1065,6 +1115,10 @@ class WeixinOCAdapter(Platform): self.meta().id, self._last_inbound_error, ) + if self._api_errcode(data) == self.SESSION_TIMEOUT_ERRCODE: + await self._handle_inbound_session_timeout() + return + await asyncio.sleep(5) return should_save_state = self._context_tokens_dirty @@ -1115,17 +1169,20 @@ class WeixinOCAdapter(Platform): target_user = session.session_id pending_text = "" has_supported_segment = False + failed_segments = 0 for segment in message_chain.chain: if isinstance(segment, Plain): pending_text += segment.text continue if isinstance(segment, (Image, Video, File)): has_supported_segment = True - await self._send_media_segment( + sent = await self._send_media_segment( target_user, segment, text=pending_text.strip() or None, ) + if not sent: + failed_segments += 1 pending_text = "" continue logger.debug( @@ -1135,12 +1192,19 @@ class WeixinOCAdapter(Platform): ) if pending_text: has_supported_segment = True - await self._send_to_session(target_user, pending_text.strip()) + sent = await self._send_to_session(target_user, pending_text.strip()) + if not sent: + failed_segments += 1 if not has_supported_segment: logger.warning( "weixin_oc(%s): outbound message ignored, no supported segments", self.meta().id, ) + if failed_segments: + raise RuntimeError( + f"weixin_oc({self.meta().id}, target_user={target_user}) " + f"failed to send {failed_segments} message segment(s)", + ) await super().send_by_session(session, message_chain) def meta(self) -> PlatformMetadata: diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index 8e12683ff..8129e70d1 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -480,7 +480,7 @@ class LLMResponse: ), # the extra_content will not serialize if it's None when calling ToolCall.model_dump() extra_content=self.tools_call_extra_content.get( - self.tools_call_ids[idx] + self.tools_call_ids[idx], ), ), ) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 042ca2ec4..f15ee0ed8 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -484,6 +484,14 @@ class ProviderManager: from .sources.gemini_embedding_source import ( GeminiEmbeddingProvider as GeminiEmbeddingProvider, ) + case "nvidia_embedding": + from .sources.nvidia_embedding_source import ( + NvidiaEmbeddingProvider as NvidiaEmbeddingProvider, + ) + case "ollama_embedding": + from .sources.ollama_embedding_source import ( + OllamaEmbeddingProvider as OllamaEmbeddingProvider, + ) case "vllm_rerank": from .sources.vllm_rerank_source import ( VLLMRerankProvider as VLLMRerankProvider, diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index e13a0fc18..bafb49100 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -11,6 +11,7 @@ from urllib.parse import urlparse import aiofiles import anyio +import httpx from google import genai from google.genai import types from google.genai.errors import APIError @@ -66,6 +67,9 @@ class ProviderGoogleGenAI(Provider): self.api_base: str | None = provider_config.get("api_base", None) if self.api_base and self.api_base.endswith("/"): self.api_base = self.api_base[:-1] + + self._http_client: httpx.AsyncClient | None = None + self._stale_http_clients: list[httpx.AsyncClient] = [] self._init_client() self.set_model(provider_config.get("model", "unknown")) self._init_safety_settings() @@ -77,9 +81,29 @@ class ProviderGoogleGenAI(Provider): base_url=self.api_base, timeout=self.timeout * 1000, ) + + # 强制使用 httpx 作为异步 HTTP 后端,避免 aiohttp 响应类型兼容问题 (#7564) + # httpx.AsyncClient 的 timeout 单位为秒(与 HttpOptions 的毫秒不同) + async_client_kwargs: dict = { + "base_url": self.api_base, + "timeout": self.timeout, + } if proxy: - http_options.async_client_args = {"proxy": proxy} - logger.info(f"[Gemini] 使用代理: {proxy}") + async_client_kwargs["proxy"] = proxy + async_client_kwargs["trust_env"] = False + logger.info("[Gemini] 使用代理") + else: + async_client_kwargs["trust_env"] = True + + # Track the previous client so it can be closed in terminate() instead + # of leaking when _init_client is called again (e.g. via set_key). + # Only the most recent stale client is kept to avoid unbounded growth. + if self._http_client is not None: + self._stale_http_clients = [self._http_client] + + self._http_client = httpx.AsyncClient(**async_client_kwargs) + http_options.httpx_async_client = self._http_client + self.client = genai.Client( api_key=self.chosen_api_key, http_options=http_options, @@ -948,6 +972,30 @@ class ProviderGoogleGenAI(Provider): image_bs64 = base64.b64encode(await f.read()).decode("utf-8") return "data:image/jpeg;base64," + image_bs64 + async def _close_httpx_client(self, client: httpx.AsyncClient | None) -> None: + """Safely close an httpx.AsyncClient, swallowing errors for idempotency.""" + if client is None: + return + try: + await client.aclose() + except Exception as e: + # Idempotent: ignore errors from already-closed or broken clients, + # but log at debug to aid diagnosing unexpected shutdown issues. + logger.debug(f"[Gemini] Ignored error while closing httpx client: {e}") + async def terminate(self) -> None: - if self.client: - await self.client.aclose() + # Close the active Gemini client (external httpx client is managed + # separately so genai.Client.aclose skips it). + if self.client is not None: + try: + await self.client.aclose() + except Exception: + pass + self.client = None + + # Close all tracked httpx clients (stale + current). + for client in self._stale_http_clients: + await self._close_httpx_client(client) + self._stale_http_clients.clear() + await self._close_httpx_client(self._http_client) + self._http_client = None diff --git a/astrbot/core/provider/sources/minimax_tts_api_source.py b/astrbot/core/provider/sources/minimax_tts_api_source.py index afa0ade69..1c472adb8 100644 --- a/astrbot/core/provider/sources/minimax_tts_api_source.py +++ b/astrbot/core/provider/sources/minimax_tts_api_source.py @@ -37,12 +37,21 @@ class ProviderMiniMaxTTSAPI(TTSProvider): "minimax-is-timber-weight", False, ) - self.timber_weight: list[dict[str, str | int]] = json.loads( - provider_config.get( - "minimax-timber-weight", - '[{"voice_id": "Chinese (Mandarin)_Warm_Girl", "weight": 1}]', - ), - ) + default_timber_weight = [ + {"voice_id": "Chinese (Mandarin)_Warm_Girl", "weight": 1}, + ] + raw_timber_weight = provider_config.get("minimax-timber-weight", "") + if not raw_timber_weight: + self.timber_weight = default_timber_weight + else: + try: + self.timber_weight = json.loads(raw_timber_weight) + except json.JSONDecodeError: + logger.warning( + "MiniMax TTS 权重配置解析失败,将使用默认值。 raw_value: %s", + raw_timber_weight, + ) + self.timber_weight = default_timber_weight self.voice_setting: dict = { "speed": provider_config.get("minimax-voice-speed", 1.0), diff --git a/astrbot/core/provider/sources/nvidia_embedding_source.py b/astrbot/core/provider/sources/nvidia_embedding_source.py new file mode 100644 index 000000000..41cc7455c --- /dev/null +++ b/astrbot/core/provider/sources/nvidia_embedding_source.py @@ -0,0 +1,138 @@ +import aiohttp + +from astrbot import logger + +from ..entities import ProviderType +from ..provider import EmbeddingProvider +from ..register import register_provider_adapter + + +@register_provider_adapter( + "nvidia_embedding", + "NVIDIA NIM Embedding 提供商适配器", + provider_type=ProviderType.EMBEDDING, +) +class NvidiaEmbeddingProvider(EmbeddingProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config, provider_settings) + self.provider_config = provider_config + self.provider_settings = provider_settings + + self.api_key = provider_config.get("embedding_api_key", "") + self.base_url = ( + provider_config.get( + "embedding_api_base", + "https://integrate.api.nvidia.com/v1", + ) + .rstrip("/") + .removesuffix("/embeddings") + ) + self.timeout = int(provider_config.get("timeout", 20)) + self.model = provider_config.get( + "embedding_model", + "nvidia/llama-nemotron-embed-1b-v2", + ) + self.input_type = provider_config.get("input_type", "passage") + + proxy = provider_config.get("proxy", "") + self.proxy = proxy + if proxy: + logger.info(f"[NVIDIA Embedding] Using proxy: {proxy}") + + self.client = None + self.set_model(self.model) + + async def _get_client(self): + if self.client is None or self.client.closed: + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "Accept": "application/json", + } + timeout = aiohttp.ClientTimeout(total=self.timeout) + self.client = aiohttp.ClientSession( + headers=headers, + timeout=timeout, + ) + return self.client + + def _build_payload(self, text: str | list[str]) -> dict: + if isinstance(text, str): + input_text = [text] + else: + input_text = text + + return { + "input": input_text, + "model": self.model, + "input_type": self.input_type, + "encoding_format": "float", + } + + def _parse_response(self, response_data: dict) -> list[list[float]]: + data = response_data.get("data", []) + embeddings = [] + for item in data: + embedding = item.get("embedding", []) + embeddings.append(embedding) + return embeddings + + async def get_embedding(self, text: str) -> list[float]: + embeddings = await self.get_embeddings([text]) + return embeddings[0] if embeddings else [] + + async def get_embeddings(self, text: list[str]) -> list[list[float]]: + client = await self._get_client() + if not client or client.closed: + raise Exception("[NVIDIA Embedding] Client session not initialized") + + payload = self._build_payload(text) + request_url = f"{self.base_url}/embeddings" + + try: + async with client.post( + request_url, + json=payload, + proxy=self.proxy or None, + ) as response: + if response.status != 200: + error_text = await response.text() + logger.error( + f"[NVIDIA Embedding] API Error: {response.status} - {error_text}", + ) + raise Exception( + f"NVIDIA Embedding API request failed: HTTP {response.status} - {error_text}", + ) + + response_data = await response.json() + embeddings = self._parse_response(response_data) + + usage = response_data.get("usage", {}) + total_tokens = usage.get("total_tokens", 0) + if total_tokens > 0: + logger.debug(f"[NVIDIA Embedding] Token usage: {total_tokens}") + + return embeddings + + except aiohttp.ClientError as e: + logger.error(f"[NVIDIA Embedding] Network error: {e}") + raise + except Exception as e: + logger.error(f"[NVIDIA Embedding] Error: {e}", exc_info=True) + raise + + def get_dim(self) -> int: + if "embedding_dimensions" in self.provider_config: + try: + return int(self.provider_config["embedding_dimensions"]) + except (ValueError, TypeError): + logger.warning( + f"embedding_dimensions in embedding configs is not a valid integer: " + f"'{self.provider_config['embedding_dimensions']}', ignored.", + ) + return 0 + + async def terminate(self): + if self.client and not self.client.closed: + await self.client.close() + self.client = None diff --git a/astrbot/core/provider/sources/ollama_embedding_source.py b/astrbot/core/provider/sources/ollama_embedding_source.py new file mode 100644 index 000000000..b1842ef5e --- /dev/null +++ b/astrbot/core/provider/sources/ollama_embedding_source.py @@ -0,0 +1,122 @@ +import aiohttp + +from astrbot import logger + +from ..entities import ProviderType +from ..provider import EmbeddingProvider +from ..register import register_provider_adapter + + +@register_provider_adapter( + "ollama_embedding", + "Ollama Embedding 提供商适配器", + provider_type=ProviderType.EMBEDDING, +) +class OllamaEmbeddingProvider(EmbeddingProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config, provider_settings) + self.provider_config = provider_config + self.provider_settings = provider_settings + + self.base_url = ( + provider_config.get("embedding_api_base", "http://localhost:11434") + .rstrip("/") + .removesuffix("/api/embed") + ) + self.timeout = int(provider_config.get("timeout", 60)) + self.model = provider_config.get("embedding_model", "nomic-embed-text") + + proxy = provider_config.get("proxy", "") + self.proxy = proxy + if proxy: + logger.info(f"[Ollama Embedding] Using proxy: {proxy}") + + self.client = None + self.set_model(self.model) + + async def _get_client(self): + if self.client is None or self.client.closed: + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + timeout = aiohttp.ClientTimeout(total=self.timeout) + self.client = aiohttp.ClientSession( + headers=headers, + timeout=timeout, + ) + return self.client + + def _build_payload(self, text: list[str]) -> dict: + payload = { + "model": self.model, + "input": text, + } + if "embedding_dimensions" in self.provider_config: + try: + dimensions = int(self.provider_config["embedding_dimensions"]) + if dimensions > 0: + payload["dimensions"] = dimensions + except (ValueError, TypeError): + pass + return payload + + async def get_embedding(self, text: str) -> list[float]: + embeddings = await self.get_embeddings([text]) + return embeddings[0] if embeddings else [] + + async def get_embeddings(self, text: list[str]) -> list[list[float]]: + client = await self._get_client() + if not client or client.closed: + raise Exception("[Ollama Embedding] Client session not initialized") + + payload = self._build_payload(text) + request_url = f"{self.base_url}/api/embed" + + try: + async with client.post( + request_url, + json=payload, + proxy=self.proxy or None, + ) as response: + if response.status != 200: + error_text = await response.text() + logger.error( + f"[Ollama Embedding] API Error: {response.status} - {error_text}", + ) + raise Exception( + f"Ollama Embedding API request failed: HTTP {response.status} - {error_text}", + ) + + response_data = await response.json() + embeddings = response_data.get("embeddings", []) + + if not embeddings: + raise Exception( + f"[Ollama Embedding] No embeddings returned: {response_data}", + ) + + return embeddings + + except aiohttp.ClientError as e: + logger.error(f"[Ollama Embedding] Network error: {e}") + raise + except Exception as e: + logger.error(f"[Ollama Embedding] Error: {e}", exc_info=True) + raise + + def get_dim(self) -> int: + if "embedding_dimensions" in self.provider_config: + try: + return int(self.provider_config["embedding_dimensions"]) + except (ValueError, TypeError): + logger.warning( + f"embedding_dimensions in embedding configs is not a valid integer: " + f"'{self.provider_config['embedding_dimensions']}', ignored.", + ) + return 0 + + async def terminate(self): + if self.client and not self.client.closed: + await self.client.close() + self.client = None diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 5ba40bd76..312ae962b 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -551,7 +551,7 @@ class ProviderOpenAIOfficial(Provider): if not isinstance(msg, dict): cleaned.append(msg) continue - msg = cast(dict[str, Any], msg) + msg = cast("dict[str, Any]", msg) if msg.get("role") != "assistant": cleaned.append(msg) continue @@ -913,6 +913,9 @@ class ProviderOpenAIOfficial(Provider): args = {} else: args = func.arguments + # Some API may return None for tools with no parameters + if args is None: + args = {} args_ls.append(args) func_name_ls.append(func.name) tool_call_ids.append(tool_call.id) @@ -1026,7 +1029,7 @@ class ProviderOpenAIOfficial(Provider): if part.get("type") == "think": reasoning_content_present = True reasoning_content = (reasoning_content or "") + str( - part.get("think") + part.get("think"), ) else: new_content.append(part) diff --git a/astrbot/core/provider/sources/sensevoice_selfhosted_source.py b/astrbot/core/provider/sources/sensevoice_selfhosted_source.py index 1c3c6cc82..e6476b266 100644 --- a/astrbot/core/provider/sources/sensevoice_selfhosted_source.py +++ b/astrbot/core/provider/sources/sensevoice_selfhosted_source.py @@ -56,7 +56,7 @@ class ProviderSenseVoiceSTTSelfHost(STTProvider): self.model = await asyncio.get_running_loop().run_in_executor( None, lambda: cast( - SenseVoiceModel, + "SenseVoiceModel", SenseVoiceSmall(self.model_name, quantize=True, batch_size=16), ), ) diff --git a/astrbot/core/skills/skill_manager.py b/astrbot/core/skills/skill_manager.py index a44b3ce53..a14563cfe 100644 --- a/astrbot/core/skills/skill_manager.py +++ b/astrbot/core/skills/skill_manager.py @@ -345,7 +345,7 @@ class SkillManager: data_path = Path(get_astrbot_data_path()) self.config_path = str(data_path / SKILLS_CONFIG_FILENAME) self.sandbox_skills_cache_path = str( - data_path / SANDBOX_SKILLS_CACHE_FILENAME + data_path / SANDBOX_SKILLS_CACHE_FILENAME, ) os.makedirs(self.skills_root, exist_ok=True) @@ -441,7 +441,8 @@ class SkillManager: continue description = str(item.get("description", "") or "") path = _normalize_cached_sandbox_skill_path( - name, str(item.get("path", "") or "") + name, + str(item.get("path", "") or ""), ) deduped[name] = { "name": name, @@ -491,7 +492,8 @@ class SkillManager: continue name = str(item.get("name", "") or "").strip() path = _normalize_cached_sandbox_skill_path( - name, str(item.get("path", "") or "") + name, + str(item.get("path", "") or ""), ) if not name or not _SKILL_NAME_RE.match(name): continue @@ -534,7 +536,7 @@ class SkillManager: source_label = "synced" if sandbox_exists else "local" if runtime == "sandbox" and show_sandbox_path: path_str = sandbox_cached_paths.get( - skill_name + skill_name, ) or _default_sandbox_skill_path(skill_name) else: path_str = str(skill_md) @@ -575,7 +577,7 @@ class SkillManager: ) if runtime == "sandbox" and show_sandbox_path: path_str = sandbox_cached_paths.get( - skill_name + skill_name, ) or _default_sandbox_skill_path(skill_name) else: path_str = str(skill_md) @@ -615,7 +617,7 @@ class SkillManager: # since there is no local path to show. Always prefer the # actual path from sandbox cache. path_str = sandbox_cached_paths.get( - skill_name + skill_name, ) or _default_sandbox_skill_path(skill_name) skills_by_name[skill_name] = SkillInfo( name=skill_name, @@ -656,7 +658,7 @@ class SkillManager: def set_skill_active(self, name: str, active: bool) -> None: if self.is_sandbox_only_skill(name): raise PermissionError( - "Sandbox preset skill cannot be enabled/disabled from local skill management." + "Sandbox preset skill cannot be enabled/disabled from local skill management.", ) config = self._load_config() config.setdefault("skills", {}) @@ -684,11 +686,11 @@ class SkillManager: def delete_skill(self, name: str) -> None: if self.is_sandbox_only_skill(name): raise PermissionError( - "Sandbox preset skill cannot be deleted from local skill management." + "Sandbox preset skill cannot be deleted from local skill management.", ) if self.is_plugin_skill(name): raise PermissionError( - "Plugin-provided skill cannot be deleted from local skill management." + "Plugin-provided skill cannot be deleted from local skill management.", ) skill_dir = Path(self.skills_root) / name @@ -740,7 +742,7 @@ class SkillManager: if skill_name_hint is not None: archive_skill_name = _normalize_skill_name(skill_name_hint) if archive_skill_name and not _SKILL_NAME_RE.fullmatch( - archive_skill_name + archive_skill_name, ): raise ValueError("Invalid skill name.") @@ -765,7 +767,7 @@ class SkillManager: candidate_name = _normalize_skill_name(src_dir_name) if not candidate_name or not _SKILL_NAME_RE.fullmatch( - candidate_name + candidate_name, ): continue @@ -782,7 +784,7 @@ class SkillManager: raise FileExistsError( "One or more skills from the archive already exist and " "overwrite=False. No skills were installed. Conflicting " - f"paths: {', '.join(conflict_dirs)}" + f"paths: {', '.join(conflict_dirs)}", ) with tempfile.TemporaryDirectory(dir=get_astrbot_temp_path()) as tmp_dir: @@ -794,7 +796,7 @@ class SkillManager: if root_mode: archive_hint = _normalize_skill_name( - archive_skill_name or zip_path_obj.stem + archive_skill_name or zip_path_obj.stem, ) if not archive_hint or not _SKILL_NAME_RE.fullmatch(archive_hint): raise ValueError("Invalid skill name.") @@ -804,7 +806,7 @@ class SkillManager: normalized_path = _normalize_skill_markdown_path(src_dir) if normalized_path is None: raise ValueError( - "SKILL.md not found in the root of the zip archive." + "SKILL.md not found in the root of the zip archive.", ) dest_dir = Path(self.skills_root) / skill_name @@ -824,7 +826,7 @@ class SkillManager: for archive_root_name in top_dirs: archive_root_name_normalized = _normalize_skill_name( - archive_root_name + archive_root_name, ) if ( @@ -852,7 +854,7 @@ class SkillManager: if dest_dir.exists(): if not overwrite: raise FileExistsError( - f"Skill {skill_name} already exists." + f"Skill {skill_name} already exists.", ) shutil.rmtree(dest_dir) @@ -862,7 +864,7 @@ class SkillManager: if not installed_skills: raise ValueError( - "No valid SKILL.md found in any folder of the zip archive." + "No valid SKILL.md found in any folder of the zip archive.", ) return ", ".join(installed_skills) diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index cf18610cf..00fa4a374 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -134,6 +134,7 @@ class Context: Raises: ChatProviderNotFoundError: If the specified chat provider ID is not found Exception: For other errors during LLM generation + """ prov = await self.provider_manager.get_provider_by_id(chat_provider_id) if not prov or not isinstance(prov, Provider): @@ -191,6 +192,7 @@ class Context: Raises: ChatProviderNotFoundError: If the specified chat provider ID is not found Exception: For other errors during LLM generation + """ # Import here to avoid circular imports from astrbot.core.astr_agent_context import ( @@ -238,10 +240,12 @@ class Context: } if request.func_tool and request.func_tool.get_tool("astrbot_file_read_tool"): other_kwargs.setdefault( - "tool_result_overflow_dir", get_astrbot_system_tmp_path() + "tool_result_overflow_dir", + get_astrbot_system_tmp_path(), ) other_kwargs.setdefault( - "read_tool", request.func_tool.get_tool("astrbot_file_read_tool") + "read_tool", + request.func_tool.get_tool("astrbot_file_read_tool"), ) await agent_runner.reset( @@ -274,6 +278,7 @@ class Context: Raises: ProviderNotFoundError: 未找到。 + """ prov = self.get_using_provider(umo) if not prov: @@ -305,6 +310,7 @@ class Context: Note: 注册的工具默认是激活状态。 + """ return self.provider_manager.llm_tools.activate_llm_tool(name, star_map) @@ -316,6 +322,7 @@ class Context: Returns: 如果成功停用返回 True,如果没找到工具返回 False。 + """ return self.provider_manager.llm_tools.deactivate_llm_tool(name) @@ -335,11 +342,12 @@ class Context: Note: 如果提供者 ID 存在但未找到提供者,会记录警告日志。 + """ prov = self.provider_manager.inst_map.get(provider_id) if provider_id and not prov: logger.warning( - f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。" + f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。", ) return prov @@ -371,6 +379,7 @@ class Context: Raises: ValueError: 该会话来源配置的的对话模型(提供商)的类型不正确。 + """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.CHAT_COMPLETION, @@ -380,7 +389,7 @@ class Context: return None if not isinstance(prov, Provider): raise ValueError( - f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}" + f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}", ) return prov @@ -395,6 +404,7 @@ class Context: Raises: ValueError: 返回的提供者不是 TTSProvider 类型。 + """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.TEXT_TO_SPEECH, @@ -415,6 +425,7 @@ class Context: Raises: ValueError: 返回的提供者不是 STTProvider 类型。 + """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.SPEECH_TO_TEXT, @@ -435,6 +446,7 @@ class Context: Note: 如果不提供 umo 参数,将返回默认配置。 + """ if not umo: # 使用默认配置 @@ -461,6 +473,7 @@ class Context: Note: 当 session 为字符串时,会尝试解析为 MessageSession 对象。(类名为MessageSesion是因为历史遗留拼写错误) qq_official(QQ 官方 API 平台) 不支持此方法。 + """ if isinstance(session, str): try: @@ -473,7 +486,7 @@ class Context: await platform.send_by_session(session, message_chain) return True logger.warning( - f"cannot find platform for session {str(session)}, message not sent" + f"cannot find platform for session {session!s}, message not sent", ) return False @@ -485,6 +498,7 @@ class Context: Note: 如果工具已存在,会替换已存在的工具。 + """ tool_name = {tool.name for tool in self.provider_manager.llm_tools.func_list} module_path = "" @@ -504,7 +518,7 @@ class Context: else: tool.handler_module_path = module_path logger.info( - f"plugin(module_path {module_path}) added LLM tool: {tool.name}" + f"plugin(module_path {module_path}) added LLM tool: {tool.name}", ) if tool.name in tool_name: @@ -529,6 +543,7 @@ class Context: Note: 如果相同路由和方法已注册,会替换现有的 API。 + """ for idx, api in enumerate(self._registered_web_apis): if api[0] == route and methods == api[2]: @@ -556,6 +571,7 @@ class Context: Note: 该方法已经过时,请使用 get_platform_inst 方法。(>= AstrBot v4.0.0) + """ for platform in self.platform_manager.platform_insts: name = platform.meta().name @@ -579,6 +595,7 @@ class Context: Note: 可以通过 event.get_platform_id() 获取平台 ID。 + """ for platform in self.platform_manager.platform_insts: if platform.meta().id == platform_id: @@ -589,6 +606,7 @@ class Context: Returns: 数据库实例。 + """ return self._db @@ -597,6 +615,7 @@ class Context: Args: provider: 提供者实例。 + """ self.provider_manager.provider_insts.append(provider) @@ -619,6 +638,7 @@ class Context: Note: 异步处理函数会接收到额外的关键词参数:event: AstrMessageEvent, context: Context。 该方法已弃用,请使用新的注册方式。 + """ md = StarHandlerMetadata( event_type=EventType.OnLLMRequestEvent, @@ -641,6 +661,7 @@ class Context: Note: 如果再要启用,需要重新注册。 该方法已弃用。 + """ self.provider_manager.llm_tools.remove_func(name) @@ -667,6 +688,7 @@ class Context: Note: 推荐使用装饰器注册指令。该方法将在未来的版本中被移除。 + """ md = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, @@ -694,5 +716,6 @@ class Context: Note: 该方法已弃用。 + """ self._register_tasks.append(task) diff --git a/astrbot/core/star/register/star_handler.py b/astrbot/core/star/register/star_handler.py index dfab95025..30825e404 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -283,7 +283,11 @@ def register_platform_adapter_type( """注册一个 PlatformAdapterType""" def decorator(awaitable): - handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent) + handler_md = get_handler_or_create( + awaitable, + EventType.AdapterMessageEvent, + **kwargs, + ) handler_md.event_filters.append( PlatformAdapterTypeFilter(platform_adapter_type), ) @@ -307,7 +311,11 @@ def register_regex(regex: str | re.Pattern, **kwargs): return decorator -def register_permission_type(permission_type: PermissionType, raise_error: bool = True): +def register_permission_type( + permission_type: PermissionType, + raise_error: bool = True, + **kwargs, +): """注册一个 PermissionType Args: @@ -317,7 +325,11 @@ def register_permission_type(permission_type: PermissionType, raise_error: bool """ def decorator(awaitable): - handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent) + handler_md = get_handler_or_create( + awaitable, + EventType.AdapterMessageEvent, + **kwargs, + ) handler_md.event_filters.append( PermissionTypeFilter(permission_type, raise_error), ) @@ -432,11 +444,11 @@ def register_on_llm_request(**kwargs): from astrbot.api.provider import ProviderRequest @on_llm_request() - async def test(self, event: AstrMessageEvent, request: ProviderRequest) -> None: - request.system_prompt += "你是一个猫娘..." + async def test(self, event: AstrMessageEvent, req: ProviderRequest) -> None: + req.system_prompt += "你是一个猫娘..." ``` - 请务必接收两个参数:event, request + 请务必接收两个参数:event, req """ diff --git a/astrbot/core/star/session_plugin_manager.py b/astrbot/core/star/session_plugin_manager.py index ef10b6f89..c1f2168c2 100644 --- a/astrbot/core/star/session_plugin_manager.py +++ b/astrbot/core/star/session_plugin_manager.py @@ -18,7 +18,7 @@ def _normalize_session_plugin_config(value: object) -> dict[str, dict[str, list[ for session_id, raw_settings in value.items(): if not isinstance(session_id, str) or not isinstance(raw_settings, dict): continue - raw_settings = cast(dict[str, Any], raw_settings) + raw_settings = cast("dict[str, Any]", raw_settings) settings: dict[str, list[str]] = {} enabled_plugins = raw_settings.get("enabled_plugins") if isinstance(enabled_plugins, list) and all( diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 41f283cef..0ea6d19bb 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -139,7 +139,7 @@ async def _install_requirements_with_precheck( if install_plan is None: logger.info( f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): " - f"{requirements_path}" + f"{requirements_path}", ) await pip_installer.install(requirements_path=requirements_path) return @@ -164,7 +164,7 @@ async def _install_requirements_with_precheck( logger.info( f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: " - f"{requirements_path} -> {sorted(install_plan.missing_names)}" + f"{requirements_path} -> {sorted(install_plan.missing_names)}", ) with _temporary_filtered_requirements_file( @@ -190,7 +190,9 @@ class PluginManager: self.plugin_config_path = get_astrbot_config_path() """存储插件配置的路径。data/config""" self.reserved_plugin_path = os.path.join( - get_astrbot_path(), "astrbot", "builtin_stars" + get_astrbot_path(), + "astrbot", + "builtin_stars", ) """保留插件的路径。在 astrbot/builtin_stars 目录下""" self.conf_schema_fname = "_conf_schema.json" @@ -308,7 +310,8 @@ class PluginManager: return plugins async def _check_plugin_dept_update( - self, target_plugin: str | None = None + self, + target_plugin: str | None = None, ) -> bool | None: """检查插件的依赖 如果 target_plugin 为 None,则检查所有插件的依赖 @@ -367,7 +370,7 @@ class PluginManager: install_plan = plan_missing_requirements_install(requirements_path) if install_plan is None: return ImportDependencyRecoveryState( - ImportDependencyRecoveryMode.RECOVER_ON_FAILURE + ImportDependencyRecoveryMode.RECOVER_ON_FAILURE, ) if install_plan.version_mismatch_names: return ImportDependencyRecoveryState( @@ -390,19 +393,19 @@ class PluginManager: ) -> ModuleType | None: try: logger.info( - f"插件 {root_dir_name} 导入失败,尝试从已安装依赖恢复: {import_exc!s}" + f"插件 {root_dir_name} 导入失败,尝试从已安装依赖恢复: {import_exc!s}", ) pip_installer.prefer_installed_dependencies( - requirements_path=requirements_path + requirements_path=requirements_path, ) module = __import__(path, fromlist=[module_str]) logger.info( - f"插件 {root_dir_name} 已从 site-packages 恢复依赖,跳过重新安装。" + f"插件 {root_dir_name} 已从 site-packages 恢复依赖,跳过重新安装。", ) return module except (ImportError, ModuleNotFoundError) as recover_exc: logger.info( - f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}" + f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}", ) return None @@ -423,11 +426,11 @@ class PluginManager: if recovery_state.mode is ImportDependencyRecoveryMode.PRELOAD_AND_RECOVER: try: pip_installer.prefer_installed_dependencies( - requirements_path=requirements_path + requirements_path=requirements_path, ) except Exception as preload_exc: logger.info( - f"插件 {root_dir_name} 预加载已安装依赖失败,将继续常规导入: {preload_exc!s}" + f"插件 {root_dir_name} 预加载已安装依赖失败,将继续常规导入: {preload_exc!s}", ) try: @@ -573,11 +576,11 @@ class PluginManager: def _validate_importable_name(plugin_name: str) -> None: if "/" in plugin_name or "\\" in plugin_name: raise ValueError( - "metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。" + "metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。", ) if not plugin_name.isidentifier() or keyword.iskeyword(plugin_name): raise Exception( - "metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。" + "metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。", ) @staticmethod @@ -750,7 +753,7 @@ class PluginManager: "display_name": metadata.display_name, "support_platforms": metadata.support_platforms, "astrbot_version": metadata.astrbot_version, - } + }, ) except Exception as metadata_error: logger.debug( @@ -785,8 +788,7 @@ class PluginManager: self.failed_plugin_info = "\n".join(lines) + "\n" async def reload_failed_plugin(self, dir_name): - """ - 重新加载未注册(加载失败)的插件 + """重新加载未注册(加载失败)的插件 Args: dir_name (str): 要重载的特定插件名称。 Returns: @@ -794,7 +796,6 @@ class PluginManager: - success (bool): 重载是否成功 - error_message (str|None): 错误信息,成功时为 None """ - async with self._pm_lock: if dir_name not in self.failed_plugin_dict: return False, "插件不存在于失败列表中" @@ -809,8 +810,7 @@ class PluginManager: self.failed_plugin_dict.pop(dir_name, None) self._rebuild_failed_plugin_info() return success, None - else: - return False, error + return False, error async def reload(self, specified_plugin_name=None): """重新加载插件 @@ -1008,7 +1008,7 @@ class PluginManager: if not is_valid: raise PluginVersionIncompatibleError( error_message - or "The plugin is not compatible with the current AstrBot version." + or "The plugin is not compatible with the current AstrBot version.", ) logger.info(metadata) @@ -1131,7 +1131,7 @@ class PluginManager: if not is_valid: raise PluginVersionIncompatibleError( error_message - or "The plugin is not compatible with the current AstrBot version." + or "The plugin is not compatible with the current AstrBot version.", ) metadata.star_cls = obj @@ -1397,7 +1397,7 @@ class PluginManager: plugin_path = os.path.join(self.plugin_store_path, repo_name) if os.path.exists(plugin_path): raise Exception( - f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。" + f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。", ) if download_url: plugin_path = await self.updator.install( @@ -1416,7 +1416,7 @@ class PluginManager: metadata_dir_name, ) if target_plugin_path != plugin_path and os.path.exists( - target_plugin_path + target_plugin_path, ): raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") if target_plugin_path != plugin_path: @@ -1434,7 +1434,7 @@ class PluginManager: if not success: raise Exception( error_message - or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" + or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。", ) # Get the plugin metadata to return repo info @@ -1642,7 +1642,7 @@ class PluginManager: module_prefix = ".".join(plugin_module_path.split(".")[:-1]) if module_prefix: unregistered_adapters = unregister_platform_adapters_by_module( - module_prefix + module_prefix, ) for adapter_name in unregistered_adapters: logger.info( @@ -1658,7 +1658,10 @@ class PluginManager: ) async def update_plugin( - self, plugin_name: str, proxy="", download_url: str = "" + self, + plugin_name: str, + proxy="", + download_url: str = "", ) -> None: """升级一个插件""" plugin = self.context.get_registered_star(plugin_name) @@ -1791,13 +1794,17 @@ class PluginManager: await self.reload(plugin_name) async def install_plugin_from_file( - self, zip_file_path: str, ignore_version_check: bool = False + self, + zip_file_path: str, + ignore_version_check: bool = False, ): dir_name = os.path.splitext(os.path.basename(zip_file_path))[0] desti_dir = tempfile.mkdtemp( - dir=self.plugin_store_path, prefix="plugin_upload_" + dir=self.plugin_store_path, + prefix="plugin_upload_", ) temp_desti_dir = desti_dir + skip_failed_tracking = False try: self.updator.unzip_file(zip_file_path, desti_dir) @@ -1807,6 +1814,7 @@ class PluginManager: metadata_dir_name, ) if target_plugin_path != desti_dir and os.path.exists(target_plugin_path): + skip_failed_tracking = True raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") if target_plugin_path != desti_dir: os.rename(desti_dir, target_plugin_path) @@ -1827,7 +1835,7 @@ class PluginManager: if not success: raise Exception( error_message - or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" + or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。", ) # Get the plugin metadata to return repo info @@ -1870,17 +1878,20 @@ class PluginManager: return plugin_info except Exception as e: - self._track_failed_install_dir( - dir_name=dir_name, - plugin_path=desti_dir, - error=e, - ) + if not skip_failed_tracking: + self._track_failed_install_dir( + dir_name=dir_name, + plugin_path=desti_dir, + error=e, + ) logger.warning( f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}", ) raise finally: - if temp_desti_dir != desti_dir and os.path.isdir(temp_desti_dir): + if (skip_failed_tracking or temp_desti_dir != desti_dir) and os.path.isdir( + temp_desti_dir, + ): try: remove_dir(temp_desti_dir) except Exception as e: diff --git a/astrbot/core/star/updator.py b/astrbot/core/star/updator.py index 3663f6a5a..7fe16b234 100644 --- a/astrbot/core/star/updator.py +++ b/astrbot/core/star/updator.py @@ -31,18 +31,21 @@ class PluginUpdator(RepoZipUpdator): return plugin_path async def update( - self, plugin: StarMetadata, proxy="", download_url: str = "" + self, + plugin: StarMetadata, + proxy="", + download_url: str = "", ) -> str: repo_url = plugin.repo if not repo_url and not download_url: raise Exception( - f"Plugin {plugin.name} does not specify a repository URL or download URL." + f"Plugin {plugin.name} does not specify a repository URL or download URL.", ) if not plugin.root_dir_name: raise Exception( - f"Plugin {plugin.name} does not specify a root directory name." + f"Plugin {plugin.name} does not specify a root directory name.", ) plugin_path = os.path.join(self.plugin_store_path, plugin.root_dir_name) @@ -52,7 +55,7 @@ class PluginUpdator(RepoZipUpdator): ) if download_url: logger.info( - f"Downloading plugin update archive for {plugin.name}: {download_url}" + f"Downloading plugin update archive for {plugin.name}: {download_url}", ) await self._download_file(download_url, plugin_path + ".zip") else: diff --git a/astrbot/core/tools/computer_tools/cua.py b/astrbot/core/tools/computer_tools/cua.py index c6ca2a2d5..cddd29dbd 100644 --- a/astrbot/core/tools/computer_tools/cua.py +++ b/astrbot/core/tools/computer_tools/cua.py @@ -42,7 +42,7 @@ async def _get_gui_component(context: ContextWrapper[AstrAgentContext]) -> Any: if gui is None: raise RuntimeError( "Current sandbox booter does not support CUA GUI capability. " - "Please switch sandbox booter to cua." + "Please switch sandbox booter to cua.", ) return gui @@ -69,7 +69,7 @@ class CuaScreenshotTool(FunctionTool): "default": True, }, }, - } + }, ) async def call( @@ -91,7 +91,7 @@ class CuaScreenshotTool(FunctionTool): payload["sent_to_user"] = True image_data = payload.pop("base64", "") content: list[ContentBlock] = [ - mcp.types.TextContent(type="text", text=_to_json(payload)) + mcp.types.TextContent(type="text", text=_to_json(payload)), ] if return_image_to_llm: content.append( @@ -99,7 +99,7 @@ class CuaScreenshotTool(FunctionTool): type="image", data=str(image_data), mimeType=str(payload.get("mime_type", "image/png")), - ) + ), ) return mcp.types.CallToolResult(content=content) except Exception as e: @@ -124,7 +124,7 @@ class CuaMouseClickTool(FunctionTool): }, }, "required": ["x", "y"], - } + }, ) async def call( @@ -156,7 +156,7 @@ class CuaKeyboardTypeTool(FunctionTool): "text": {"type": "string", "description": "Text to type."}, }, "required": ["text"], - } + }, ) async def call( diff --git a/astrbot/core/tools/computer_tools/fs.py b/astrbot/core/tools/computer_tools/fs.py index 3f32f59ba..a68cd4b36 100644 --- a/astrbot/core/tools/computer_tools/fs.py +++ b/astrbot/core/tools/computer_tools/fs.py @@ -83,7 +83,7 @@ def _restricted_env_path_labels(umo: str, *, include_plugin_skills: bool) -> lis f"data/workspaces/{normalized_umo}", get_astrbot_system_tmp_path(), get_astrbot_temp_path(), - ] + ], ) return labels @@ -195,12 +195,12 @@ def _normalize_rw_path( allowed_roots=allowed_roots, ): allowed = ", ".join( - _restricted_env_path_labels(umo, include_plugin_skills=not write) + _restricted_env_path_labels(umo, include_plugin_skills=not write), ) access = "Write" if write else "Read" raise PermissionError( f"{access} access is restricted for this user. " - f"Allowed directories: {allowed}. Blocked path: {normalized_path}." + f"Allowed directories: {allowed}. Blocked path: {normalized_path}.", ) return normalized_path @@ -589,7 +589,7 @@ class GrepTool(FunctionTool): ] if disallowed: allowed = ", ".join( - _restricted_env_path_labels(umo, include_plugin_skills=True) + _restricted_env_path_labels(umo, include_plugin_skills=True), ) blocked = ", ".join(disallowed) raise PermissionError( @@ -793,7 +793,7 @@ class FileDownloadTool(FunctionTool): message_component = File(name=name, file=local_path) sent_as = "file" await context.context.event.send( - MessageChain(chain=[message_component]) + MessageChain(chain=[message_component]), ) except Exception as e: logger.error(f"Error sending file message: {e}") diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index 0604ff54a..60890c730 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -113,7 +113,10 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): self, context: ContextWrapper[AstrAgentContext], path: str, + *, + component_type: str = "file", ) -> tuple[str, bool]: + path = str(path) # if the path is relative, check if the file exists in user's local workspace if not os.path.isabs(path): unified_msg_origin = context.context.event.unified_msg_origin @@ -149,8 +152,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): return local_path, True except Exception as exc: logger.warning(f"Failed to check/download file from sandbox: {exc}") + raise - return path, False + raise FileNotFoundError(f"{component_type} path does not exist: {path}") async def call( self, @@ -164,7 +168,8 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): session = kwargs.get("session") or current_session if session != current_session: if permission_error := check_admin_permission( - context, "Send message to another session" + context, + "Send message to another session", ): return permission_error messages = kwargs.get("messages") @@ -194,6 +199,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): local_path, _ = await self._resolve_path_from_sandbox( context, path, + component_type="image", ) components.append(Comp.Image.fromFileSystem(path=local_path)) elif url: @@ -207,6 +213,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): local_path, _ = await self._resolve_path_from_sandbox( context, path, + component_type="record", ) components.append(Comp.Record.fromFileSystem(path=local_path)) elif url: @@ -220,6 +227,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): local_path, _ = await self._resolve_path_from_sandbox( context, path, + component_type="video", ) components.append(Comp.Video.fromFileSystem(path=local_path)) elif url: @@ -239,6 +247,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): local_path, _ = await self._resolve_path_from_sandbox( context, path, + component_type="file", ) components.append(Comp.File(name=name, file=local_path)) elif url: @@ -254,6 +263,8 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): return ( f"error: unsupported message type '{msg_type}' at index {idx}." ) + except FileNotFoundError as exc: + return f"error: {exc}" except Exception as exc: return f"error: failed to build messages[{idx}] component: {exc}" diff --git a/astrbot/core/tools/web_search_tools.py b/astrbot/core/tools/web_search_tools.py index e9d4fedba..fd462c260 100644 --- a/astrbot/core/tools/web_search_tools.py +++ b/astrbot/core/tools/web_search_tools.py @@ -283,35 +283,37 @@ async def _firecrawl_search( "Authorization": f"Bearer {firecrawl_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( + async with ( + aiohttp.ClientSession(trust_env=True) as session, + session.post( "https://api.firecrawl.dev/v2/search", json=payload, headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Firecrawl web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - rows = data.get("data", []) - if isinstance(rows, dict): - rows = rows.get("web", []) - return [ - SearchResult( - title=item.get("title", ""), - url=item.get("url", ""), - snippet=( - item.get("description") - or item.get("snippet") - or item.get("markdown") - or "" - ), - ) - for item in rows - if item.get("url") - ] + ) as response, + ): + if response.status != 200: + reason = await response.text() + raise Exception( + f"Firecrawl web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + rows = data.get("data", []) + if isinstance(rows, dict): + rows = rows.get("web", []) + return [ + SearchResult( + title=item.get("title", ""), + url=item.get("url", ""), + snippet=( + item.get("description") + or item.get("snippet") + or item.get("markdown") + or "" + ), + ) + for item in rows + if item.get("url") + ] async def _firecrawl_scrape(provider_settings: dict, payload: dict) -> dict: @@ -320,24 +322,26 @@ async def _firecrawl_scrape(provider_settings: dict, payload: dict) -> dict: "Authorization": f"Bearer {firecrawl_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( + async with ( + aiohttp.ClientSession(trust_env=True) as session, + session.post( "https://api.firecrawl.dev/v2/scrape", json=payload, headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Firecrawl web scraper failed: {reason}, status: {response.status}", - ) - data = await response.json() - result = data.get("data", {}) - if not result: - raise ValueError( - "Error: Firecrawl web scraper does not return any results." - ) - return result + ) as response, + ): + if response.status != 200: + reason = await response.text() + raise Exception( + f"Firecrawl web scraper failed: {reason}, status: {response.status}", + ) + data = await response.json() + result = data.get("data", {}) + if not result: + raise ValueError( + "Error: Firecrawl web scraper does not return any results.", + ) + return result async def _baidu_search( @@ -661,7 +665,7 @@ class FirecrawlWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -715,7 +719,7 @@ class FirecrawlExtractWebPageTool(FunctionTool[AstrAgentContext]): }, }, "required": ["url"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index c42ed9f13..52327ecdd 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -143,12 +143,13 @@ class AstrBotUpdator(RepoZipUpdator): async def get_releases(self) -> list: return await self.fetch_release_info(self.ASTRBOT_RELEASE_API) - async def update( # type: ignore[override] + async def update( self, reboot=False, latest=True, version=None, proxy="", + progress_callback=None, ) -> None: update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest) file_url = None @@ -181,7 +182,11 @@ class AstrBotUpdator(RepoZipUpdator): file_url = f"{proxy}/{file_url}" try: - await self._download_file(file_url, "temp.zip") + await self._download_file( + file_url, + "temp.zip", + progress_callback=progress_callback, + ) logger.info("下载 AstrBot Core 更新文件完成,正在执行解压...") self.unzip_file("temp.zip", self.MAIN_PATH) except BaseException as e: diff --git a/astrbot/core/utils/auth_password.py b/astrbot/core/utils/auth_password.py index db5a686c2..c80317165 100644 --- a/astrbot/core/utils/auth_password.py +++ b/astrbot/core/utils/auth_password.py @@ -4,6 +4,7 @@ import hashlib import hmac import re import secrets +import string from typing import Any try: @@ -20,9 +21,26 @@ _PBKDF2_ALGORITHM = "pbkdf2_sha256" _PBKDF2_FORMAT = f"{_PBKDF2_ALGORITHM}$" _LEGACY_MD5_LENGTH = 32 _DASHBOARD_PASSWORD_MIN_LENGTH = 12 +_GENERATED_DASHBOARD_PASSWORD_LENGTH = 24 DEFAULT_DASHBOARD_PASSWORD = "astrbot" +def generate_dashboard_password() -> str: + """Generate a strong dashboard password that satisfies the complexity policy.""" + alphabet = string.ascii_letters + string.digits + password_chars = [ + secrets.choice(string.ascii_uppercase), + secrets.choice(string.ascii_lowercase), + secrets.choice(string.digits), + *( + secrets.choice(alphabet) + for _ in range(_GENERATED_DASHBOARD_PASSWORD_LENGTH - 3) + ), + ] + secrets.SystemRandom().shuffle(password_chars) + return "".join(password_chars) + + def hash_dashboard_password(raw_password: str) -> str: """Return a salted hash for dashboard password using Argon2 (if available) or PBKDF2-HMAC-SHA256 fallback.""" if not isinstance(raw_password, str) or raw_password == "": @@ -44,6 +62,13 @@ def hash_dashboard_password(raw_password: str) -> str: return f"{_PBKDF2_FORMAT}{_PBKDF2_ITERATIONS}${salt}${digest}" +def hash_legacy_dashboard_password(raw_password: str) -> str: + """Return legacy MD5 hash for downgrade compatibility only.""" + if not isinstance(raw_password, str) or raw_password == "": + raise ValueError("Password cannot be empty") + return hashlib.md5(raw_password.encode("utf-8")).hexdigest() + + def validate_dashboard_password(raw_password: str) -> None: """Validate whether dashboard password meets the minimal complexity policy.""" if not isinstance(raw_password, str) or raw_password == "": diff --git a/astrbot/core/utils/core_constraints.py b/astrbot/core/utils/core_constraints.py index 60a0ae71b..6d0b1c8a5 100644 --- a/astrbot/core/utils/core_constraints.py +++ b/astrbot/core/utils/core_constraints.py @@ -106,8 +106,8 @@ class CoreConstraintsProvider: ( *_get_core_constraints(self._core_dist_name), *get_desktop_core_lock_constraints(), - ) - ) + ), + ), ) if not constraints: yield None @@ -151,8 +151,8 @@ class CoreConstraintsProvider: ( *_get_core_constraints(self._core_dist_name), *get_desktop_core_lock_constraints(), - ) - ) + ), + ), ) if not constraints: yield None diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index fa622db26..af09841b8 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -1,7 +1,9 @@ import asyncio import base64 +import inspect import logging import os +import re import shutil import socket import ssl @@ -18,6 +20,7 @@ import psutil from PIL import Image from .astrbot_path import get_astrbot_data_path, get_astrbot_path, get_astrbot_temp_path +from .version_comparator import VersionComparator logger = logging.getLogger("astrbot") @@ -157,7 +160,20 @@ async def download_image_by_url( raise e -async def download_file(url: str, path: str, show_progress: bool = False) -> None: +async def _emit_download_progress(progress_callback, payload: dict) -> None: + if not progress_callback: + return + result = progress_callback(payload) + if inspect.isawaitable(result): + await result + + +async def download_file( + url: str, + path: str, + show_progress: bool = False, + progress_callback=None, +) -> None: """从指定 url 下载文件到指定路径 path""" aiohttp = _get_aiohttp() try: @@ -175,13 +191,23 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non ) as resp: if resp.status != 200: logger.error( - f"Failed to download file from {url}. HTTP status code: {resp.status}" + f"Failed to download file from {url}. HTTP status code: {resp.status}", ) total_size = int(resp.headers.get("content-length", 0)) downloaded_size = 0 start_time = time.time() if show_progress: print(f"Downloading: {url} | Size: {total_size / 1024:.2f} KB") + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": 0, + "total": total_size, + "percent": 0, + "speed": 0, + }, + ) with open(path, "wb") as f: while True: chunk = await resp.content.read(8192) @@ -189,22 +215,43 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non break await f.write(chunk) downloaded_size += len(chunk) + elapsed_time = ( + time.time() - start_time + if time.time() - start_time > 0 + else 1 + ) + speed = downloaded_size / 1024 / elapsed_time # KB/s + percent = downloaded_size / total_size if total_size > 0 else 0 + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": percent, + "speed": speed, + }, + ) if show_progress: - elapsed_time = ( - time.time() - start_time - if time.time() - start_time > 0 - else 1 - ) - speed = downloaded_size / 1024 / elapsed_time # KB/s print( - f"\rProgress: {downloaded_size / total_size:.2%} Speed: {speed:.2f} KB/s", + f"\rProgress: {percent:.2%} Speed: {speed:.2f} KB/s", end="", ) + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": 1, + "speed": 0, + }, + ) except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): # 关闭SSL验证(仅在证书验证失败时作为fallback) logger.warning( f"SSL certificate verification failed for {url}. " - "Falling back to unverified connection (CERT_NONE). " + "Falling back to unverified connection (CERT_NONE). ", ) logger.warning( f"SSL certificate verification failed for {url}. " @@ -215,31 +262,66 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE - async with aiohttp.ClientSession() as session: - async with session.get( + async with ( + aiohttp.ClientSession() as session, + session.get( url, ssl=ssl_context, timeout=aiohttp.ClientTimeout(total=120), - ) as resp: - total_size = int(resp.headers.get("content-length", 0)) - downloaded_size = 0 - start_time = time.time() - if show_progress: - print(f"Size: {total_size / 1024:.2f} KB | URL: {url}") - with open(path, "wb") as f: - while True: - chunk = await resp.content.read(8192) - if not chunk: - break - await f.write(chunk) - downloaded_size += len(chunk) - if show_progress: - elapsed_time = time.time() - start_time - speed = downloaded_size / 1024 / elapsed_time # KB/s - print( - f"\rProgress: {downloaded_size / total_size:.2%} Speed: {speed:.2f} KB/s", - end="", - ) + ) as resp, + ): + total_size = int(resp.headers.get("content-length", 0)) + downloaded_size = 0 + start_time = time.time() + if show_progress: + print(f"Size: {total_size / 1024:.2f} KB | URL: {url}") + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": 0, + "total": total_size, + "percent": 0, + "speed": 0, + }, + ) + with open(path, "wb") as f: + while True: + chunk = await resp.content.read(8192) + if not chunk: + break + await f.write(chunk) + downloaded_size += len(chunk) + elapsed_time = ( + time.time() - start_time if time.time() - start_time > 0 else 1 + ) + speed = downloaded_size / 1024 / elapsed_time # KB/s + percent = downloaded_size / total_size if total_size > 0 else 0 + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": percent, + "speed": speed, + }, + ) + if show_progress: + print( + f"\rProgress: {percent:.2%} Speed: {speed:.2f} KB/s", + end="", + ) + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": 1, + "speed": 0, + }, + ) if show_progress: logger.info("下载完成") @@ -298,20 +380,68 @@ async def get_public_ip_address() -> list[IPv4Address | IPv6Address]: return list(found_ips.values()) +def _read_dashboard_dist_version(dist_dir: str | Path) -> str | None: + version_file = Path(dist_dir) / "assets" / "version" + if version_file.exists(): + return version_file.read_text(encoding="utf-8").strip() + return None + + +def get_bundled_dashboard_dist_path() -> Path: + return Path(get_astrbot_path()) / "astrbot" / "dashboard" / "dist" + + +def _normalize_dashboard_version(version: str) -> str: + version = version.strip() + if version[:1].lower() == "v": + version = version[1:] + if not re.match( + r"^[0-9]+(?:\.[0-9]+)*" + r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?" + r"(?:\+.+)?$", + version, + ): + raise ValueError(f"invalid dashboard version: {version!r}") + return version + + +def should_use_bundled_dashboard_dist( + user_dist: str | Path, + current_version: str, +) -> bool: + user_version = _read_dashboard_dist_version(user_dist) + bundled_dist = get_bundled_dashboard_dist_path() + if user_version is None or not bundled_dist.exists(): + return False + try: + return ( + VersionComparator.compare_version( + _normalize_dashboard_version(current_version), + _normalize_dashboard_version(user_version), + ) + > 0 + ) + except (TypeError, ValueError): + return False + + async def get_dashboard_version(): # First check user data directory (manually updated / downloaded dashboard). dist_dir = os.path.join(get_astrbot_data_path(), "dist") - if not await anyio.Path(dist_dir).exists(): - # Fall back to the dist bundled inside the installed wheel. - _bundled = Path(get_astrbot_path()) / "astrbot" / "dashboard" / "dist" - if _bundled.exists(): - dist_dir = str(_bundled) - if await anyio.Path(dist_dir).exists(): - version_file = os.path.join(dist_dir, "assets", "version") - if await anyio.Path(version_file).exists(): - async with await anyio.open_file(version_file, encoding="utf-8") as f: - v = (await f.read()).strip() - return v + if os.path.exists(dist_dir): + from astrbot.core.config.default import VERSION + + if should_use_bundled_dashboard_dist(dist_dir, VERSION): + bundled_version = _read_dashboard_dist_version( + get_bundled_dashboard_dist_path(), + ) + if bundled_version is not None: + return bundled_version + return _read_dashboard_dist_version(dist_dir) + + bundled = get_bundled_dashboard_dist_path() + if bundled.exists(): + return _read_dashboard_dist_version(bundled) return None @@ -321,6 +451,7 @@ async def download_dashboard( latest: bool = True, version: str | None = None, proxy: str | None = None, + progress_callback=None, ) -> None: """下载管理面板文件""" if path is None: @@ -339,23 +470,26 @@ async def download_dashboard( dashboard_release_url, str(zip_path), show_progress=True, + progress_callback=progress_callback, ) except BaseException as _: if latest: # Resolve latest release tag from GitHub API to construct correct asset URL ssl_context = ssl.create_default_context(cafile=certifi.where()) - async with aiohttp.ClientSession( - connector=aiohttp.TCPConnector(ssl=ssl_context), - trust_env=True, - ) as session: - async with session.get( + async with ( + aiohttp.ClientSession( + connector=aiohttp.TCPConnector(ssl=ssl_context), + trust_env=True, + ) as session, + session.get( "https://api.github.com/repos/AstrBotDevs/AstrBot/releases/latest", timeout=30, headers={"Accept": "application/vnd.github+json"}, - ) as api_resp: - api_resp.raise_for_status() - release_data = await api_resp.json() - tag = release_data["tag_name"] + ) as api_resp, + ): + api_resp.raise_for_status() + release_data = await api_resp.json() + tag = release_data["tag_name"] else: tag = version dashboard_release_url = f"https://github.com/AstrBotDevs/AstrBot/releases/download/{tag}/AstrBot-{tag}-dashboard.zip" @@ -365,12 +499,18 @@ async def download_dashboard( dashboard_release_url, str(zip_path), show_progress=True, + progress_callback=progress_callback, ) else: url = f"https://github.com/AstrBotDevs/astrbot-release-harbour/releases/download/release-{version}/dist.zip" logger.info(f"Downloading AstrBot WebUI from {url}") if proxy: url = f"{proxy}/{url}" - await download_file(url, str(zip_path), show_progress=True) + await download_file( + url, + str(zip_path), + show_progress=True, + progress_callback=progress_callback, + ) with zipfile.ZipFile(zip_path, "r") as z: z.extractall(extract_path) diff --git a/astrbot/core/utils/media_utils.py b/astrbot/core/utils/media_utils.py index 36fa7b221..2577285a8 100644 --- a/astrbot/core/utils/media_utils.py +++ b/astrbot/core/utils/media_utils.py @@ -17,6 +17,7 @@ from PIL import Image as PILImage from astrbot import logger from astrbot.core.utils.astrbot_path import get_astrbot_temp_path +from astrbot.core.utils.tencent_record_helper import tencent_silk_to_wav IMAGE_COMPRESS_DEFAULT_MAX_SIZE = 1280 IMAGE_COMPRESS_DEFAULT_QUALITY = 95 @@ -70,80 +71,12 @@ async def get_media_duration(file_path: str) -> int | None: async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) -> str: - """使用ffmpeg将音频转换为opus格式 - - Args: - audio_path: 原始音频文件路径 - output_path: 输出文件路径,如果为None则自动生成 - - Returns: - 转换后的opus文件路径 - - Raises: - Exception: 转换失败时抛出异常 - - """ - # 如果已经是opus格式,直接返回 - if audio_path.lower().endswith(".opus"): - return audio_path - - # 生成输出文件路径 - if output_path is None: - temp_dir = get_astrbot_temp_path() - await anyio.Path(temp_dir).mkdir(parents=True, exist_ok=True) - output_path = os.path.join(temp_dir, f"media_audio_{uuid.uuid4().hex}.opus") - - try: - # 使用ffmpeg转换为opus格式 - # -y: 覆盖输出文件 - # -i: 输入文件 - # -acodec libopus: 使用opus编码器 - # -ac 1: 单声道 - # -ar 16000: 采样率16kHz - process = await asyncio.create_subprocess_exec( - "ffmpeg", - "-y", - "-i", - audio_path, - "-acodec", - "libopus", - "-ac", - "1", - "-ar", - "16000", - output_path, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - _stdout, stderr = await process.communicate() - - if process.returncode != 0: - # 清理可能已生成但无效的临时文件 - if output_path and await anyio.Path(output_path).exists(): - try: - await anyio.Path(output_path).unlink() - logger.debug( - f"[Media Utils] 已清理失败的opus输出文件: {output_path}", - ) - except OSError as e: - logger.warning(f"[Media Utils] 清理失败的opus输出文件时出错: {e}") - - error_msg = stderr.decode() if stderr else "未知错误" - logger.error(f"[Media Utils] ffmpeg转换音频失败: {error_msg}") - raise Exception(f"ffmpeg conversion failed: {error_msg}") - - logger.debug(f"[Media Utils] 音频转换成功: {audio_path} -> {output_path}") - return output_path - - except FileNotFoundError: - logger.error( - "[Media Utils] ffmpeg未安装或不在PATH中,无法转换音频格式。请安装ffmpeg: https://ffmpeg.org/", - ) - raise Exception("ffmpeg not found") - except Exception as e: - logger.error(f"[Media Utils] 转换音频格式时出错: {e}") - raise + """将音频转换为opus格式。""" + return await convert_audio_format( + audio_path=audio_path, + output_format="opus", + output_path=output_path, + ) async def convert_video_format( @@ -252,7 +185,24 @@ async def convert_audio_format( args = ["ffmpeg", "-y", "-i", audio_path] if output_format == "amr": - args.extend(["-ac", "1", "-ar", "8000", "-ab", "12.2k"]) + args.extend( + [ + "-ac", + "1", + "-ar", + "8000", + "-ab", + "12.2k", + "-af", + ( + "highpass=f=310:poles=2," + "lowpass=f=3720:poles=2," + "equalizer=f=3150:width_type=h:width=1000:g=7.5," + "loudnorm=I=-18.5:TP=-1.5:LRA=6," + "aresample=8000" + ), + ], + ) elif output_format == "ogg" or output_format == "opus": args.extend(["-acodec", "libopus", "-ac", "1", "-ar", "16000"]) args.append(output_path) @@ -304,9 +254,17 @@ async def ensure_wav(audio_path: str, output_path: str | None = None) -> str: if not audio_path: return audio_path - if _get_audio_magic_type(audio_path) == "wav": + audio_type = _get_audio_magic_type(audio_path) + if audio_type == "wav": return audio_path + if audio_type == "silk": + if output_path is None: + temp_dir = get_astrbot_temp_path() + os.makedirs(temp_dir, exist_ok=True) + output_path = os.path.join(temp_dir, f"media_audio_{uuid.uuid4().hex}.wav") + return await tencent_silk_to_wav(audio_path, output_path) + return await convert_audio_to_wav(audio_path, output_path) @@ -345,7 +303,11 @@ def _get_audio_magic_type(audio_path: str) -> str: if header[:4] == b"ftyp" and b"mp4" in header[:8]: return "mp4" - if header[:8] == b"#!SILK_V3": + if header.startswith(b"#!SILK_V3"): + return "silk" + + # Tencent SILK: leading \x02 byte before #!SILK_V3 + if header.startswith(b"\x02#!SILK_V3"): return "silk" return "" @@ -463,7 +425,7 @@ async def compress_image( if not local_path.exists(): return url_or_path if local_path.stat().st_size < min_file_size_bytes and not _exceeds_max_size( - local_path + local_path, ): return url_or_path with local_path.open("rb") as f: diff --git a/astrbot/core/utils/metrics.py b/astrbot/core/utils/metrics.py index 6831eaca9..213a31852 100644 --- a/astrbot/core/utils/metrics.py +++ b/astrbot/core/utils/metrics.py @@ -81,7 +81,7 @@ class Metric: (key, Metric._format_group_value(value)) for key, value in kwargs.items() if key not in Metric._counter_fields - ) + ), ) @staticmethod diff --git a/astrbot/core/utils/network_utils.py b/astrbot/core/utils/network_utils.py index 4f4421b4c..8f53a2ab3 100644 --- a/astrbot/core/utils/network_utils.py +++ b/astrbot/core/utils/network_utils.py @@ -117,11 +117,14 @@ def create_proxy_client( Returns: An httpx.AsyncClient created with the hybrid SSL context (system store + certifi); the proxy is applied only if one is provided. + """ resolved_verify = _SYSTEM_SSL_CTX if verify is None else verify if proxy: logger.info(f"[{provider_label}] 使用代理: {proxy}") return httpx_module.AsyncClient( - proxy=proxy, verify=resolved_verify, headers=headers + proxy=proxy, + verify=resolved_verify, + headers=headers, ) return httpx_module.AsyncClient(verify=resolved_verify, headers=headers) diff --git a/astrbot/core/utils/t2i/template_manager.py b/astrbot/core/utils/t2i/template_manager.py index 72eb2216e..15dfe3040 100644 --- a/astrbot/core/utils/t2i/template_manager.py +++ b/astrbot/core/utils/t2i/template_manager.py @@ -1,11 +1,53 @@ # astrbot/core/utils/t2i/template_manager.py +import logging import os +import re import shutil from typing import ClassVar from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_path +logger = logging.getLogger("astrbot") + +_ALLOWED_VARS = frozenset({"text", "version", "shiki_runtime"}) + +_SSTI_BLACKLIST: list[tuple[str, re.Pattern]] = [ + ( + "dunder_chain", + re.compile( + r"__\s*(class|globals|init|mro|base|bases|subclasses|reduce|getitem|builtins|import|self|func|code|reduce_ex)__", + ), + ), + ( + "dangerous_builtins", + re.compile( + r"\b(import\s+(?!url)|os\.\w+|subprocess\.|\.popen\(|eval\(|exec\()", + ), + ), + ("flask_context", re.compile(r"\{\{.*?\b(config|request|session|g)\b.*?\}\}")), +] + +_VAR_RE = re.compile(r"\{\{\s*(\w+)\s*(\|[^}]*)?\}\}") + + +def validate_template_content(content: str, *, strict: bool = False) -> None: + for label, pattern in _SSTI_BLACKLIST: + if pattern.search(content): + logger.warning(f"SSTI validation blocked template: matched rule [{label}]") + raise ValueError(f"Template contains forbidden pattern ({label}).") + if strict: + for m in _VAR_RE.finditer(content): + var = m.group(1) + if var not in _ALLOWED_VARS: + logger.warning( + f"SSTI validation blocked template: unauthorized variable '{var}'", + ) + raise ValueError( + f"Unauthorized Jinja2 variable '{var}'; " + f"allowed: {', '.join(sorted(_ALLOWED_VARS))}.", + ) + class TemplateManager: """负责管理 t2i HTML 模板的 CRUD 和重置操作。 @@ -82,7 +124,8 @@ class TemplateManager: raise FileNotFoundError("模板不存在。") def create_template(self, name: str, content: str) -> None: - """在用户目录中创建一个新的模板文件。""" + """在用户目录中创建一个新的模板文件。""" + validate_template_content(content, strict=True) path = self._get_user_template_path(name) if os.path.exists(path): raise FileExistsError("同名模板已存在。") @@ -94,6 +137,7 @@ class TemplateManager: 如果更新的是一个内置模板,此操作实际上会在用户目录中创建一个修改后的副本, 从而实现对内置模板的“覆盖”。 """ + validate_template_content(content, strict=True) path = self._get_user_template_path(name) with open(path, "w", encoding="utf-8") as f: f.write(content) diff --git a/astrbot/core/utils/tencent_record_helper.py b/astrbot/core/utils/tencent_record_helper.py index 768fe2773..42c936ad0 100644 --- a/astrbot/core/utils/tencent_record_helper.py +++ b/astrbot/core/utils/tencent_record_helper.py @@ -5,7 +5,6 @@ import subprocess import tempfile import wave from io import BytesIO -from typing import TYPE_CHECKING import anyio import pysilk # requires silk-python (core dependency) @@ -13,9 +12,6 @@ import pysilk # requires silk-python (core dependency) from astrbot.core import logger from astrbot.core.utils.astrbot_path import get_astrbot_temp_path -if TYPE_CHECKING: - pass # pilk/p Moffmpeg are runtime optional deps - async def tencent_silk_to_wav(silk_path: str, output_path: str) -> str: async with await anyio.open_file(silk_path, "rb") as f: diff --git a/astrbot/core/zip_updator.py b/astrbot/core/zip_updator.py index 15096cc81..dc813542c 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -1,6 +1,8 @@ +import inspect import os import re import shutil +import time import zipfile from pathlib import Path from typing import NoReturn @@ -53,18 +55,64 @@ class RepoZipUpdator: return body[:max_len] + "...[truncated]" async def _download_file( - self, url: str, path: str, timeout: float = 1800.0 + self, + url: str, + path: str, + timeout: float = 1800.0, + progress_callback=None, ) -> None: target_path = Path(path) ensure_dir(target_path.parent) + async def _emit_progress(payload: dict) -> None: + if not progress_callback: + return + result = progress_callback(payload) + if inspect.isawaitable(result): + await result + try: async with self._create_httpx_client(timeout=timeout) as client: async with client.stream("GET", url) as response: response.raise_for_status() + headers = getattr(response, "headers", {}) + total_size = int(headers.get("content-length", 0)) + downloaded_size = 0 + start_time = time.time() + await _emit_progress( + { + "url": url, + "downloaded": 0, + "total": total_size, + "percent": 0, + "speed": 0, + }, + ) with target_path.open("wb") as file: async for chunk in response.aiter_bytes(8192): file.write(chunk) + downloaded_size += len(chunk) + elapsed_time = max(time.time() - start_time, 1) + await _emit_progress( + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": downloaded_size / total_size + if total_size > 0 + else 0, + "speed": downloaded_size / 1024 / elapsed_time, + }, + ) + await _emit_progress( + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": 1, + "speed": 0, + }, + ) except Exception as e: logger.error(f"下载文件失败: {url} -> {target_path}, 错误: {e}") if self.rm_on_error and target_path.exists(): @@ -175,7 +223,10 @@ class RepoZipUpdator: ) async def download_from_repo_url( - self, target_path: str, repo_url: str, proxy="" + self, + target_path: str, + repo_url: str, + proxy="", ) -> None: author, repo, branch = self.parse_github_url(repo_url) @@ -248,7 +299,9 @@ class RepoZipUpdator: root_candidates: list[str] = [] for raw_entry, normalized_entry, portable_entry in zip( - entries, normalized_entries, portable_entries + entries, + normalized_entries, + portable_entries, ): if normalized_entry == ".": continue @@ -314,7 +367,7 @@ class RepoZipUpdator: os.remove(zip_path) except Exception: logger.warning( - f"删除更新文件失败,可以手动删除 {zip_path} 和 {update_root_path}" + f"删除更新文件失败,可以手动删除 {zip_path} 和 {update_root_path}", ) def format_name(self, name: str) -> str: diff --git a/astrbot/dashboard/password_state.py b/astrbot/dashboard/password_state.py new file mode 100644 index 000000000..8fa64a7aa --- /dev/null +++ b/astrbot/dashboard/password_state.py @@ -0,0 +1,94 @@ +from astrbot.core.config.astrbot_config import AstrBotConfig +from astrbot.core.db import BaseDatabase +from astrbot.core.utils.auth_password import ( + hash_dashboard_password, + hash_legacy_dashboard_password, + is_legacy_dashboard_password, +) + +PASSWORD_STORAGE_UPGRADED_KEY = "password_storage_upgraded" +PASSWORD_CHANGE_REQUIRED_KEY = "password_change_required" + + +def _set_dashboard_flag(config: AstrBotConfig, key: str, value: bool) -> None: + if config["dashboard"].get(key) == bool(value): + return + config["dashboard"][key] = bool(value) + config.save_config() + + +def _has_usable_pbkdf2_password(config: AstrBotConfig) -> bool: + password = config["dashboard"].get("pbkdf2_password", "") + if not isinstance(password, str) or not password.startswith("pbkdf2_sha256$"): + return False + + parts = password.split("$") + if len(parts) != 4: + return False + + _, iterations, salt, digest = parts + try: + int(iterations) + bytes.fromhex(salt) + bytes.fromhex(digest) + except ValueError: + return False + return True + + +async def is_password_storage_upgraded( + db: BaseDatabase, + config: AstrBotConfig, +) -> bool: + config_upgraded = _has_usable_pbkdf2_password(config) + if config["dashboard"].get(PASSWORD_STORAGE_UPGRADED_KEY) != config_upgraded: + _set_dashboard_flag(config, PASSWORD_STORAGE_UPGRADED_KEY, config_upgraded) + return config_upgraded + + +async def set_password_storage_upgraded( + db: BaseDatabase, + config: AstrBotConfig, + upgraded: bool, +) -> None: + _set_dashboard_flag(config, PASSWORD_STORAGE_UPGRADED_KEY, upgraded) + + +async def is_password_change_required( + db: BaseDatabase, + config: AstrBotConfig, +) -> bool: + stored = config["dashboard"].get(PASSWORD_CHANGE_REQUIRED_KEY, None) + if stored is not None: + return bool(stored) + + required = bool( + getattr(config, "_generated_dashboard_password_change_required", False) + or getattr(config, "_dashboard_password_change_required_from_config", False), + ) + if required: + _set_dashboard_flag(config, PASSWORD_CHANGE_REQUIRED_KEY, True) + return required + + +async def set_password_change_required( + db: BaseDatabase, + config: AstrBotConfig, + required: bool, +) -> None: + _set_dashboard_flag(config, PASSWORD_CHANGE_REQUIRED_KEY, required) + + +def get_dashboard_password_hash(config: AstrBotConfig, *, upgraded: bool) -> str: + if upgraded and _has_usable_pbkdf2_password(config): + return config["dashboard"].get("pbkdf2_password", "") + + legacy_password = config["dashboard"].get("password", "") + if upgraded and not is_legacy_dashboard_password(legacy_password): + return "" + return legacy_password + + +def set_dashboard_password_hashes(config: AstrBotConfig, raw_password: str) -> None: + config["dashboard"]["pbkdf2_password"] = hash_dashboard_password(raw_password) + config["dashboard"]["password"] = hash_legacy_dashboard_password(raw_password) diff --git a/astrbot/dashboard/plugin_page_auth.py b/astrbot/dashboard/plugin_page_auth.py index f2571b3ee..fadfbd50f 100644 --- a/astrbot/dashboard/plugin_page_auth.py +++ b/astrbot/dashboard/plugin_page_auth.py @@ -11,7 +11,7 @@ class PluginPageAuth: @staticmethod def is_protected_path(path: str) -> bool: return path.startswith(PLUGIN_PAGE_CONTENT_PREFIX) or path.startswith( - PLUGIN_PAGE_BRIDGE_PATH + PLUGIN_PAGE_BRIDGE_PATH, ) @staticmethod diff --git a/astrbot/dashboard/routes/auth.py b/astrbot/dashboard/routes/auth.py index e5d5283e1..7450deff2 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -1,36 +1,67 @@ import asyncio import datetime +import os import secrets import jwt -from quart import current_app, jsonify, make_response, request +from quart import current_app, g, jsonify, make_response, request from astrbot import logger from astrbot.core import DEMO_MODE from astrbot.core.utils.auth_password import ( get_dashboard_login_challenge, - hash_dashboard_password, is_default_dashboard_password, is_legacy_dashboard_password, validate_dashboard_password, verify_dashboard_login_proof, verify_dashboard_password, ) +from astrbot.dashboard.password_state import ( + get_dashboard_password_hash, + is_password_change_required, + is_password_storage_upgraded, + set_dashboard_password_hashes, + set_password_change_required, + set_password_storage_upgraded, +) from .route import Response, Route, RouteContext DASHBOARD_JWT_COOKIE_NAME = "astrbot_dashboard_jwt" DASHBOARD_JWT_COOKIE_MAX_AGE = 7 * 24 * 60 * 60 +SKIP_DEFAULT_PASSWORD_AUTH_ENV = "ASTRBOT_DASHBOARD_SKIP_DEFAULT_PASSWORD_AUTH" +SKIP_DEFAULT_PASSWORD_AUTH_ENV_LEGACY = "DASHBOARD_SKIP_DEFAULT_PASSWORD_AUTH" +LOCAL_DASHBOARD_HOSTS = {"127.0.0.1", "localhost", "::1"} +DEFAULT_PASSWORD_LOGIN_FAILURE_MESSAGE = ( + "Login failed. If this is your first time using AstrBot, the old default " + "astrbot password has been replaced by a random strong password printed in " + "the startup logs. Check the initial password in the logs and try again. " + "Learn more: https://docs.astrbot.app/en/faq.html\n\n" + "登录失败。如果您是初次使用,旧版默认 astrbot 密码已改为启动日志中输出的" + "随机强密码。请使用日志中提供的的初始密码来登录。了解更多:" + "https://docs.astrbot.app/faq.html" +) +LEGACY_PASSWORD_LOGIN_FAILURE_MESSAGE = ( + "Incorrect username or password. If you cannot log in after upgrading " + "AstrBot even though the password is correct, see " + "https://docs.astrbot.app/en/faq.html\n\n" + "用户名或密码错误。如果你在升级 AstrBot 后遇到了密码正确但无法登录的情况," + "请参考 https://docs.astrbot.app/faq.html" +) class AuthRoute(Route): - def __init__(self, context: RouteContext) -> None: + def __init__(self, context: RouteContext, db) -> None: super().__init__(context) + self.db = db self._login_challenges: dict[str, dict[str, object]] = {} self.routes = { "/auth/login/challenge": ("POST", self.login_challenge), "/auth/login": ("POST", self.login), "/auth/logout": ("POST", self.logout), + "/auth/setup-status": ("GET", self.setup_status), + "/auth/setup": ("POST", self.setup), + "/auth/setup-authenticated": ("POST", self.setup_authenticated), "/auth/account/edit": ("POST", self.edit_account), } self.register_routes() @@ -69,9 +100,84 @@ class AuthRoute(Route): .__dict__ ) + async def setup_status(self): + return ( + Response() + .ok( + { + "setup_required": await self._is_setup_required(), + "skip_default_password_auth": self._can_skip_default_password_auth(), + "password_upgrade_required": not await is_password_storage_upgraded( + self.db, + self.config, + ), + }, + ) + .__dict__ + ) + + async def setup(self): + if not self._can_skip_default_password_auth(): + return Response().error("Setup without password is not enabled").__dict__ + if not await self._is_setup_required(): + return Response().error("Setup is not required").__dict__ + + return await self._complete_setup() + + async def setup_authenticated(self): + if not await self._is_setup_required(): + return Response().error("Setup is not required").__dict__ + if not isinstance(getattr(g, "username", None), str): + return Response().error("未授权").__dict__ + + return await self._complete_setup() + + async def _complete_setup(self): + post_data = await request.json + if not isinstance(post_data, dict): + return Response().error("Invalid request payload").__dict__ + + new_username = post_data.get("username") + new_password = post_data.get("password") + confirm_password = post_data.get("confirm_password") + if not isinstance(new_username, str) or len(new_username.strip()) < 3: + return Response().error("用户名长度至少3位").__dict__ + if not isinstance(new_password, str): + return Response().error("新密码无效").__dict__ + if not isinstance(confirm_password, str) or confirm_password != new_password: + return Response().error("两次输入的新密码不一致").__dict__ + + try: + validate_dashboard_password(new_password) + except ValueError as e: + return Response().error(str(e)).__dict__ + + username = new_username.strip() + self.config["dashboard"]["username"] = username + set_dashboard_password_hashes(self.config, new_password) + await set_password_storage_upgraded(self.db, self.config, True) + await set_password_change_required(self.db, self.config, False) + self.config.save_config() + + token = self.generate_jwt(username) + payload = Response().ok( + { + "token": token, + "username": username, + "change_pwd_hint": False, + "legacy_pwd_hint": False, + "password_upgrade_required": False, + }, + "Setup completed successfully", + ) + response = await make_response(jsonify(payload.__dict__)) + self._set_dashboard_jwt_cookie(response, token) + return response + async def login(self): - stored_username = self.config["dashboard"]["username"] - stored_password = self.config["dashboard"]["password"] + username = self.config["dashboard"]["username"] + storage_upgraded = await is_password_storage_upgraded(self.db, self.config) + password = get_dashboard_password_hash(self.config, upgraded=storage_upgraded) post_data = await request.json req_username = ( @@ -86,101 +192,75 @@ class AuthRoute(Route): req_password_proof = ( post_data.get("password_proof") if isinstance(post_data, dict) else None ) - if not isinstance(req_username, str) or not isinstance(req_password, str): + if not isinstance(req_username, str): + return Response().error("Invalid request payload").__dict__ + has_password = isinstance(req_password, str) + has_challenge = isinstance(req_challenge_id, str) and isinstance( + req_password_proof, + str, + ) + if not has_password and not has_challenge: return Response().error("Invalid request payload").__dict__ - # First login: if password is not configured (empty/default), trust the user's input - if is_default_dashboard_password(stored_password) and not DEMO_MODE: - # First-time setup: save user's input as admin credentials - new_password_hash = hash_dashboard_password(req_password) - self.config["dashboard"]["username"] = req_username - self.config["dashboard"]["password"] = new_password_hash - self.config.save_config() - logger.warning( - f"Dashboard admin configured via first login: username={req_username}", - ) - return ( - Response() - .ok( - { - "token": self.generate_jwt(req_username), - "username": req_username, - "change_pwd_hint": False, - "legacy_pwd_hint": False, - }, - ) - .__dict__ - ) - login_verified = False - if isinstance(req_password, str): - login_verified = ( - req_username == stored_username - and verify_dashboard_password( - stored_password, - req_password, - ) + if has_password: + login_verified = req_username == username and verify_dashboard_password( + password, + req_password, ) - elif isinstance(req_challenge_id, str) and isinstance(req_password_proof, str): + if not login_verified and has_challenge: challenge_nonce = self._consume_login_challenge(req_challenge_id) login_verified = ( - req_username == stored_username + req_username == username and isinstance(challenge_nonce, str) and verify_dashboard_login_proof( - stored_password, + password, challenge_nonce, req_password_proof, ) ) - else: - return Response().error("Invalid request payload").__dict__ if login_verified: change_pwd_hint = False - legacy_pwd_hint = is_legacy_dashboard_password(stored_password) - if is_default_dashboard_password(stored_password) and not DEMO_MODE: + legacy_pwd_hint = is_legacy_dashboard_password(password) + password_change_required = await is_password_change_required( + self.db, + self.config, + ) + if ( + storage_upgraded + and username == "astrbot" + and is_default_dashboard_password(password) + and not DEMO_MODE + ): change_pwd_hint = True - logger.warning( - "The dashboard is using the default password, please change it immediately to ensure security.", - ) legacy_pwd_hint = True - - token = self.generate_jwt(stored_username) + logger.warning("为了保证安全,请尽快修改默认密码。") + if password_change_required and not DEMO_MODE: + change_pwd_hint = True + token = self.generate_jwt(username) payload = Response().ok( { "token": token, - "username": stored_username, + "username": username, "change_pwd_hint": change_pwd_hint, "legacy_pwd_hint": legacy_pwd_hint, + "password_upgrade_required": not storage_upgraded, }, ) response = await make_response(jsonify(payload.__dict__)) self._set_dashboard_jwt_cookie(response, token) return response await asyncio.sleep(3) - return Response().error("User not found or incorrect password").__dict__ - - def _prune_login_challenges(self) -> None: - now = datetime.datetime.now(datetime.timezone.utc) - expired_ids = [ - challenge_id - for challenge_id, challenge in self._login_challenges.items() - if challenge.get("expires_at") <= now - ] - for challenge_id in expired_ids: - self._login_challenges.pop(challenge_id, None) - - def _consume_login_challenge(self, challenge_id: str) -> str | None: - self._prune_login_challenges() - challenge = self._login_challenges.pop(challenge_id, None) - if not isinstance(challenge, dict): - return None - nonce = challenge.get("nonce") - return nonce if isinstance(nonce, str) else None + if req_password == "astrbot": + return Response().error(DEFAULT_PASSWORD_LOGIN_FAILURE_MESSAGE).__dict__ + if is_legacy_dashboard_password(password): + return Response().error(LEGACY_PASSWORD_LOGIN_FAILURE_MESSAGE).__dict__ + return Response().error("用户名或密码错误").__dict__ async def logout(self): response = await make_response( - jsonify(Response().ok(None, "已退出登录").__dict__) + jsonify(Response().ok(None, "已退出登录").__dict__), ) self._clear_dashboard_jwt_cookie(response) return response @@ -193,7 +273,8 @@ class AuthRoute(Route): .__dict__ ) - password = self.config["dashboard"]["password"] + storage_upgraded = await is_password_storage_upgraded(self.db, self.config) + password = get_dashboard_password_hash(self.config, upgraded=storage_upgraded) post_data = await request.json if not isinstance(post_data, dict): return Response().error("Invalid request payload").__dict__ @@ -207,6 +288,12 @@ class AuthRoute(Route): new_pwd = post_data.get("new_password", None) new_username = post_data.get("new_username", None) + password_change_required = await is_password_change_required( + self.db, + self.config, + ) + if (not storage_upgraded or password_change_required) and not new_pwd: + return Response().error("请设置新密码以完成安全升级").__dict__ if not new_pwd and not new_username: return Response().error("新用户名和新密码不能同时为空").__dict__ @@ -221,7 +308,9 @@ class AuthRoute(Route): validate_dashboard_password(new_pwd) except ValueError as e: return Response().error(str(e)).__dict__ - self.config["dashboard"]["password"] = hash_dashboard_password(new_pwd) + set_dashboard_password_hashes(self.config, new_pwd) + await set_password_storage_upgraded(self.db, self.config, True) + await set_password_change_required(self.db, self.config, False) if new_username: self.config["dashboard"]["username"] = new_username @@ -241,13 +330,52 @@ class AuthRoute(Route): token = jwt.encode(payload, jwt_token, algorithm="HS256") return token + async def _is_setup_required(self) -> bool: + if DEMO_MODE: + return False + + dashboard_config = self.config["dashboard"] + password_change_required = await is_password_change_required( + self.db, + self.config, + ) + if password_change_required: + return True + + storage_upgraded = await is_password_storage_upgraded(self.db, self.config) + if not storage_upgraded: + return False + + return dashboard_config.get( + "username", + ) == "astrbot" and is_default_dashboard_password( + dashboard_config.get("pbkdf2_password", ""), + ) + + def _can_skip_default_password_auth(self) -> bool: + if not self._env_flag_enabled(SKIP_DEFAULT_PASSWORD_AUTH_ENV): + return False + host = ( + os.environ.get("DASHBOARD_HOST") + or os.environ.get("ASTRBOT_DASHBOARD_HOST") + or self.config["dashboard"].get("host", "") + ) + return str(host).strip().lower() in LOCAL_DASHBOARD_HOSTS + + @staticmethod + def _env_flag_enabled(name: str) -> bool: + value = os.environ.get(name) + if value is None and name == SKIP_DEFAULT_PASSWORD_AUTH_ENV: + value = os.environ.get(SKIP_DEFAULT_PASSWORD_AUTH_ENV_LEGACY) + return str(value or "").strip().lower() in {"1", "true", "yes", "on"} + @staticmethod def _use_secure_dashboard_jwt_cookie() -> bool: return bool( current_app.config.get( "DASHBOARD_JWT_COOKIE_SECURE", not current_app.debug and not current_app.testing, - ) + ), ) @staticmethod @@ -271,3 +399,21 @@ class AuthRoute(Route): secure=AuthRoute._use_secure_dashboard_jwt_cookie(), path="/", ) + + def _prune_login_challenges(self) -> None: + now = datetime.datetime.now(datetime.timezone.utc) + expired_ids = [ + challenge_id + for challenge_id, challenge in self._login_challenges.items() + if challenge.get("expires_at") <= now + ] + for challenge_id in expired_ids: + self._login_challenges.pop(challenge_id, None) + + def _consume_login_challenge(self, challenge_id: str) -> str | None: + self._prune_login_challenges() + challenge = self._login_challenges.pop(challenge_id, None) + if not isinstance(challenge, dict): + return None + nonce = challenge.get("nonce") + return nonce if isinstance(nonce, str) else None diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 8b5a65e5e..c48280916 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -141,7 +141,9 @@ class BotMessageAccumulator: self.parts.append(part) def build_message_parts( - self, *, include_pending_tool_calls: bool = False + self, + *, + include_pending_tool_calls: bool = False, ) -> list[dict]: self._flush_pending_text() if include_pending_tool_calls and self.pending_tool_calls: @@ -261,20 +263,24 @@ class ChatRoute(Route): file_path = os.path.join(self.attachments_dir, os.path.basename(filename)) real_file_path = await asyncio.to_thread(os.path.realpath, file_path) real_imgs_dir = await asyncio.to_thread( - os.path.realpath, self.attachments_dir + os.path.realpath, + self.attachments_dir, ) if not await asyncio.to_thread(os.path.exists, real_file_path): # try legacy file_path = os.path.join( - self.legacy_img_dir, os.path.basename(filename) + self.legacy_img_dir, + os.path.basename(filename), ) if await asyncio.to_thread(os.path.exists, file_path): real_file_path = await asyncio.to_thread( - os.path.realpath, file_path + os.path.realpath, + file_path, ) real_imgs_dir = await asyncio.to_thread( - os.path.realpath, self.legacy_img_dir + os.path.realpath, + self.legacy_img_dir, ) if not real_file_path.startswith(real_imgs_dir): @@ -351,7 +357,7 @@ class ChatRoute(Route): "attachment_id": attachment.attachment_id, "filename": filename, "type": attach_type, - } + }, ) .__dict__ ) @@ -365,7 +371,9 @@ class ChatRoute(Route): ) async def _create_attachment_from_file( - self, filename: str, attach_type: str + self, + filename: str, + attach_type: str, ) -> dict | None: """从本地文件创建 attachment 并返回消息部分。""" return await create_attachment_part_from_existing_file( @@ -377,7 +385,9 @@ class ChatRoute(Route): ) def _extract_web_search_refs( - self, accumulated_text: str, accumulated_parts: list + self, + accumulated_text: str, + accumulated_parts: list, ) -> dict: """从消息中提取 web_search_tavily 的引用 @@ -387,6 +397,7 @@ class ChatRoute(Route): Returns: 包含 used 列表的字典,记录被引用的搜索结果 + """ supported = [ "web_search_baidu", @@ -405,7 +416,7 @@ class ChatRoute(Route): for part in tool_call_parts: for tool_call in part["tool_calls"]: if tool_call.get("name") not in supported or not tool_call.get( - "result" + "result", ): continue try: @@ -500,7 +511,8 @@ class ChatRoute(Route): async def _delete_threads_by_ids(self, thread_ids: list[str], creator: str) -> None: for thread_id in thread_ids: unified_msg_origin = self._build_thread_unified_msg_origin( - creator, thread_id + creator, + thread_id, ) active_event_registry.request_agent_stop_all(unified_msg_origin) await self.conv_mgr.delete_conversations_by_user_id(unified_msg_origin) @@ -515,7 +527,7 @@ class ChatRoute(Route): async def _load_current_conversation_history(self, session) -> tuple[str, list]: unified_msg_origin = self._build_webchat_unified_msg_origin(session) conversation_id = await self.conv_mgr.get_curr_conversation_id( - unified_msg_origin + unified_msg_origin, ) if not conversation_id: return "", [] @@ -534,7 +546,9 @@ class ChatRoute(Route): return conversation_id, history if isinstance(history, list) else [] def _find_checkpoint_index( - self, history: list[dict], checkpoint_id: str + self, + history: list[dict], + checkpoint_id: str, ) -> int | None: for index, message in enumerate(history): if get_checkpoint_id(message) == checkpoint_id: @@ -542,7 +556,9 @@ class ChatRoute(Route): return None def _find_turn_range( - self, history: list[dict], checkpoint_id: str + self, + history: list[dict], + checkpoint_id: str, ) -> tuple[int, int] | None: checkpoint_index = self._find_checkpoint_index(history, checkpoint_id) if checkpoint_index is None: @@ -626,7 +642,10 @@ class ChatRoute(Route): return result def _find_turn_user_index( - self, history: list[dict], start: int, end: int + self, + history: list[dict], + start: int, + end: int, ) -> int | None: for index in range(start, end): message = history[index] @@ -635,7 +654,10 @@ class ChatRoute(Route): return None def _find_turn_final_assistant_index( - self, history: list[dict], start: int, end: int + self, + history: list[dict], + start: int, + end: int, ) -> int | None: for index in range(end - 1, start - 1, -1): message = history[index] @@ -657,7 +679,9 @@ class ChatRoute(Route): return history_list async def _delete_platform_history_after( - self, session, message_id: int + self, + session, + message_id: int, ) -> list[int]: history_list = await self._get_sorted_platform_history(session) should_delete = False @@ -778,7 +802,7 @@ class ChatRoute(Route): "data": { "id": saved_user_record.id, "created_at": to_utc_isoformat( - saved_user_record.created_at + saved_user_record.created_at, ), "llm_checkpoint_id": llm_checkpoint_id, }, @@ -788,7 +812,8 @@ class ChatRoute(Route): async with track_conversation(self.running_convs, webchat_conv_id): while True: result, should_break = await _poll_webchat_stream_result( - back_queue, username + back_queue, + username, ) if should_break: client_disconnected = True @@ -829,7 +854,7 @@ class ChatRoute(Route): except Exception as e: if not client_disconnected: logger.debug( - f"[WebChat] 用户 {username} 断开聊天长连接。 {e}" + f"[WebChat] 用户 {username} 断开聊天长连接。 {e}", ) client_disconnected = True @@ -849,7 +874,7 @@ class ChatRoute(Route): if accumulated_text: # 如果累积了文本,则先保存文本 accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" elif chain_type == "tool_call_result": @@ -862,7 +887,7 @@ class ChatRoute(Route): { "type": "tool_call", "tool_calls": [tool_calls[tc_id]], - } + }, ) tool_calls.pop(tc_id, None) elif chain_type == "reasoning": @@ -874,14 +899,16 @@ class ChatRoute(Route): elif msg_type == "image": filename = result_text.replace("[IMAGE]", "") part = await self._create_attachment_from_file( - filename, "image" + filename, + "image", ) if part: accumulated_parts.append(part) elif msg_type == "record": filename = result_text.replace("[RECORD]", "") part = await self._create_attachment_from_file( - filename, "record" + filename, + "record", ) if part: accumulated_parts.append(part) @@ -889,7 +916,8 @@ class ChatRoute(Route): # 格式: [FILE]filename filename = result_text.replace("[FILE]", "") part = await self._create_attachment_from_file( - filename, "file" + filename, + "file", ) if part: accumulated_parts.append(part) @@ -936,7 +964,7 @@ class ChatRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), "llm_checkpoint_id": llm_checkpoint_id, }, @@ -987,7 +1015,7 @@ class ChatRoute(Route): ) response = cast( - QuartResponse, + "QuartResponse", await make_response( stream(), { @@ -1136,7 +1164,7 @@ class ChatRoute(Route): "deleted_count": deleted_count, "failed_count": len(failed_items), "failed_items": failed_items, - } + }, ) .__dict__ ) @@ -1165,7 +1193,7 @@ class ChatRoute(Route): await asyncio.to_thread(os.remove, attachment.path) except OSError as e: logger.warning( - f"Failed to delete attachment file {attachment.path}: {e}" + f"Failed to delete attachment file {attachment.path}: {e}", ) except Exception as e: logger.warning(f"Failed to get attachments: {e}") @@ -1196,7 +1224,7 @@ class ChatRoute(Route): data={ "session_id": session.session_id, "platform_id": session.platform_id, - } + }, ) .__dict__ ) @@ -1230,7 +1258,7 @@ class ChatRoute(Route): "is_group": session.is_group, "created_at": to_utc_isoformat(session.created_at), "updated_at": to_utc_isoformat(session.updated_at), - } + }, ) return Response().ok(data=sessions_data).__dict__ @@ -1248,7 +1276,8 @@ class ChatRoute(Route): # 获取项目信息(如果会话属于某个项目) username = g.get("username", "guest") project_info = await self.db.get_project_by_session( - session_id=session_id, creator=username + session_id=session_id, + creator=username, ) # Get platform message history using session_id @@ -1310,7 +1339,7 @@ class ChatRoute(Route): return Response().error("Permission denied").__dict__ parent_record = await self.db.get_platform_message_history_by_id( - parent_message_id + parent_message_id, ) if ( not parent_record @@ -1339,7 +1368,7 @@ class ChatRoute(Route): return Response().ok(data=self._serialize_thread(existing)).__dict__ conversation_id, history = await self._load_current_conversation_history( - session + session, ) turn_range = self._find_turn_range(history, checkpoint_id) if not conversation_id or not turn_range: @@ -1390,7 +1419,7 @@ class ChatRoute(Route): "thread": self._serialize_thread(thread), "history": [history.model_dump() for history in history_ls], "is_running": self.running_convs.get(thread_id, False), - } + }, ) .__dict__ ) @@ -1421,7 +1450,7 @@ class ChatRoute(Route): "selected_model": post_data.get("selected_model"), "_platform_history_id": "webchat_thread", "_thread_selected_text": thread.selected_text, - } + }, ) async def delete_thread(self): @@ -1507,7 +1536,7 @@ class ChatRoute(Route): ) conversation_id, history = await self._load_current_conversation_history( - session + session, ) turn_range = self._find_turn_range(history, checkpoint_id) if not conversation_id or not turn_range: @@ -1529,7 +1558,8 @@ class ChatRoute(Route): llm_checkpoint_id=new_checkpoint_id, ) deleted_message_ids = await self._delete_platform_history_after( - session, message_id + session, + message_id, ) thread_ids = await self.db.delete_webchat_threads_by_parent_message_ids( session_id, @@ -1550,7 +1580,7 @@ class ChatRoute(Route): "message": updated.model_dump() if updated else None, "needs_regenerate": True, "truncated_after_message": True, - } + }, ) .__dict__ ) @@ -1598,7 +1628,7 @@ class ChatRoute(Route): return Response().error("Message is not linked to LLM history").__dict__ conversation_id, history = await self._load_current_conversation_history( - session + session, ) turn_range = self._find_turn_range(history, checkpoint_id) if not conversation_id or not turn_range: @@ -1668,7 +1698,7 @@ class ChatRoute(Route): "selected_model": post_data.get("selected_model"), "_skip_user_history": True, "_llm_checkpoint_id": new_checkpoint_id, - } + }, ) async def update_session_display_name(self): diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index 16c605848..5c0ca5a87 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -60,7 +60,7 @@ class LiveChatSession: start_time = time.time() if not self.is_speaking or stamp != self.current_stamp: logger.warning( - f"[Live Chat] stamp 不匹配或未在说话状态: {stamp} vs {self.current_stamp}" + f"[Live Chat] stamp 不匹配或未在说话状态: {stamp} vs {self.current_stamp}", ) return None, 0.0 @@ -86,7 +86,7 @@ class LiveChatSession: self.temp_audio_path = audio_path logger.info( - f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {os.path.getsize(audio_path)} bytes" + f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {os.path.getsize(audio_path)} bytes", ) return audio_path, time.time() - start_time @@ -183,7 +183,9 @@ class LiveChatRoute(Route): logger.info(f"[Live Chat] WebSocket 连接关闭: {username}") async def _create_attachment_from_file( - self, filename: str, attach_type: str + self, + filename: str, + attach_type: str, ) -> dict | None: """从本地文件创建 attachment 并返回消息部分。""" return await create_attachment_part_from_existing_file( @@ -195,7 +197,9 @@ class LiveChatRoute(Route): ) def _extract_web_search_refs( - self, accumulated_text: str, accumulated_parts: list + self, + accumulated_text: str, + accumulated_parts: list, ) -> dict: """从消息中提取 web_search 引用。""" supported = [ @@ -214,7 +218,7 @@ class LiveChatRoute(Route): for part in tool_call_parts: for tool_call in part["tool_calls"]: if tool_call.get("name") not in supported or not tool_call.get( - "result" + "result", ): continue try: @@ -291,7 +295,8 @@ class LiveChatRoute(Route): request_id: str, ) -> None: back_queue = webchat_queue_mgr.get_or_create_back_queue( - request_id, chat_session_id + request_id, + chat_session_id, ) try: while True: @@ -344,7 +349,9 @@ class LiveChatRoute(Route): session.chat_subscription_tasks.clear() async def _handle_chat_message( - self, session: LiveChatSession, message: dict + self, + session: LiveChatSession, + message: dict, ) -> None: """处理 Chat Mode 消息(ct=chat)""" msg_type = message.get("t") @@ -551,7 +558,7 @@ class LiveChatRoute(Route): tool_calls[tool_call.get("id")] = tool_call if accumulated_text: accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" except Exception: @@ -567,7 +574,7 @@ class LiveChatRoute(Route): { "type": "tool_call", "tool_calls": [tool_calls[tc_id]], - } + }, ) tool_calls.pop(tc_id, None) except Exception: @@ -606,7 +613,7 @@ class LiveChatRoute(Route): or accumulated_text or accumulated_reasoning or refs - or agent_stats + or agent_stats, ) elif (streaming and msg_type == "complete") or not streaming: if chain_type not in ( @@ -646,7 +653,7 @@ class LiveChatRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), "llm_checkpoint_id": llm_checkpoint_id, }, @@ -669,7 +676,7 @@ class LiveChatRoute(Route): { "ct": "chat", "t": "error", - "data": f"处理失败: {str(e)}", + "data": f"处理失败: {e!s}", "code": "PROCESSING_ERROR", }, ) @@ -733,13 +740,16 @@ class LiveChatRoute(Route): logger.info(f"[Live Chat] 用户打断: {session.username}") async def _process_audio( - self, session: LiveChatSession, audio_path: str, assemble_duration: float + self, + session: LiveChatSession, + audio_path: str, + assemble_duration: float, ) -> None: """处理音频:STT -> LLM -> 流式 TTS""" try: # 发送 WAV 组装耗时 await websocket.send_json( - {"t": "metrics", "data": {"wav_assemble_time": assemble_duration}} + {"t": "metrics", "data": {"wav_assemble_time": assemble_duration}}, ) wav_assembly_finish_time = time.time() @@ -756,7 +766,7 @@ class LiveChatRoute(Route): return await websocket.send_json( - {"t": "metrics", "data": {"stt": stt_provider.meta().type}} + {"t": "metrics", "data": {"stt": stt_provider.meta().type}}, ) user_text = await stt_provider.get_text(audio_path) @@ -770,7 +780,7 @@ class LiveChatRoute(Route): { "t": "user_msg", "data": {"text": user_text, "ts": int(time.time() * 1000)}, - } + }, ) # 2. 构造消息事件并发送到 pipeline @@ -802,7 +812,9 @@ class LiveChatRoute(Route): await websocket.send_json({"t": "stop_play"}) # 保存消息并标记为被打断 await self._save_interrupted_message( - session, user_text, bot_text + session, + user_text, + bot_text, ) # 清空队列中未处理的消息 while not back_queue.empty(): @@ -823,7 +835,7 @@ class LiveChatRoute(Route): result_message_id = result.get("message_id") if result_message_id != message_id: logger.warning( - f"[Live Chat] 消息 ID 不匹配: {result_message_id} != {message_id}" + f"[Live Chat] 消息 ID 不匹配: {result_message_id} != {message_id}", ) continue @@ -842,7 +854,7 @@ class LiveChatRoute(Route): "llm_total_time": stats.get("end_time", 0) - stats.get("start_time", 0), }, - } + }, ) except Exception as e: logger.error(f"[Live Chat] 解析 AgentStats 失败: {e}") @@ -855,7 +867,7 @@ class LiveChatRoute(Route): { "t": "metrics", "data": stats, - } + }, ) except Exception as e: logger.error(f"[Live Chat] 解析 TTSStats 失败: {e}") @@ -879,9 +891,9 @@ class LiveChatRoute(Route): { "t": "metrics", "data": { - "speak_to_first_frame": speak_to_first_frame_latency + "speak_to_first_frame": speak_to_first_frame_latency, }, - } + }, ) text = result.get("text") @@ -890,7 +902,7 @@ class LiveChatRoute(Route): { "t": "bot_text_chunk", "data": {"text": text}, - } + }, ) # 发送音频数据给前端 @@ -898,7 +910,7 @@ class LiveChatRoute(Route): { "t": "response", "data": data, # base64 编码的音频数据 - } + }, ) elif result_type in ["complete", "end"]: @@ -914,7 +926,7 @@ class LiveChatRoute(Route): "text": bot_text, "ts": int(time.time() * 1000), }, - } + }, ) # 发送结束标记 @@ -926,7 +938,7 @@ class LiveChatRoute(Route): { "t": "metrics", "data": {"wav_to_tts_total_time": wav_to_tts_duration}, - } + }, ) break finally: @@ -934,14 +946,17 @@ class LiveChatRoute(Route): except Exception as e: logger.error(f"[Live Chat] 处理音频失败: {e}", exc_info=True) - await websocket.send_json({"t": "error", "data": f"处理失败: {str(e)}"}) + await websocket.send_json({"t": "error", "data": f"处理失败: {e!s}"}) finally: session.is_processing = False session.should_interrupt = False async def _save_interrupted_message( - self, session: LiveChatSession, user_text: str, bot_text: str + self, + session: LiveChatSession, + user_text: str, + bot_text: str, ) -> None: """保存被打断的消息""" interrupted_text = bot_text + " [用户打断]" @@ -951,11 +966,11 @@ class LiveChatRoute(Route): try: timestamp = int(time.time() * 1000) logger.info( - f"[Live Chat] 用户消息: {user_text} (session: {session.session_id}, ts: {timestamp})" + f"[Live Chat] 用户消息: {user_text} (session: {session.session_id}, ts: {timestamp})", ) if bot_text: logger.info( - f"[Live Chat] Bot 消息(打断): {interrupted_text} (session: {session.session_id}, ts: {timestamp})" + f"[Live Chat] Bot 消息(打断): {interrupted_text} (session: {session.session_id}, ts: {timestamp})", ) except Exception as e: logger.error(f"[Live Chat] 记录消息失败: {e}", exc_info=True) diff --git a/astrbot/dashboard/routes/open_api.py b/astrbot/dashboard/routes/open_api.py index 58db0fa1b..d210702bf 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -461,7 +461,7 @@ class OpenApiRoute(Route): should_save = False if msg_type == "end": should_save = bool( - message_accumulator.has_content() or refs or agent_stats + message_accumulator.has_content() or refs or agent_stats, ) elif (streaming and msg_type == "complete") or not streaming: if chain_type not in ("tool_call", "tool_call_result"): @@ -469,10 +469,10 @@ class OpenApiRoute(Route): if should_save: message_parts_to_save = message_accumulator.build_message_parts( - include_pending_tool_calls=True + include_pending_tool_calls=True, ) plain_text = collect_plain_text_from_message_parts( - message_parts_to_save + message_parts_to_save, ) try: refs = self.chat_route._extract_web_search_refs( diff --git a/astrbot/dashboard/routes/platform.py b/astrbot/dashboard/routes/platform.py index 50917d161..3fcb93eb0 100644 --- a/astrbot/dashboard/routes/platform.py +++ b/astrbot/dashboard/routes/platform.py @@ -3,15 +3,35 @@ 提供统一的 webhook 回调入口,支持多个平台使用同一端口接收回调。 """ +import secrets +import string + from quart import request from astrbot.core import logger from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.platform import Platform +from astrbot.core.platform.sources.dingtalk.app_registration import ( + poll_dingtalk_app_registration_once, + request_dingtalk_app_registration, +) +from astrbot.core.platform.sources.lark.app_registration import ( + poll_app_registration_once, + request_app_registration, +) +from astrbot.core.platform.sources.lark.bot_info import request_lark_bot_info +from astrbot.core.platform.sources.weixin_oc.login_registration import ( + poll_weixin_oc_login_once, + request_weixin_oc_login_qr, +) from .route import Response, Route, RouteContext +def _random_platform_id_suffix() -> str: + return "_" + "".join(secrets.choice(string.ascii_lowercase) for _ in range(4)) + + class PlatformRoute(Route): """统一 Webhook 路由""" @@ -42,6 +62,12 @@ class PlatformRoute(Route): methods=["GET"], ) + self.app.add_url_rule( + "/api/platform/registration/", + view_func=self.handle_platform_registration, + methods=["POST"], + ) + async def unified_webhook_callback(self, webhook_uuid: str): """统一 webhook 回调入口 @@ -105,4 +131,163 @@ class PlatformRoute(Route): return Response().ok(stats).to_json() except Exception as e: logger.error(f"获取平台统计信息失败: {e}", exc_info=True) - return Response().error(f"获取统计信息失败: {e}").to_json(), 500 + return Response().error(f"获取统计信息失败: {e}").__dict__, 500 + + async def handle_platform_registration(self, platform_type: str): + """Handle dashboard one-click platform registration actions.""" + try: + payload = await request.get_json(silent=True) or {} + action = str(payload.get("action", "")).strip().lower() + if not action: + return Response().error("Missing action").__dict__, 400 + + platform_config = payload.get("platform_config") + if not isinstance(platform_config, dict): + platform_config = {} + + if platform_type == "lark": + return await self._handle_lark_registration( + action, + payload, + platform_config, + ) + if platform_type == "weixin_oc": + return await self._handle_weixin_oc_registration( + action, + payload, + platform_config, + ) + if platform_type == "dingtalk": + return await self._handle_dingtalk_registration(action, payload) + + return Response().error( + f"Unsupported platform registration: {platform_type}", + ).__dict__, 404 + except Exception as e: + logger.error(f"处理平台一键创建请求失败: {e}", exc_info=True) + return Response().error(str(e)).__dict__, 500 + + async def _handle_lark_registration( + self, + action: str, + payload: dict, + platform_config: dict, + ): + domain = str(platform_config.get("domain") or "").strip() + + if action == "start": + registration = await request_app_registration(domain) + return ( + Response() + .ok( + { + "status": "pending", + "device_code": registration.device_code, + "registration_code": registration.device_code, + "user_code": registration.user_code, + "verification_uri": registration.verification_uri, + "verification_uri_complete": registration.verification_uri_complete, + "expires_in": registration.expires_in, + "interval": registration.interval, + }, + ) + .__dict__ + ) + + if action == "poll": + device_code = str( + payload.get("device_code") or payload.get("registration_code") or "", + ).strip() + if not device_code: + return Response().error("Missing device_code").__dict__, 400 + result = await poll_app_registration_once( + domain=domain, + device_code=device_code, + ) + if result.get("status") == "created": + try: + bot_info = await request_lark_bot_info( + domain=str(result.get("domain") or domain), + app_id=str(result.get("app_id") or ""), + app_secret=str(result.get("app_secret") or ""), + ) + if bot_info.app_name: + result["bot_name"] = bot_info.app_name + if bot_info.open_id: + result["bot_open_id"] = bot_info.open_id + except Exception as e: + logger.error(f"获取飞书机器人信息失败: {e}", exc_info=True) + return Response().ok(result).__dict__ + + return Response().error(f"Unsupported action: {action}").__dict__, 400 + + async def _handle_dingtalk_registration(self, action: str, payload: dict): + if action == "start": + registration = await request_dingtalk_app_registration() + return ( + Response() + .ok( + { + "status": "pending", + "device_code": registration.device_code, + "registration_code": registration.device_code, + "user_code": registration.user_code, + "verification_uri": registration.verification_uri, + "verification_uri_complete": registration.verification_uri_complete, + "expires_in": registration.expires_in, + "interval": registration.interval, + }, + ) + .__dict__ + ) + + if action == "poll": + device_code = str( + payload.get("device_code") or payload.get("registration_code") or "", + ).strip() + if not device_code: + return Response().error("Missing device_code").__dict__, 400 + result = await poll_dingtalk_app_registration_once(device_code) + if result.get("status") == "created": + result["platform_id_suffix"] = _random_platform_id_suffix() + return Response().ok(result).__dict__ + + return Response().error(f"Unsupported action: {action}").__dict__, 400 + + async def _handle_weixin_oc_registration( + self, + action: str, + payload: dict, + platform_config: dict, + ): + if action == "start": + registration = await request_weixin_oc_login_qr(platform_config) + return ( + Response() + .ok( + { + "status": "pending", + "registration_code": registration.qrcode, + "qrcode": registration.qrcode, + "qrcode_img_content": registration.qrcode_img_content, + "interval": registration.interval, + }, + ) + .__dict__ + ) + + if action == "poll": + qrcode = str( + payload.get("qrcode") or payload.get("registration_code") or "", + ).strip() + if not qrcode: + return Response().error("Missing qrcode").__dict__, 400 + result = await poll_weixin_oc_login_once( + platform_config=platform_config, + qrcode=qrcode, + ) + if result.get("status") == "created": + result["platform_id_suffix"] = _random_platform_id_suffix() + return Response().ok(result).__dict__ + + return Response().error(f"Unsupported action: {action}").__dict__, 400 diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 1093022e3..73a6995b7 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -201,7 +201,8 @@ class PluginRoute(Route): async def get_plugin_page_bridge_sdk(self): if not await aio_ospath.isfile(str(_PLUGIN_PAGE_BRIDGE_FILE)): return await self._plugin_page_error_response( - 404, "Plugin Page bridge SDK not found" + 404, + "Plugin Page bridge SDK not found", ) bridge_js = await self._read_plugin_page_text(_PLUGIN_PAGE_BRIDGE_FILE) initial_context = self._get_plugin_page_initial_context() @@ -211,9 +212,10 @@ class PluginRoute(Route): f"\n;window.AstrBotPluginPage?.__setInitialContext({context_json});\n" ) response = cast( - QuartResponse, + "QuartResponse", await make_response( - bridge_js, {"Content-Type": "application/javascript; charset=utf-8"} + bridge_js, + {"Content-Type": "application/javascript; charset=utf-8"}, ), ) return self._apply_plugin_page_security_headers(response) @@ -346,7 +348,7 @@ class PluginRoute(Route): base_dir = Path( self.plugin_manager.reserved_plugin_path if plugin.reserved - else self.plugin_manager.plugin_store_path + else self.plugin_manager.plugin_store_path, ).resolve(strict=False) plugin_root = (base_dir / plugin.root_dir_name).resolve(strict=False) plugin_root.relative_to(base_dir) @@ -393,7 +395,7 @@ class PluginRoute(Route): name=page_name, title=page_name, entry_file=_PLUGIN_PAGE_ENTRY_FILE_NAME, - ) + ), ) return pages @@ -454,7 +456,7 @@ class PluginRoute(Route): "mailto:", "tel:", "blob:", - ) + ), ): return False return True @@ -500,7 +502,7 @@ class PluginRoute(Route): path, query, original_fragment, - ) + ), ) @staticmethod @@ -539,7 +541,7 @@ class PluginRoute(Route): "/api/plugin/page/bridge-sdk.js", query, "", - ) + ), ) @staticmethod @@ -608,7 +610,9 @@ class PluginRoute(Route): bridge_tag = f'' if "" in rewritten_html: rewritten_html = rewritten_html.replace( - "", f"{bridge_tag}", 1 + "", + f"{bridge_tag}", + 1, ) else: rewritten_html += bridge_tag @@ -776,7 +780,7 @@ class PluginRoute(Route): "iat": now, "exp": now + timedelta(seconds=_PLUGIN_PAGE_ASSET_TOKEN_TTL_SECONDS), } - return cast(str, jwt.encode(payload, jwt_secret, algorithm="HS256")) + return cast("str", jwt.encode(payload, jwt_secret, algorithm="HS256")) def _prepare_plugin_page_query_params( self, @@ -830,9 +834,10 @@ class PluginRoute(Route): extra_query_params=extra_query_params, ) response = cast( - QuartResponse, + "QuartResponse", await make_response( - rewritten_html, {"Content-Type": "text/html; charset=utf-8"} + rewritten_html, + {"Content-Type": "text/html; charset=utf-8"}, ), ) return self._apply_plugin_page_security_headers(response) @@ -854,9 +859,10 @@ class PluginRoute(Route): extra_query_params=extra_query_params, ) response = cast( - QuartResponse, + "QuartResponse", await make_response( - rewritten_css, {"Content-Type": "text/css; charset=utf-8"} + rewritten_css, + {"Content-Type": "text/css; charset=utf-8"}, ), ) return self._apply_plugin_page_security_headers(response) @@ -878,7 +884,7 @@ class PluginRoute(Route): extra_query_params=extra_query_params, ) response = cast( - QuartResponse, + "QuartResponse", await make_response( rewritten_js, {"Content-Type": "application/javascript; charset=utf-8"}, @@ -889,7 +895,7 @@ class PluginRoute(Route): async def _serve_plugin_page_static_asset(self, file_path: Path): raw_bytes = await self._read_plugin_page_binary(file_path) response = cast( - QuartResponse, + "QuartResponse", await make_response( raw_bytes, {"Content-Type": self._guess_plugin_page_mime_type(file_path)}, @@ -918,7 +924,8 @@ class PluginRoute(Route): ) except (FileNotFoundError, ValueError): return await self._plugin_page_error_response( - 404, "Plugin Page asset not found" + 404, + "Plugin Page asset not found", ) extra_query_params = self._prepare_plugin_page_query_params( @@ -1184,7 +1191,7 @@ class PluginRoute(Route): remote_md5 = await self._fetch_remote_md5(source.md5_url) if remote_md5 is None: logger.warning( - "Cannot fetch remote MD5, using cache without validation" + "Cannot fetch remote MD5, using cache without validation", ) return True # 如果无法获取远程MD5,认为缓存有效 @@ -1357,7 +1364,7 @@ class PluginRoute(Route): "astrbot_version": plugin.astrbot_version, "installed_at": self._get_plugin_installed_at(plugin), "i18n": plugin.i18n, - } + }, ) .__dict__ ) @@ -1432,7 +1439,7 @@ class PluginRoute(Route): component_type = "command" info["display_type"] = "指令" info["cmd"] = self._get_command_filter_display_name( - event_filter + event_filter, ) component = self._build_command_filter_component( event_filter, @@ -1525,7 +1532,7 @@ class PluginRoute(Route): "name": skill.name, "description": skill.description or "无描述", "path": skill.path, - } + }, ) return components @@ -1820,7 +1827,9 @@ class PluginRoute(Route): try: logger.info(f"正在更新插件 {plugin_name}") await self.plugin_manager.update_plugin( - plugin_name, proxy, download_url=download_url + plugin_name, + proxy, + download_url=download_url, ) # self.core_lifecycle.restart() await self.plugin_manager.reload(plugin_name) @@ -1858,7 +1867,9 @@ class PluginRoute(Route): logger.info(f"批量更新插件 {name}") download_url = str(download_urls.get(name) or "").strip() await self.plugin_manager.update_plugin( - name, proxy, download_url=download_url + name, + proxy, + download_url=download_url, ) return {"name": name, "status": "ok", "message": "更新成功"} except Exception as e: diff --git a/astrbot/dashboard/routes/skills.py b/astrbot/dashboard/routes/skills.py index eac326e6d..f77710399 100644 --- a/astrbot/dashboard/routes/skills.py +++ b/astrbot/dashboard/routes/skills.py @@ -111,7 +111,7 @@ class SkillsRoute(Route): skill_mgr = SkillManager() if skill_mgr.is_sandbox_only_skill(skill_name): raise PermissionError( - "Sandbox preset skill cannot be opened from local skill files." + "Sandbox preset skill cannot be opened from local skill files.", ) plugin_skill_dir = skill_mgr._get_plugin_skill_dir(skill_name) @@ -508,7 +508,7 @@ class SkillsRoute(Route): return ( Response() .error( - "Plugin-provided skill cannot be downloaded from local skill files." + "Plugin-provided skill cannot be downloaded from local skill files.", ) .__dict__ ) @@ -572,7 +572,7 @@ class SkillsRoute(Route): skill_dir, resolved, readonly=readonly, - ) + ), ) return ( @@ -582,7 +582,7 @@ class SkillsRoute(Route): "name": name, "path": self._skill_relative_path(skill_dir, target_dir), "entries": entries, - } + }, ) .__dict__ ) @@ -621,7 +621,7 @@ class SkillsRoute(Route): "content": content, "size": size, "editable": not SkillManager().is_plugin_skill(name), - } + }, ) .__dict__ ) @@ -674,7 +674,7 @@ class SkillsRoute(Route): "name": name, "path": self._skill_relative_path(skill_dir, target_file), "size": len(encoded), - } + }, ) .__dict__ ) diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index f23040c1f..f5f2484c5 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -28,6 +28,11 @@ from astrbot.core.utils.auth_password import ( from astrbot.core.utils.io import get_dashboard_version from astrbot.core.utils.storage_cleaner import StorageCleaner from astrbot.core.utils.version_comparator import VersionComparator +from astrbot.dashboard.password_state import ( + get_dashboard_password_hash, + is_password_change_required, + is_password_storage_upgraded, +) from .route import Response, Route, RouteContext @@ -75,17 +80,37 @@ class StatRoute(Route): hours, minutes = divmod(minutes, 60) return {"hours": hours, "minutes": minutes, "seconds": seconds} - def is_default_cred(self): - username = self.config["dashboard"]["username"] - password = self.config["dashboard"]["password"] - return ( - username == "astrbot" - and is_default_dashboard_password(password) - and not DEMO_MODE + async def is_default_cred(self): + password_change_required = await is_password_change_required( + self.db_helper, + self.config, ) + if password_change_required: + return not DEMO_MODE + + storage_upgraded = await is_password_storage_upgraded( + self.db_helper, + self.config, + ) + if not storage_upgraded: + return False + + username = self.config["dashboard"]["username"] + password = get_dashboard_password_hash(self.config, upgraded=True) + return ( + username == "astrbot" and is_default_dashboard_password(password) + ) and not DEMO_MODE async def get_version(self): need_migration = await check_migration_needed_v4(self.core_lifecycle.db) + storage_upgraded = await is_password_storage_upgraded( + self.db_helper, + self.config, + ) + password = get_dashboard_password_hash( + self.config, + upgraded=storage_upgraded, + ) return ( Response() @@ -93,10 +118,9 @@ class StatRoute(Route): { "version": VERSION, "dashboard_version": await get_dashboard_version(), - "change_pwd_hint": self.is_default_cred(), - "legacy_pwd_hint": is_legacy_dashboard_password( - self.config["dashboard"]["password"], - ), + "change_pwd_hint": await self.is_default_cred(), + "legacy_pwd_hint": is_legacy_dashboard_password(password), + "password_upgrade_required": not storage_upgraded, "need_migration": need_migration, }, ) diff --git a/astrbot/dashboard/routes/update.py b/astrbot/dashboard/routes/update.py index 80246fc5c..f633b6971 100644 --- a/astrbot/dashboard/routes/update.py +++ b/astrbot/dashboard/routes/update.py @@ -1,4 +1,5 @@ import traceback +import uuid from quart import request @@ -24,6 +25,7 @@ class UpdateRoute(Route): super().__init__(context) self.routes = { "/update/check": ("GET", self.check_update), + "/update/progress": ("GET", self.get_update_progress), "/update/releases": ("GET", self.get_releases), "/update/do": ("POST", self.update_project), "/update/dashboard": ("POST", self.update_dashboard), @@ -32,8 +34,104 @@ class UpdateRoute(Route): } self.astrbot_updator = astrbot_updator self.core_lifecycle = core_lifecycle + self.update_progress: dict[str, dict] = {} self.register_routes() + def _init_update_progress(self, progress_id: str, version: str) -> None: + self.update_progress[progress_id] = { + "id": progress_id, + "status": "running", + "stage": "preparing", + "version": version or "latest", + "message": "正在准备更新...", + "overall_percent": 0, + "stages": { + "dashboard": self._empty_stage("pending"), + "core": self._empty_stage("pending"), + }, + } + + @staticmethod + def _empty_stage(status: str = "pending") -> dict: + return { + "status": status, + "downloaded": 0, + "total": 0, + "percent": 0, + "speed": 0, + } + + def _set_update_stage( + self, + progress_id: str, + stage: str, + status: str, + message: str, + overall_percent: int | None = None, + ) -> None: + progress = self.update_progress.get(progress_id) + if not progress: + return + progress["stage"] = stage + progress["message"] = message + progress["stages"].setdefault(stage, self._empty_stage()) + progress["stages"][stage]["status"] = status + if overall_percent is not None: + progress["overall_percent"] = overall_percent + + @staticmethod + def _normalize_percent(value) -> int: + try: + percent = float(value or 0) + except (TypeError, ValueError): + return 0 + if percent <= 1: + percent *= 100 + return max(0, min(100, int(percent))) + + def _make_progress_callback( + self, + progress_id: str, + stage: str, + stage_start: int, + stage_weight: int, + ): + def _callback(payload: dict) -> None: + progress = self.update_progress.get(progress_id) + if not progress: + return + stage_percent = self._normalize_percent(payload.get("percent")) + progress["stage"] = stage + progress["stages"][stage] = { + "status": "running" if stage_percent < 100 else "done", + "downloaded": payload.get("downloaded", 0), + "total": payload.get("total", 0), + "percent": stage_percent, + "speed": payload.get("speed", 0), + } + progress["overall_percent"] = min( + 99, + stage_start + int(stage_percent * stage_weight / 100), + ) + + return _callback + + async def get_update_progress(self): + progress_id = request.args.get("id", "") + if not progress_id: + return Response().error("缺少参数 id。").__dict__ + progress = self.update_progress.get(progress_id) + if not progress: + return ( + Response() + .ok( + {"id": progress_id, "status": "idle"}, + "没有正在进行的更新。", + ) + .__dict__ + ) + return Response().ok(progress).__dict__ + async def do_migration(self): need_migration = await check_migration_needed_v4(self.core_lifecycle.db) if not need_migration: @@ -89,6 +187,7 @@ class UpdateRoute(Route): data = await request.json version = data.get("version", "") reboot = data.get("reboot", True) + progress_id = data.get("progress_id") or uuid.uuid4().hex if version == "" or version == "latest": latest = True version = "" @@ -99,33 +198,112 @@ class UpdateRoute(Route): if proxy: proxy = proxy.removesuffix("/") + self._init_update_progress(progress_id, version) try: + self._set_update_stage( + progress_id, + "dashboard", + "running", + "正在下载 WebUI...", + 0, + ) + await download_dashboard( + latest=latest, + version=version, + proxy=proxy, + progress_callback=self._make_progress_callback( + progress_id, + "dashboard", + 0, + 45, + ), + ) + self._set_update_stage( + progress_id, + "dashboard", + "done", + "WebUI 下载完成。", + 45, + ) + + self._set_update_stage( + progress_id, + "core", + "running", + "正在下载 AstrBot 项目代码...", + 45, + ) await self.astrbot_updator.update( latest=latest, version=version, proxy=proxy, + progress_callback=self._make_progress_callback( + progress_id, + "core", + 45, + 45, + ), + ) + self._set_update_stage( + progress_id, + "core", + "done", + "项目代码下载完成。", + 90, ) - try: - await download_dashboard(latest=latest, version=version, proxy=proxy) - except Exception as e: - logger.error(f"下载管理面板文件失败: {e}。") - # pip 更新依赖 + self._set_update_stage( + progress_id, + "dependencies", + "running", + "正在更新依赖...", + 92, + ) logger.info("更新依赖中...") try: await pip_installer.install(requirements_path="requirements.txt") except Exception as e: logger.error(f"更新依赖失败: {e}") + self._set_update_stage( + progress_id, + "dependencies", + "done", + "依赖更新完成。", + 96, + ) if reboot: + self._set_update_stage( + progress_id, + "restart", + "running", + "更新成功,正在准备重启...", + 98, + ) await self.core_lifecycle.restart() + self.update_progress[progress_id].update( + { + "status": "success", + "stage": "done", + "message": "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。", + "overall_percent": 100, + }, + ) ret = ( Response() .ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。") .to_json() ) return ret, 200, CLEAR_SITE_DATA_HEADERS + self.update_progress[progress_id].update( + { + "status": "success", + "stage": "done", + "message": "更新成功,AstrBot 将在下次启动时应用新的代码。", + "overall_percent": 100, + }, + ) ret = ( Response() .ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。") @@ -133,6 +311,12 @@ class UpdateRoute(Route): ) return ret, 200, CLEAR_SITE_DATA_HEADERS except Exception as e: + self.update_progress[progress_id].update( + { + "status": "error", + "message": e.__str__(), + }, + ) logger.error(f"/api/update_project: {traceback.format_exc()}") return Response().error(e.__str__()).to_json() diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index c2a7b6329..159c7d673 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -30,7 +30,11 @@ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db import BaseDatabase from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.datetime_utils import to_utc_isoformat -from astrbot.core.utils.io import get_local_ip_addresses +from astrbot.core.utils.io import ( + get_bundled_dashboard_dist_path, + get_local_ip_addresses, + should_use_bundled_dashboard_dist, +) from .plugin_page_auth import PluginPageAuth from .routes import ( @@ -66,9 +70,6 @@ from .routes.api_key import ALL_OPEN_API_SCOPES from .routes.auth import DASHBOARD_JWT_COOKIE_NAME from .routes.route import is_runtime_request_ready, runtime_loading_response -# Static assets shipped inside the wheel (built during `hatch build`). -_BUNDLED_DIST = Path(__file__).parent / "dist" - _PUBLIC_ALLOWED_ENDPOINT_PREFIXES = ( "/api/auth/login", "/api/file", @@ -129,7 +130,7 @@ def _match_registered_web_api(registered_web_apis, subpath: str, method: str): endpoint="plugin_api", methods=allowed_methods, ), - ] + ], ) try: _, path_values = url_map.bind("").match( @@ -237,12 +238,14 @@ class AstrBotDashboard: self.data_path = os.path.abspath(webui_dir) else: user_dist = os.path.join(get_astrbot_data_path(), "dist") - if os.path.exists(user_dist): + bundled_dist = get_bundled_dashboard_dist_path() + if os.path.exists(user_dist) and not should_use_bundled_dashboard_dist( + user_dist, + VERSION, + ): self.data_path = os.path.abspath(user_dist) - elif _BUNDLED_DIST.exists(): - # resolve() follows symlinks so self.data_path points to the - # actual directory, not the symlink itself. - self.data_path = str(_BUNDLED_DIST.resolve()) + elif bundled_dist.exists(): + self.data_path = str(bundled_dist) logger.info("Using bundled dashboard dist: %s", self.data_path) else: self.data_path = os.path.abspath(user_dist) @@ -343,7 +346,7 @@ class AstrBotDashboard: self.cr = ConfigRoute(self.context, self.core_lifecycle) self.lr = LogRoute(self.context, self.core_lifecycle.log_broker) self.sfr = StaticFileRoute(self.context) - self.ar = AuthRoute(self.context) + self.ar = AuthRoute(self.context, db) self.api_key_route = ApiKeyRoute(self.context, db) self.chat_route = ChatRoute(self.context, db, self.core_lifecycle) self.open_api_route = OpenApiRoute( @@ -496,15 +499,21 @@ class AstrBotDashboard: return guard_resp return None - allowed_endpoints = [ + allowed_exact_endpoints = { "/api/auth/login", "/api/auth/logout", + "/api/auth/setup-status", + "/api/auth/setup", + } + allowed_endpoint_prefixes = [ "/api/file", "/api/platform/webhook", "/api/stat/start-time", "/api/backup/download", # 备份下载使用 URL 参数传递 token ] - if any(request.path.startswith(prefix) for prefix in allowed_endpoints): + if request.path in allowed_exact_endpoints or any( + request.path.startswith(prefix) for prefix in allowed_endpoint_prefixes + ): guard_resp = self._maybe_runtime_guard( request.path, include_failure_details=False, @@ -523,7 +532,7 @@ class AstrBotDashboard: try: payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"]) if PluginPageAuth.is_asset_token( - payload + payload, ) and not PluginPageAuth.is_scope_valid( payload, request.path, @@ -630,6 +639,20 @@ class AstrBotDashboard: return f"获取进程信息失败: {e!s}" return "未知进程" + def _build_dashboard_credentials_display(self) -> str: + username = self.config["dashboard"].get("username", "astrbot") + generated_password = getattr(self.config, "_generated_dashboard_password", None) + if not generated_password: + return f" ➜ Username: {username}\n ✨✨✨\n" + + credentials_display = ( + f" ➜ Initial username: {username}\n" + f" ➜ Initial password: {generated_password}\n" + " ➜ Change it after logging in\n ✨✨✨\n" + ) + object.__setattr__(self.config, "_generated_dashboard_password", None) + return credentials_display + @staticmethod def _resolve_dashboard_ssl_config( ssl_config: dict, @@ -723,7 +746,7 @@ class AstrBotDashboard: if not self.enable_webui: logger.info("WebUI disabled.") - return None + return port_value = ( os.environ.get("DASHBOARD_PORT") @@ -769,7 +792,7 @@ class AstrBotDashboard: parts.append(f" ➜ Local: {scheme}://localhost:{port}\n") for ip in ip_addr: parts.append(f" ➜ Network: {scheme}://{ip}:{port}\n") - parts.append(" ➜ Default username/password: astrbot / astrbot\n ✨✨✨\n") + parts.append(self._build_dashboard_credentials_display()) display = "".join(parts) if not ip_addr: diff --git a/changelogs/v4.24.4.md b/changelogs/v4.24.4.md new file mode 100644 index 000000000..f58fd16bc --- /dev/null +++ b/changelogs/v4.24.4.md @@ -0,0 +1,62 @@ +- [更新日志(简体中文)](#chinese) +- [Changelog(English)](#english) + + + +## What's Changed + +### 优化 + +- 强化 Dashboard 登录与密码安全:首次启动生成强随机初始密码,密码存储升级为 PBKDF2,保留旧版 MD5 兼容升级流程,并在需要时引导用户完成安全升级。([#7338](https://github.com/AstrBotDevs/AstrBot/pull/7338)) +- 增强插件页面国际化能力,插件页面、扩展页和相关 Dashboard 文案可更好地按当前语言展示。([#7998](https://github.com/AstrBotDevs/AstrBot/pull/7998)) +- 新增 WebUI 指标开关配置 `disable_metrics`,可在 Dashboard 中关闭指标统计。([#7946](https://github.com/AstrBotDevs/AstrBot/pull/7946)) +- 新增控制台自动滚动开关持久化,刷新页面后保留用户选择。([#8024](https://github.com/AstrBotDevs/AstrBot/pull/8024)) +- 新增思考内容与最终回复之间的视觉分隔,提升消息阅读体验。([#8059](https://github.com/AstrBotDevs/AstrBot/pull/8059)) +- 优化插件安装、备份恢复与路径冲突处理,增加自愈逻辑并减少临时目录残留和错误追踪误报。([#7737](https://github.com/AstrBotDevs/AstrBot/pull/7737), [#8148](https://github.com/AstrBotDevs/AstrBot/pull/8148)) +- 优化 Windows 更新器 zip 根目录归一化与 Python 工具编码处理,提升 Windows 环境兼容性。([#8019](https://github.com/AstrBotDevs/AstrBot/pull/8019)) +- 优化 CUA 文件上传逻辑,改用原生文件接口处理上传。([#8069](https://github.com/AstrBotDevs/AstrBot/pull/8069)) +- 优化 CUA 空闲沙盒会话过期能力,并在 Dashboard 暴露 CUA idle timeout 配置。([#8074](https://github.com/AstrBotDevs/AstrBot/pull/8074), [#8075](https://github.com/AstrBotDevs/AstrBot/pull/8075)) +- 优化 Gemini Provider,使其使用受管理的 httpx client。([#8112](https://github.com/AstrBotDevs/AstrBot/pull/8112)) +- 优化 Dashboard 移动端布局、控制台日志级别对齐,以及列表项操作按钮显示逻辑。([#7988](https://github.com/AstrBotDevs/AstrBot/pull/7988), [#8081](https://github.com/AstrBotDevs/AstrBot/pull/8081)) + +### 修复 + +- 修复知识库在空 prompt 下仍触发检索的问题。([#8073](https://github.com/AstrBotDevs/AstrBot/pull/8073)) +- 修复 Discord 命令同步达到配额时会影响平台启动的问题。([#8061](https://github.com/AstrBotDevs/AstrBot/pull/8061)) +- 修复 GitHub fallback 下载 URL 中资源文件名错误的问题。([#8046](https://github.com/AstrBotDevs/AstrBot/pull/8046)) +- 修复文件夹重命名后父级关系丢失的问题。([#7974](https://github.com/AstrBotDevs/AstrBot/pull/7974)) +- 修复配置缺失 `websearch_firecrawl_key`,以及百度搜索关闭时仍显示 key 字段的问题。([#8012](https://github.com/AstrBotDevs/AstrBot/pull/8012), [#7992](https://github.com/AstrBotDevs/AstrBot/pull/7992)) +- 修复 T2I 模板内容未校验可能导致 Jinja2 SSTI 注入的问题。([#8077](https://github.com/AstrBotDevs/AstrBot/pull/8077)) +- 修复贡献者图片数量上限、API Key 文档示例、插件发布 16MB 限制说明、README 徽章和多处插件开发文档错误。([#8000](https://github.com/AstrBotDevs/AstrBot/pull/8000), [#7977](https://github.com/AstrBotDevs/AstrBot/pull/7977), [#8108](https://github.com/AstrBotDevs/AstrBot/pull/8108), [#8079](https://github.com/AstrBotDevs/AstrBot/pull/8079), [#7979](https://github.com/AstrBotDevs/AstrBot/pull/7979), [#8001](https://github.com/AstrBotDevs/AstrBot/pull/8001), [#8129](https://github.com/AstrBotDevs/AstrBot/pull/8129), [#8166](https://github.com/AstrBotDevs/AstrBot/pull/8166)) + + + +## What's Changed (EN) + +### New Features + +- Added plugin changelogs and a plugin update system, allowing plugin detail pages to show version update information and supporting a more complete plugin update flow. +- Enhanced plugin page internationalization so plugin pages, extension views, and related Dashboard copy can better follow the current language. ([#7998](https://github.com/AstrBotDevs/AstrBot/pull/7998)) +- Added the `disable_metrics` WebUI config option to disable metrics collection from the Dashboard. ([#7946](https://github.com/AstrBotDevs/AstrBot/pull/7946)) +- Added persisted console auto-scroll preference. ([#8024](https://github.com/AstrBotDevs/AstrBot/pull/8024)) +- Added a visual separator between thinking content and the final response. ([#8059](https://github.com/AstrBotDevs/AstrBot/pull/8059)) +- Added idle expiration for CUA sandbox sessions and exposed the CUA idle timeout setting in the Dashboard. ([#8074](https://github.com/AstrBotDevs/AstrBot/pull/8074), [#8075](https://github.com/AstrBotDevs/AstrBot/pull/8075)) + +### Improvements + +- Strengthened Dashboard authentication and password security: initial passwords are generated randomly, password storage is upgraded to PBKDF2, legacy MD5 compatibility is preserved for upgrades, and users are guided through security upgrades when required. ([#7338](https://github.com/AstrBotDevs/AstrBot/pull/7338)) +- Improved plugin installation, backup restore, and path-conflict handling with self-healing behavior and fewer temporary-directory leftovers or false error reports. ([#7737](https://github.com/AstrBotDevs/AstrBot/pull/7737), [#8148](https://github.com/AstrBotDevs/AstrBot/pull/8148)) +- Improved Windows updater zip-root normalization and Python tool encoding handling for better Windows compatibility. ([#8019](https://github.com/AstrBotDevs/AstrBot/pull/8019)) +- Improved CUA uploads by using native file interfaces. ([#8069](https://github.com/AstrBotDevs/AstrBot/pull/8069)) +- Improved the Gemini Provider to use a managed httpx client. ([#8112](https://github.com/AstrBotDevs/AstrBot/pull/8112)) +- Improved Dashboard mobile layout, console log-level alignment, and list-item action-button visibility. ([#7988](https://github.com/AstrBotDevs/AstrBot/pull/7988), [#8081](https://github.com/AstrBotDevs/AstrBot/pull/8081)) + +### Bug Fixes + +- Fixed missing validation for T2I template content that could allow Jinja2 SSTI injection. ([#8077](https://github.com/AstrBotDevs/AstrBot/pull/8077)) +- Fixed knowledge base retrieval being triggered for blank prompts. ([#8073](https://github.com/AstrBotDevs/AstrBot/pull/8073)) +- Fixed Discord startup being interrupted by command quota handling. ([#8061](https://github.com/AstrBotDevs/AstrBot/pull/8061)) +- Fixed incorrect asset filenames in GitHub fallback download URLs. ([#8046](https://github.com/AstrBotDevs/AstrBot/pull/8046)) +- Fixed folder parent relationships being lost on rename. ([#7974](https://github.com/AstrBotDevs/AstrBot/pull/7974)) +- Fixed missing `websearch_firecrawl_key` in the default config and hidden Baidu web-search keys when disabled. ([#8012](https://github.com/AstrBotDevs/AstrBot/pull/8012), [#7992](https://github.com/AstrBotDevs/AstrBot/pull/7992)) +- Fixed contributor image limits, API Key examples, plugin publishing size-limit docs, README badges, and multiple plugin development guide issues. ([#8000](https://github.com/AstrBotDevs/AstrBot/pull/8000), [#7977](https://github.com/AstrBotDevs/AstrBot/pull/7977), [#8108](https://github.com/AstrBotDevs/AstrBot/pull/8108), [#8079](https://github.com/AstrBotDevs/AstrBot/pull/8079), [#7979](https://github.com/AstrBotDevs/AstrBot/pull/7979), [#8001](https://github.com/AstrBotDevs/AstrBot/pull/8001), [#8129](https://github.com/AstrBotDevs/AstrBot/pull/8129), [#8166](https://github.com/AstrBotDevs/AstrBot/pull/8166)) diff --git a/changelogs/v4.24.5.md b/changelogs/v4.24.5.md new file mode 100644 index 000000000..6ef1b512a --- /dev/null +++ b/changelogs/v4.24.5.md @@ -0,0 +1,42 @@ +- [更新日志(简体中文)](#chinese) +- [Changelog(English)](#english) + + + +## What's Changed + +### 优化 + +- Dashboard 更新流程新增两阶段下载进度,升级项目时会先下载 WebUI,再下载项目代码,并在界面中展示每个阶段的进度。 +- 新增更新后重启等待体验:WebUI 会在更新前记录 AstrBot 启动时间,更新完成后展示正在重启状态,并轮询启动时间变化后自动刷新页面。 +- 新增 `ASTRBOT_DASHBOARD_INITIAL_PASSWORD` 环境变量,可为首次生成的 Dashboard 密码指定初始值。 +- `astrbot init` 现在会识别 `ASTRBOT_DASHBOARD_INITIAL_PASSWORD`,并在初始化阶段创建 `data/cmd_config.json` 写入哈希后的初始密码,便于自动化部署。 +- 优化 Dashboard 更新项目弹窗,减少对用户无用的信息,保留独立更新 WebUI 到最新版本的兜底入口,并将其收起到高级设置中。 +- 优化 Release 列表加载状态和预发布版本提示逻辑:Release 未加载完成时显示表格 loading,第一页没有预发布版本时不再显示提醒。 +- 优化升级后旧版 Dashboard 密码登录失败时的提示,引导用户参考 FAQ 处理升级后密码正确但无法登录的情况。 +- 更新 FAQ 文档,补充升级后密码正确但无法登录时可删除 `data/dist` 后重启 AstrBot 的处理方案。 + +### 修复 + +- 修复 Shipyard Neo 在显式配置 profile 时可能未正确尊重该配置的问题。([#8167](https://github.com/AstrBotDevs/AstrBot/pull/8167)) +- 修复 `message_tools` 在目标路径不存在时未抛出异常并阻止消息发送的问题。([#8149](https://github.com/AstrBotDevs/AstrBot/pull/8149)) + + + +## What's Changed (EN) + +### Improvements + +- Added two-stage download progress to the Dashboard update flow. Project upgrades now download the WebUI first, then the core project code, with per-stage progress shown in the UI. +- Added a restart-waiting experience after updates. The WebUI records AstrBot's start time before updating, shows a restarting state after the update completes, polls for a changed start time, and refreshes automatically. +- Added the `ASTRBOT_DASHBOARD_INITIAL_PASSWORD` environment variable to specify the first generated Dashboard password. +- `astrbot init` now recognizes `ASTRBOT_DASHBOARD_INITIAL_PASSWORD` and creates `data/cmd_config.json` during initialization with the hashed initial password, making automated deployments easier. +- Improved the Dashboard project update dialog by hiding low-value details, keeping the standalone WebUI update fallback, and moving it under Advanced Settings. +- Improved Release list loading and prerelease notices: the table now shows a loading state before releases are loaded, and the prerelease warning is hidden when the first page has no prerelease entries. +- Improved the login failure message for legacy Dashboard password upgrade cases, guiding users to the FAQ when a correct password no longer works after upgrading. +- Updated the FAQ with recovery steps for upgrade cases where the correct password cannot log in: delete `data/dist` and restart AstrBot. + +### Bug Fixes + +- Fixed Shipyard Neo so explicit profile configuration is respected. ([#8167](https://github.com/AstrBotDevs/AstrBot/pull/8167)) +- Fixed `message_tools` so missing target paths raise an exception and block message sending. ([#8149](https://github.com/AstrBotDevs/AstrBot/pull/8149)) diff --git a/changelogs/v4.25.0.md b/changelogs/v4.25.0.md new file mode 100644 index 000000000..29e1745be --- /dev/null +++ b/changelogs/v4.25.0.md @@ -0,0 +1,61 @@ +- [更新日志(简体中文)](#chinese) +- [Changelog(English)](#english) + + + +## What's Changed + +### 修复 + +- 修复 Tencent SILK 音频带 `\x02` 前缀时未被识别,导致后续 ffmpeg 转换失败的问题。([#8009](https://github.com/AstrBotDevs/AstrBot/pull/8009)) +- 修复个人微信媒体消息发送失败时错误未向上暴露的问题。([#8175](https://github.com/AstrBotDevs/AstrBot/pull/8175)) +- 修复 Claude API 对无参数工具返回 `None` 参数时工具调用失败的问题。([#8136](https://github.com/AstrBotDevs/AstrBot/pull/8136)) +- 修复 `register_platform_adapter_type` 与 `register_permission_type` 未正确透传 `**kwargs` 的问题。([#8141](https://github.com/AstrBotDevs/AstrBot/pull/8141)) +- 修复 MiniMax TTS timber weight 配置为空或非法 JSON 时可能导致初始化崩溃的问题。 + +### 新功能 + +- 新增 Ollama Embedding 与 NVIDIA NIM Embedding 提供商适配器。([#8104](https://github.com/AstrBotDevs/AstrBot/pull/8104)) +- 新增飞书扫码一键创建能力。([#8191](https://github.com/AstrBotDevs/AstrBot/pull/8191)) +- 新增钉钉扫码一键创建能力。([#8198](https://github.com/AstrBotDevs/AstrBot/pull/8198)) +- 新增个人微信创建时扫码登录流程,选择个人微信后直接在创建弹窗中显示二维码,登录成功后再保存机器人配置。([#8196](https://github.com/AstrBotDevs/AstrBot/pull/8196)) + +### 优化 + +- 优化个人微信登录态处理:当接口返回 session timeout 时清理旧登录态,避免继续高频请求失效会话。([#8196](https://github.com/AstrBotDevs/AstrBot/pull/8196)) +- 优化 AMR 音频转换质量,并复用通用音频转换逻辑简化 Opus 转换实现。([#8153](https://github.com/AstrBotDevs/AstrBot/pull/8153)) +- 优化 WebUI 资源选择逻辑:当 `data/dist` 中的 Dashboard 版本旧于当前核心版本时,优先使用随包内置的 Dashboard,避免旧前端资源造成兼容问题。([#8172](https://github.com/AstrBotDevs/AstrBot/pull/8172)) +- Dashboard 新增 Noto Sans 字体支持,改善西里尔文字等多语言文本显示效果。([#8015](https://github.com/AstrBotDevs/AstrBot/pull/8015)) +- 优化控制台自动滚动状态同步,刷新后能正确恢复并同步到日志显示组件。([#8186](https://github.com/AstrBotDevs/AstrBot/pull/8186)) +- 更新飞书、钉钉、个人微信平台文档,补充扫码创建 / 登录说明、飞书权限开通提醒,以及个人微信新版创建流程。 +- 更新 AI 插件开发文档,明确 `context.register_llm_tool()` 已弃用,并补充 LLM Tool docstring 参数解析要求。([#8178](https://github.com/AstrBotDevs/AstrBot/pull/8178)) + + + + +## What's Changed (EN) + +### New Features + +- Added one-click QR setup for Feishu / Lark. When creating a bot, users can choose Feishu China or Lark Global, scan the QR code, and automatically fill `app_id`, `app_secret`, and domain settings. AstrBot also fetches the bot name for the generated config ID display. ([#8191](https://github.com/AstrBotDevs/AstrBot/pull/8191)) +- Added one-click QR setup for DingTalk. Users can create or bind a DingTalk app by scanning a QR code, automatically filling `ClientID` and `ClientSecret`, while manual setup remains available. ([#8198](https://github.com/AstrBotDevs/AstrBot/pull/8198)) +- Added QR login during Personal WeChat bot creation. The creation dialog now shows the login QR code directly and allows saving after login succeeds. ([#8196](https://github.com/AstrBotDevs/AstrBot/pull/8196)) +- Added Ollama Embedding and NVIDIA NIM Embedding provider adapters. ([#8104](https://github.com/AstrBotDevs/AstrBot/pull/8104)) + +### Improvements + +- Improved Personal WeChat login-state handling by clearing stale login state when the API reports session timeout, avoiding repeated requests against an invalid session. ([#8196](https://github.com/AstrBotDevs/AstrBot/pull/8196)) +- Improved AMR audio conversion quality and simplified Opus conversion by reusing the shared audio conversion path. ([#8153](https://github.com/AstrBotDevs/AstrBot/pull/8153)) +- Improved WebUI asset selection: when `data/dist` contains a Dashboard older than the current core version, AstrBot now prefers the bundled Dashboard to avoid stale frontend compatibility issues. ([#8172](https://github.com/AstrBotDevs/AstrBot/pull/8172)) +- Added Noto Sans support to the Dashboard for better Cyrillic and multilingual text rendering. ([#8015](https://github.com/AstrBotDevs/AstrBot/pull/8015)) +- Improved console auto-scroll state synchronization so the restored preference is applied to the log display component after refresh. ([#8186](https://github.com/AstrBotDevs/AstrBot/pull/8186)) +- Updated Feishu / Lark, DingTalk, and Personal WeChat platform docs with QR setup / login instructions, Feishu permission reminders, and the new Personal WeChat creation flow. +- Updated the AI plugin development guide to clarify that `context.register_llm_tool()` is deprecated and to document the required LLM Tool docstring argument format. ([#8178](https://github.com/AstrBotDevs/AstrBot/pull/8178)) + +### Bug Fixes + +- Fixed Tencent SILK audio with a leading `\x02` byte not being detected, which could cause ffmpeg conversion failures. ([#8009](https://github.com/AstrBotDevs/AstrBot/pull/8009)) +- Fixed Personal WeChat media send failures not being surfaced properly. ([#8175](https://github.com/AstrBotDevs/AstrBot/pull/8175)) +- Fixed tool calls failing when the Claude API returns `None` arguments for no-parameter tools. ([#8136](https://github.com/AstrBotDevs/AstrBot/pull/8136)) +- Fixed `register_platform_adapter_type` and `register_permission_type` not forwarding `**kwargs` correctly. ([#8141](https://github.com/AstrBotDevs/AstrBot/pull/8141)) +- Fixed MiniMax TTS initialization crashes when timber weight config is empty or invalid JSON. diff --git a/changelogs/v4.25.1.md b/changelogs/v4.25.1.md new file mode 100644 index 000000000..ed617c871 --- /dev/null +++ b/changelogs/v4.25.1.md @@ -0,0 +1,26 @@ +- [更新日志(简体中文)](#chinese) +- [Changelog(English)](#english) + + + +## What's Changed + +### 优化 + +- 个人微信和钉钉扫码创建成功后,平台配置 ID 会自动追加随机 4 位小写字母后缀,例如 `_abcd`,降低多个扫码创建配置之间的 ID 冲突概率。 + +### 修复 + +- 修复 WebUI 全局字体栈未完整覆盖西里尔文字场景的问题,改善俄文等多语言文本显示效果。([#8205](https://github.com/AstrBotDevs/AstrBot/pull/8205)) + + + +## What's Changed (EN) + +### Improvements + +- Personal WeChat and DingTalk QR setup now append a random four-letter lowercase suffix to the generated platform config ID, such as `_abcd`, reducing ID conflicts across multiple scan-created configs. + +### Bug Fixes + +- Fixed the WebUI global font-family stack so Cyrillic text and other multilingual content render more consistently. ([#8205](https://github.com/AstrBotDevs/AstrBot/pull/8205)) diff --git a/dashboard/index.html b/dashboard/index.html index 8961a40cd..0cb9ec8fc 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -9,7 +9,7 @@ diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index 75721b2fb..df5c50503 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -4,162 +4,180 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + immutable: 4.3.8 + lodash-es: 4.17.23 + importers: .: dependencies: '@guolao/vue-monaco-editor': - specifier: ^1.6.0 - version: 1.6.0(monaco-editor@0.55.1)(vue@3.5.31(typescript@6.0.3)) + specifier: ^1.5.4 + version: 1.6.0(monaco-editor@0.52.2)(vue@3.3.4) '@tiptap/starter-kit': - specifier: 3.20.5 - version: 3.20.5 + specifier: 2.1.7 + version: 2.1.7(@tiptap/pm@2.27.2) '@tiptap/vue-3': - specifier: 3.20.5 - version: 3.20.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(vue@3.5.31(typescript@6.0.3)) + specifier: 2.1.7 + version: 2.1.7(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(vue@3.3.4) apexcharts: - specifier: 5.10.4 - version: 5.10.4 + specifier: 3.42.0 + version: 3.42.0 axios: - specifier: 1.13.6 - version: 1.13.6 + specifier: 1.13.5 + version: 1.13.5 axios-mock-adapter: - specifier: ^2.1.0 - version: 2.1.0(axios@1.13.6) + specifier: ^1.22.0 + version: 1.22.0(axios@1.13.5) chance: - specifier: 1.1.13 - version: 1.1.13 - d3: - specifier: ^7.9.0 - version: 7.9.0 + specifier: 1.1.11 + version: 1.1.11 date-fns: - specifier: 4.1.0 - version: 4.1.0 + specifier: 2.30.0 + version: 2.30.0 dompurify: - specifier: ^3.3.3 - version: 3.4.1 + specifier: ^3.3.2 + version: 3.3.2 event-source-polyfill: specifier: ^1.0.31 version: 1.0.31 highlight.js: - specifier: ^11.11.1 + specifier: 11.11.1 version: 11.11.1 - js-md5: - specifier: ^0.8.3 - version: 0.8.3 katex: - specifier: ^0.16.44 - version: 0.16.45 + specifier: ^0.16.27 + version: 0.16.28 lodash: - specifier: 4.18.1 - version: 4.18.1 + specifier: 4.17.23 + version: 4.17.23 markdown-it: specifier: ^14.1.1 version: 14.1.1 markstream-vue: - specifier: ^0.0.9 - version: 0.0.9(katex@0.16.45)(mermaid@11.14.0)(shiki@3.23.0)(stream-markdown@0.0.14(shiki@3.23.0))(vue-i18n@11.3.2(vue@3.5.31(typescript@6.0.3)))(vue@3.5.31(typescript@6.0.3)) + 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) mermaid: - specifier: ^11.14.0 - version: 11.14.0 + specifier: ^11.12.2 + version: 11.12.2 monaco-editor: - specifier: ^0.55.1 - version: 0.55.1 + specifier: ^0.52.2 + version: 0.52.2 pinia: - specifier: ^3.0.4 - version: 3.0.4(typescript@6.0.3)(vue@3.5.31(typescript@6.0.3)) + specifier: 2.1.6 + version: 2.1.6(typescript@5.1.6)(vue@3.3.4) pinyin-pro: - specifier: ^3.28.0 + specifier: ^3.26.0 version: 3.28.0 qrcode: specifier: ^1.5.4 version: 1.5.4 shiki: - specifier: ^3.23.0 - version: 3.23.0 + specifier: ^3.20.0 + version: 3.22.0 stream-markdown: - specifier: ^0.0.14 - version: 0.0.14(shiki@3.23.0) + specifier: ^0.0.13 + version: 0.0.13(shiki@3.22.0) vee-validate: - specifier: 4.15.1 - version: 4.15.1(vue@3.5.31(typescript@6.0.3)) + specifier: 4.11.3 + version: 4.11.3(vue@3.3.4) vite-plugin-vuetify: specifier: 2.1.3 - version: 2.1.3(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3))(vue@3.5.31(typescript@6.0.3))(vuetify@4.0.4) + 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.5.31 - version: 3.5.31(typescript@6.0.3) + specifier: 3.3.4 + version: 3.3.4 vue-i18n: - specifier: ^11.3.0 - version: 11.3.2(vue@3.5.31(typescript@6.0.3)) + specifier: ^11.1.5 + version: 11.2.8(vue@3.3.4) vue-router: - specifier: 5.0.4 - version: 5.0.4(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.31(typescript@6.0.3)))(vue@3.5.31(typescript@6.0.3)) + specifier: 4.2.4 + version: 4.2.4(vue@3.3.4) vue3-apexcharts: - specifier: 1.11.1 - version: 1.11.1(apexcharts@5.10.4)(vue@3.5.31(typescript@6.0.3)) + specifier: 1.4.4 + version: 1.4.4(apexcharts@3.42.0)(vue@3.3.4) vue3-print-nb: specifier: 0.1.4 - version: 0.1.4(typescript@6.0.3) + version: 0.1.4 vuetify: - specifier: 4.0.4 - version: 4.0.4(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.31(typescript@6.0.3)) + specifier: 3.7.11 + version: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4) yup: - specifier: 1.7.1 - version: 1.7.1 + specifier: 1.2.0 + version: 1.2.0 devDependencies: - '@biomejs/biome': - specifier: 2.4.13 - version: 2.4.13 '@mdi/font': - specifier: 7.4.47 - version: 7.4.47 + specifier: 7.2.96 + version: 7.2.96 + '@rushstack/eslint-patch': + specifier: 1.3.3 + version: 1.3.3 '@types/chance': - specifier: 1.1.7 - version: 1.1.7 + specifier: 1.1.3 + version: 1.1.3 + '@types/dompurify': + specifier: ^3.0.5 + version: 3.2.0 '@types/markdown-it': specifier: ^14.1.2 version: 14.1.2 '@types/node': - specifier: ^25.5.2 - version: 25.6.0 + specifier: ^20.5.7 + version: 20.19.32 '@vitejs/plugin-vue': - specifier: 6.0.5 - version: 6.0.5(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3))(vue@3.5.31(typescript@6.0.3)) + 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) + '@vue/eslint-config-typescript': + specifier: 11.0.3 + version: 11.0.3(eslint-plugin-vue@9.17.0(eslint@8.48.0))(eslint@8.48.0)(typescript@5.1.6) '@vue/tsconfig': - specifier: ^0.9.1 - version: 0.9.1(typescript@6.0.3)(vue@3.5.31(typescript@6.0.3)) + specifier: ^0.4.0 + version: 0.4.0 + eslint: + specifier: 8.48.0 + version: 8.48.0 + eslint-plugin-vue: + specifier: 9.17.0 + version: 9.17.0(eslint@8.48.0) + prettier: + specifier: 3.0.2 + version: 3.0.2 sass: - specifier: 1.98.0 - version: 1.98.0 + specifier: 1.66.1 + version: 1.66.1 sass-loader: - specifier: 16.0.7 - version: 16.0.7(sass@1.98.0)(webpack@5.105.0) + specifier: 13.3.2 + version: 13.3.2(sass@1.66.1)(webpack@5.105.0) subset-font: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^2.4.0 + version: 2.4.0 typescript: - specifier: ~6.0.2 - version: 6.0.3 + specifier: 5.1.6 + version: 5.1.6 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3) + specifier: 6.4.1 + version: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) vite-plugin-webfont-dl: specifier: ^3.12.0 - version: 3.12.0(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3)) + version: 3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)) + vue-cli-plugin-vuetify: + specifier: 2.5.8 + version: 2.5.8(sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0))(vue@3.3.4)(vuetify-loader@2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0))(webpack@5.105.0) vue-tsc: - specifier: ^3.2.6 - version: 3.2.7(typescript@6.0.3) + specifier: 1.8.8 + version: 1.8.8(typescript@5.1.6) + vuetify-loader: + specifier: ^2.0.0-alpha.9 + version: 2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0) packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -173,72 +191,14 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} - engines: {node: '>=6.0.0'} - hasBin: true + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.4.13': - resolution: {integrity: sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.4.13': - resolution: {integrity: sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.4.13': - resolution: {integrity: sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.4.13': - resolution: {integrity: sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-arm64@2.4.13': - resolution: {integrity: sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-linux-x64-musl@2.4.13': - resolution: {integrity: sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-x64@2.4.13': - resolution: {integrity: sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-win32-arm64@2.4.13': - resolution: {integrity: sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.4.13': - resolution: {integrity: sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -248,48 +208,204 @@ packages: '@cacheable/utils@2.4.0': resolution: {integrity: sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==} - '@chevrotain/cst-dts-gen@12.0.0': - resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} - '@chevrotain/gast@12.0.0': - resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} - '@chevrotain/regexp-to-ast@12.0.0': - resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} - '@chevrotain/types@12.0.0': - resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} - '@chevrotain/utils@12.0.0': - resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@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.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@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.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@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.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@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-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/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.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.48.0': + resolution: {integrity: sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - '@floating-ui/dom@1.7.5': resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@guolao/vue-monaco-editor@1.6.0': resolution: {integrity: sha512-w2IiJ6eJGGeuIgCK6EKZOAfhHTTUB5aZwslzwGbZ5e89Hb4avx6++GkLTW8p84Sng/arFMjLPPxSBI56cFudyQ==} peerDependencies: @@ -300,34 +416,40 @@ packages: '@vue/composition-api': optional: true + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} '@iconify/utils@3.1.0': resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} - '@intlify/core-base@11.3.2': - resolution: {integrity: sha512-cgsUaV/dyD6aS49UPgerIblrWeXAZHNaDWqm4LujOGC7IafSyhghGXEiSVvuDYaDPiQTP+tSFSTM1HIu7Yp1nA==} + '@intlify/core-base@11.2.8': + resolution: {integrity: sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==} engines: {node: '>= 16'} - '@intlify/devtools-types@11.3.2': - resolution: {integrity: sha512-q96G2ZZw0FNoXzejbjIf9dbfgz1xyYBZu6ZT4b5TE/55j8d1O9X5jv0k+U+L3fVe7uebPcqRQFD0ffm30i5mJA==} + '@intlify/message-compiler@11.2.8': + resolution: {integrity: sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==} engines: {node: '>= 16'} - '@intlify/message-compiler@11.3.2': - resolution: {integrity: sha512-d/awyHUkNSaGPxBxT/qlUpfRizxHX9dt55CnW03xx5p1KmMyfYHKupCnvzINX+Na8JR8LAR7y32lPKjoeQGmzA==} - engines: {node: '>= 16'} - - '@intlify/shared@11.3.2': - resolution: {integrity: sha512-x66fjdH6i+lNYPae5URSQGTjBL68Av6hi09jvC5Ci96iTkwfqrPhCj46aylQZmgMaG89rOZCIKqS7ApC8ZDVjg==} + '@intlify/shared@11.2.8': + resolution: {integrity: sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==} engines: {node: '>= 16'} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -350,386 +472,329 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@mdi/font@7.4.47': - resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==} + '@mdi/font@7.2.96': + resolution: {integrity: sha512-e//lmkmpFUMZKhmCY9zdjRe4zNXfbOIJnn6xveHbaV2kSw5aJ5dLXUxcRt1Gxfi7ZYpFLUWlkG2MGSFAiqAu7w==} - '@mermaid-js/parser@1.1.0': - resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} '@monaco-editor/loader@1.7.0': resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} - '@oxc-project/types@0.122.0': - resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@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] - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} cpu: [arm64] os: [darwin] - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} cpu: [x64] os: [darwin] - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} + '@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] - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] libc: [glibc] - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] libc: [musl] - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] libc: [glibc] - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] libc: [musl] - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] os: [linux] libc: [glibc] - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] os: [linux] libc: [musl] - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} - - '@rolldown/binding-android-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} - engines: {node: ^20.19.0 || >=22.12.0} + '@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] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} - engines: {node: ^20.19.0 || >=22.12.0} + '@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] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@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] - '@rolldown/pluginutils@1.0.0-rc.12': - resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] - '@rolldown/pluginutils@1.0.0-rc.2': - resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + '@rushstack/eslint-patch@1.3.3': + resolution: {integrity: sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==} - '@shikijs/core@3.23.0': - resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + '@shikijs/core@3.22.0': + resolution: {integrity: sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==} - '@shikijs/engine-javascript@3.23.0': - resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + '@shikijs/engine-javascript@3.22.0': + resolution: {integrity: sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==} - '@shikijs/engine-oniguruma@3.23.0': - resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + '@shikijs/engine-oniguruma@3.22.0': + resolution: {integrity: sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==} - '@shikijs/langs@3.23.0': - resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + '@shikijs/langs@3.22.0': + resolution: {integrity: sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==} - '@shikijs/themes@3.23.0': - resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + '@shikijs/monaco@3.22.0': + resolution: {integrity: sha512-4Bi/Gr5+ZVGmILq4ksyWtNbylfHxYB0BDMLR76UsaKOkWNJ/1w+c2s7bIjYnBNydyLVRlp28qntqEvB9EaJu3g==} - '@shikijs/types@3.23.0': - resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + '@shikijs/themes@3.22.0': + resolution: {integrity: sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==} + + '@shikijs/types@3.22.0': + resolution: {integrity: sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@tiptap/core@3.22.4': - resolution: {integrity: sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ==} + '@tiptap/core@2.27.2': + resolution: {integrity: sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==} peerDependencies: - '@tiptap/pm': 3.22.4 + '@tiptap/pm': ^2.7.0 - '@tiptap/extension-blockquote@3.22.4': - resolution: {integrity: sha512-7/61kNPbGFhMgM//zMknD0pSb69rGdRIkpulXOWS1JBrFHkH6hjZDfrOETNzgKkO+NlmzVl9rXSTv0xauS3lzA==} + '@tiptap/extension-blockquote@2.27.2': + resolution: {integrity: sha512-oIGZgiAeA4tG3YxbTDfrmENL4/CIwGuP3THtHsNhwRqwsl9SfMk58Ucopi2GXTQSdYXpRJ0ahE6nPqB5D6j/Zw==} peerDependencies: - '@tiptap/core': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-bold@3.22.4': - resolution: {integrity: sha512-jIaPKfNOQu2lhpbLDvtwlQqM+mjF+Kk+auHpzYjBnsuwUli1Cl5ZOau7RH+rru/SQvZe1DtpQlANujDywugZAA==} + '@tiptap/extension-bold@2.27.2': + resolution: {integrity: sha512-bR7J5IwjCGQ0s3CIxyMvOCnMFMzIvsc5OVZKscTN5UkXzFsaY6muUAIqtKxayBUucjtUskm5qZowJITCeCb1/A==} peerDependencies: - '@tiptap/core': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-bubble-menu@3.22.4': - resolution: {integrity: sha512-v4pux5Ql3THAEjaLMY4ldtdy/Xy2qU7PJLBkq8ugLp8qicaKC+tpqxp6sGif4vLIjz7Ap5hurRbTNbXzszyyHA==} + '@tiptap/extension-bubble-menu@2.27.2': + resolution: {integrity: sha512-VkwlCOcr0abTBGzjPXklJ92FCowG7InU8+Od9FyApdLNmn0utRYGRhw0Zno6VgE9EYr1JY4BRnuSa5f9wlR72w==} peerDependencies: - '@tiptap/core': 3.22.4 - '@tiptap/pm': 3.22.4 + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 - '@tiptap/extension-bullet-list@3.22.4': - resolution: {integrity: sha512-TB+d3fGcTixYjO7coKqTr1mGTJuqr8hjDCPUFgzuvKyJnBhqWITmBzQ/8CLq4rr6mihgGURbD3N+xkQuPAKFiw==} + '@tiptap/extension-bullet-list@2.27.2': + resolution: {integrity: sha512-gmFuKi97u5f8uFc/GQs+zmezjiulZmFiDYTh3trVoLRoc2SAHOjGEB7qxdx7dsqmMN7gwiAWAEVurLKIi1lnnw==} peerDependencies: - '@tiptap/extension-list': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-code-block@3.22.4': - resolution: {integrity: sha512-MEurzNXfMET3rhjpoPJYUgMfxTdTqbzT9+ToFrqNGAHocdXVm6m1hhO2frVC7fEtHPnxXKsn0Z3NUbCRkRTLuA==} + '@tiptap/extension-code-block@2.27.2': + resolution: {integrity: sha512-KgvdQHS4jXr79aU3wZOGBIZYYl9vCB7uDEuRFV4so2rYrfmiYMw3T8bTnlNEEGe4RUeAms1i4fdwwvQp9nR1Dw==} peerDependencies: - '@tiptap/core': 3.22.4 - '@tiptap/pm': 3.22.4 + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 - '@tiptap/extension-code@3.22.4': - resolution: {integrity: sha512-cnbxmVhAcc7X3G81QUYEmKP0ve2hRmvAiFXBuuv9RUtQlBiRnzmhHoJOMgkX0CsMR7+8kMRpTfeDUYq2xp5s5w==} + '@tiptap/extension-code@2.27.2': + resolution: {integrity: sha512-7X9AgwqiIGXoZX7uvdHQsGsjILnN/JaEVtqfXZnPECzKGaWHeK/Ao4sYvIIIffsyZJA8k5DC7ny2/0sAgr2TuA==} peerDependencies: - '@tiptap/core': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-document@3.22.4': - resolution: {integrity: sha512-XQKla1+703FqQJC48tPDVgt9ucGiFbIEmQdOg5L5o07z9a6/NzuaZAc+1zJ7NxcUZzy+z6wBn1PrVMTiqiSXlw==} + '@tiptap/extension-document@2.27.2': + resolution: {integrity: sha512-CFhAYsPnyYnosDC4639sCJnBUnYH4Cat9qH5NZWHVvdgtDwu8GZgZn2eSzaKSYXWH1vJ9DSlCK+7UyC3SNXIBA==} peerDependencies: - '@tiptap/core': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-dropcursor@3.22.4': - resolution: {integrity: sha512-N9/yMDC35jJp0V/naL0+6gi4gUDUIcPpWEzFdCDWUSYBA8mt41c1kI1ZU7UTKYIBzTClenhYHRc2XKZxxx0+LQ==} + '@tiptap/extension-dropcursor@2.27.2': + resolution: {integrity: sha512-oEu/OrktNoQXq1x29NnH/GOIzQZm8ieTQl3FK27nxfBPA89cNoH4mFEUmBL5/OFIENIjiYG3qWpg6voIqzswNw==} peerDependencies: - '@tiptap/extensions': 3.22.4 + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 - '@tiptap/extension-floating-menu@3.22.4': - resolution: {integrity: sha512-DFuyYxgaZPgxum5z1yvJPbfYCvDdO8geXsdyqt0qYYdiat3aGE4ncJhiLRIFDhSHBhaZg5eCgu/YPYAN6jZnrA==} + '@tiptap/extension-floating-menu@2.27.2': + resolution: {integrity: sha512-GUN6gPIGXS7ngRJOwdSmtBRBDt9Kt9CM/9pSwKebhLJ+honFoNA+Y6IpVyDvvDMdVNgBchiJLs6qA5H97gAePQ==} peerDependencies: - '@floating-ui/dom': ^1.0.0 - '@tiptap/core': 3.22.4 - '@tiptap/pm': 3.22.4 + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 - '@tiptap/extension-gapcursor@3.22.4': - resolution: {integrity: sha512-UYBEUj3SFpKINIE7AdzcyeS3xICK+ee+YLBbuqNXyHStYChjJOohzJehqiqhjR16A88KQQ+ZjgyDcItKGygSog==} + '@tiptap/extension-gapcursor@2.27.2': + resolution: {integrity: sha512-/c9VF1HBxj+AP54XGVgCmD9bEGYc5w5OofYCFQgM7l7PB1J00A4vOke0oPkHJnqnOOyPlFaxO/7N6l3XwFcnKA==} peerDependencies: - '@tiptap/extensions': 3.22.4 + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 - '@tiptap/extension-hard-break@3.22.4': - resolution: {integrity: sha512-xq+a4dE7T6VwApCkh/yU3p30gn3F8g8Arb9CyEZm58/WIJUIGvHSTjDdHmvU16+kiWSBg+wOOsaFHhYjJjxcKA==} + '@tiptap/extension-hard-break@2.27.2': + resolution: {integrity: sha512-kSRVGKlCYK6AGR0h8xRkk0WOFGXHIIndod3GKgWU49APuIGDiXd8sziXsSlniUsWmqgDmDXcNnSzPcV7AQ8YNg==} peerDependencies: - '@tiptap/core': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-heading@3.22.4': - resolution: {integrity: sha512-TUaj5f0Ir5qy9HKKt2ocnwfXKpZDYeHgbbP9gshKFzdq5PLe1RbIgkjfy6bnoI865cYjmPYWRjcT7XsKyIcb9Q==} + '@tiptap/extension-heading@2.27.2': + resolution: {integrity: sha512-iM3yeRWuuQR/IRQ1djwNooJGfn9Jts9zF43qZIUf+U2NY8IlvdNsk2wTOdBgh6E0CamrStPxYGuln3ZS4fuglw==} peerDependencies: - '@tiptap/core': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-horizontal-rule@3.22.4': - resolution: {integrity: sha512-cCI1HekGQwhY/MbgaKQ0R/7HcH5ZM1oFAyI/J72QGLC0XnF403S/OXoHMuBWr1mCu8hNiQWCzeNRJUty0iytNw==} + '@tiptap/extension-history@2.27.2': + resolution: {integrity: sha512-+hSyqERoFNTWPiZx4/FCyZ/0eFqB9fuMdTB4AC/q9iwu3RNWAQtlsJg5230bf/qmyO6bZxRUc0k8p4hrV6ybAw==} peerDependencies: - '@tiptap/core': 3.22.4 - '@tiptap/pm': 3.22.4 + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 - '@tiptap/extension-italic@3.22.4': - resolution: {integrity: sha512-fVSDx5AYXgDI3v2zZIqb7V8EewthwM2NJ/ZCX+XaxRsqNEpnjVhgHs7UlvDqK1wj2OJ6zmUNjPtVlAFRxwT+HQ==} + '@tiptap/extension-horizontal-rule@2.27.2': + resolution: {integrity: sha512-WGWUSgX+jCsbtf9Y9OCUUgRZYuwjVoieW5n6mAUohJ9/6gc6sGIOrUpBShf+HHo6WD+gtQjRd+PssmX3NPWMpg==} peerDependencies: - '@tiptap/core': 3.22.4 + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 - '@tiptap/extension-link@3.22.4': - resolution: {integrity: sha512-uoP3yus02uwGPVzW2QaEPJWVIrUb/r5nKm6c8DiJv9fNSX1+gykZZMg42c6GwRFLZ/vyfWjVCbAE03VMUqafgA==} + '@tiptap/extension-italic@2.27.2': + resolution: {integrity: sha512-1OFsw2SZqfaqx5Fa5v90iNlPRcqyt+lVSjBwTDzuPxTPFY4Q0mL89mKgkq2gVHYNCiaRkXvFLDxaSvBWbmthgg==} peerDependencies: - '@tiptap/core': 3.22.4 - '@tiptap/pm': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-list-item@3.22.4': - resolution: {integrity: sha512-H659KXTvggSypIDWSOJBZ37jh9pKjQriDDvYPYvOZCdfij0D0hsDXN/wXoypArneUkoBdgruHfTtMkFOaQlgkw==} + '@tiptap/extension-list-item@2.27.2': + resolution: {integrity: sha512-eJNee7IEGXMnmygM5SdMGDC8m/lMWmwNGf9fPCK6xk0NxuQRgmZHL6uApKcdH6gyNcRPHCqvTTkhEP7pbny/fg==} peerDependencies: - '@tiptap/extension-list': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-list-keymap@3.22.4': - resolution: {integrity: sha512-t/zhker4oIS78AIGYDdFFfZC6zSBlszfD7z/zqFLGCg5PHNNgkZK5hKj6Vyix6D2SapRn/ajnx+8mhbKIUH5eA==} + '@tiptap/extension-ordered-list@2.27.2': + resolution: {integrity: sha512-M7A4tLGJcLPYdLC4CI2Gwl8LOrENQW59u3cMVa+KkwG1hzSJyPsbDpa1DI6oXPC2WtYiTf22zrbq3gVvH+KA2w==} peerDependencies: - '@tiptap/extension-list': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-list@3.22.4': - resolution: {integrity: sha512-Xe8UFvvHmyp/c/TJsFwlwU9CWACYbBirNsluJ3U1+H8BTu1wqdrT/AXR5uIXeyCl5kiWKgX5q71eHWbYFOrqrg==} + '@tiptap/extension-paragraph@2.27.2': + resolution: {integrity: sha512-elYVn2wHJJ+zB9LESENWOAfI4TNT0jqEN34sMA/hCtA4im1ZG2DdLHwkHIshj/c4H0dzQhmsS/YmNC5Vbqab/A==} peerDependencies: - '@tiptap/core': 3.22.4 - '@tiptap/pm': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-ordered-list@3.22.4': - resolution: {integrity: sha512-w77hPVf7pcHt97vfrybg/l0t5CimCd4y75OJKuHuo3CfgM5xbUP/gaPNMDyLLe7MYole/UHi/XvG3XjgzqTzAw==} + '@tiptap/extension-strike@2.27.2': + resolution: {integrity: sha512-HHIjhafLhS2lHgfAsCwC1okqMsQzR4/mkGDm4M583Yftyjri1TNA7lzhzXWRFWiiMfJxKtdjHjUAQaHuteRTZw==} peerDependencies: - '@tiptap/extension-list': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-paragraph@3.22.4': - resolution: {integrity: sha512-de6dFkIhigiENESY6rNJ3yTVS/337ybfP30dNPudTwGe9oAu9ZCS+04j6QCvXSjhlI3ULiv7wiSHqrP26Gd+Hw==} + '@tiptap/extension-text@2.27.2': + resolution: {integrity: sha512-Xk7nYcigljAY0GO9hAQpZ65ZCxqOqaAlTPDFcKerXmlkQZP/8ndx95OgUb1Xf63kmPOh3xypurGS2is3v0MXSA==} peerDependencies: - '@tiptap/core': 3.22.4 + '@tiptap/core': ^2.7.0 - '@tiptap/extension-strike@3.22.4': - resolution: {integrity: sha512-aRHWQj42HiailXSC9LkKYM3jWMcSeGwOjbqM4PiuxQZmHVDRFmeHkfJItOdn2cSHaO0vuEVK+TvrWUWsBFi3pg==} + '@tiptap/pm@2.27.2': + resolution: {integrity: sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==} + + '@tiptap/starter-kit@2.1.7': + resolution: {integrity: sha512-z2cmJRSC7ImaTGWrHv+xws9y1wIG0OCPosBYpmpwlEfA3JG3axWFmVRJlWnsQV4eSMi3QY3vaPgBAnrR4IxRhQ==} + + '@tiptap/vue-3@2.1.7': + resolution: {integrity: sha512-JJRXWKLJ8mopb0uZV4JXyOW6vKQnarYoCj0hsy9ZT2LhSuLlPXY0D40NAbFVMMbQssewUtgPUFgVZ/TusMEysQ==} peerDependencies: - '@tiptap/core': 3.22.4 - - '@tiptap/extension-text@3.22.4': - resolution: {integrity: sha512-mM69uUW5cSxIhyEpWXi/YcfyupcJMDLCPEfYi62awH0iOP/LRoCv/nHjJq4Hyj/KxRJbe8HKwIUnqaCUf7m5Pg==} - peerDependencies: - '@tiptap/core': 3.22.4 - - '@tiptap/extension-underline@3.22.4': - resolution: {integrity: sha512-08kGdbhIrA6h10GWXqOkqIveaBj5tmxclK208/nUIAlonI9hPd739vu7fmVtpnmqCnSSNpoRtU4u6Gj5at0ZpA==} - peerDependencies: - '@tiptap/core': 3.22.4 - - '@tiptap/extensions@3.22.4': - resolution: {integrity: sha512-fOe8VptJvLPs32bNdUYo8SRyljwqKNQVXWW056VoXIc5en/59OdJlJQVeHI0jRRciH3MtrqODi/gfJR0VHNZ8A==} - peerDependencies: - '@tiptap/core': 3.22.4 - '@tiptap/pm': 3.22.4 - - '@tiptap/pm@3.22.4': - resolution: {integrity: sha512-hj8Qka6WcHRllHUdeSjDnq2XaisUo4KsoGJc1WcFpoa1Yd+OeD861zUMnV7DFVGdZRy45Obht0CUYJpXQ4yA4w==} - - '@tiptap/starter-kit@3.20.5': - resolution: {integrity: sha512-L5E2TCGK0EiwmGIlwMsiwNTW1TLbfPF1Dsji4bSKRJnPbccZIMCB6qdId8v/Z+QGm85NVcBHeruQrDlKDddXBA==} - - '@tiptap/vue-3@3.20.5': - resolution: {integrity: sha512-5uUK3RAMNvUetZOv56Kz8nurhxHxMH60GgCCrVFgIBZoTc14u3d3v7EpcA6gNgzogutrR8GxvyFU3iIkj4kkHA==} - peerDependencies: - '@floating-ui/dom': ^1.0.0 - '@tiptap/core': ^3.20.5 - '@tiptap/pm': ^3.20.5 + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 vue: ^3.0.0 - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/chance@1.1.7': - resolution: {integrity: sha512-40you9610GTQPJyvjMBgmj9wiDO6qXhbfjizNYod/fmvLSfUUxURAJMTD8tjmbcZSsyYE5iEUox61AAcCjW/wQ==} + '@types/chance@1.1.3': + resolution: {integrity: sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw==} '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -824,6 +889,10 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -854,8 +923,14 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/node@25.6.0': - resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@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==} '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -863,124 +938,166 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@5.62.0': + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@5.62.0': + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/type-utils@5.62.0': + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@upsetjs/venn.js@2.0.0': - resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} - - '@vitejs/plugin-vue@6.0.5': - resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==} - engines: {node: ^20.19.0 || >=22.12.0} + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 - '@volar/language-core@2.4.28': - resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + '@volar/language-core@1.10.10': + resolution: {integrity: sha512-nsV1o3AZ5n5jaEAObrS3MWLBWaGwUj/vAsc15FVNIv+DbpizQRISg9wzygsHBr56ELRH8r4K75vkYNMtsSNNWw==} - '@volar/source-map@2.4.28': - resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + '@volar/source-map@1.10.10': + resolution: {integrity: sha512-GVKjLnifV4voJ9F0vhP56p4+F3WGf+gXlRtjFZsv6v3WxBTWU3ZVeaRaEHJmWrcv5LXmoYYpk/SC25BKemPRkg==} - '@volar/typescript@2.4.28': - resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + '@volar/typescript@1.10.10': + resolution: {integrity: sha512-4a2r5bdUub2m+mYVnLu2wt59fuoYWe7nf0uXtGHU8QQ5LDNfzAR0wK7NgDiQ9rcl2WT3fxT2AA9AylAwFtj50A==} - '@vue-macros/common@3.1.2': - resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} - engines: {node: '>=20.19.0'} - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - peerDependenciesMeta: - vue: - optional: true + '@vue/compiler-core@3.3.4': + resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} '@vue/compiler-core@3.5.27': resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==} - '@vue/compiler-core@3.5.31': - resolution: {integrity: sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==} - - '@vue/compiler-core@3.5.33': - resolution: {integrity: sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==} + '@vue/compiler-dom@3.3.4': + resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} '@vue/compiler-dom@3.5.27': resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==} - '@vue/compiler-dom@3.5.31': - resolution: {integrity: sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==} + '@vue/compiler-sfc@3.3.4': + resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} - '@vue/compiler-dom@3.5.33': - resolution: {integrity: sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==} - - '@vue/compiler-sfc@3.5.31': - resolution: {integrity: sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==} - - '@vue/compiler-sfc@3.5.33': - resolution: {integrity: sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==} - - '@vue/compiler-ssr@3.5.31': - resolution: {integrity: sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==} - - '@vue/compiler-ssr@3.5.33': - resolution: {integrity: sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==} + '@vue/compiler-ssr@3.3.4': + resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - '@vue/devtools-api@7.7.9': - resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} - - '@vue/devtools-api@8.1.1': - resolution: {integrity: sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==} - - '@vue/devtools-kit@7.7.9': - resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} - - '@vue/devtools-kit@8.1.1': - resolution: {integrity: sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==} - - '@vue/devtools-shared@7.7.9': - resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} - - '@vue/devtools-shared@8.1.1': - resolution: {integrity: sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==} - - '@vue/language-core@3.2.7': - resolution: {integrity: sha512-Gn4q/tRxbpVGLEuARQ43p3YELlNAFgRUVCgW9U5Cr+5q4vfD2bWDWpl3ABbJMXUt5xlE1dF8dkigg2aUq7JYYw==} - - '@vue/reactivity@3.5.31': - resolution: {integrity: sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==} - - '@vue/runtime-core@3.5.31': - resolution: {integrity: sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==} - - '@vue/runtime-dom@3.5.31': - resolution: {integrity: sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==} - - '@vue/server-renderer@3.5.31': - resolution: {integrity: sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==} + '@vue/eslint-config-prettier@8.0.0': + resolution: {integrity: sha512-55dPqtC4PM/yBjhAr+yEw6+7KzzdkBuLmnhBrDfp4I48+wy+Giqqj9yUr5T2uD/BkBROjjmqnLZmXRdOx/VtQg==} peerDependencies: - vue: 3.5.31 + eslint: '>= 8.0.0' + prettier: '>= 3.0.0' + + '@vue/eslint-config-typescript@11.0.3': + resolution: {integrity: sha512-dkt6W0PX6H/4Xuxg/BlFj5xHvksjpSlVjtkQCpaYJBIEuKj2hOVU7r+TIe+ysCwRYFz/lGqvklntRkCAibsbPw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + eslint-plugin-vue: ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@1.8.8': + resolution: {integrity: sha512-i4KMTuPazf48yMdYoebTkgSOJdFraE4pQf0B+FTOFkbB+6hAfjrSou/UmYWRsWyZV6r4Rc6DDZdI39CJwL0rWw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity-transform@3.3.4': + resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} + + '@vue/reactivity@3.3.4': + resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} + + '@vue/reactivity@3.5.27': + resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==} + + '@vue/runtime-core@3.3.4': + resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} + + '@vue/runtime-dom@3.3.4': + resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} + + '@vue/server-renderer@3.3.4': + resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} + peerDependencies: + vue: 3.3.4 + + '@vue/shared@3.3.4': + resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} '@vue/shared@3.5.27': resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} - '@vue/shared@3.5.31': - resolution: {integrity: sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==} + '@vue/tsconfig@0.4.0': + resolution: {integrity: sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==} - '@vue/shared@3.5.33': - resolution: {integrity: sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==} + '@vue/typescript@1.8.8': + resolution: {integrity: sha512-jUnmMB6egu5wl342eaUH236v8tdcEPXXkPgj+eI/F6JwW/lb+yAU6U07ZbQ3MVabZRlupIlPESB7ajgAGixhow==} - '@vue/tsconfig@0.9.1': - resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==} + '@vuetify/loader-shared@1.7.1': + resolution: {integrity: sha512-kLUvuAed6RCvkeeTNJzuy14pqnkur8lTuner7v7pNE/kVhPR97TuyXwBSBMR1cJeiLiOfu6SF5XlCYbXByEx1g==} peerDependencies: - typescript: '>= 5.8' - vue: ^3.4.0 - peerDependenciesMeta: - typescript: - optional: true - vue: - optional: true + vue: ^3.0.0 + vuetify: ^3.0.0-beta.4 '@vuetify/loader-shared@2.1.2': resolution: {integrity: sha512-X+1jBLmXHkpQEnC0vyOb4rtX2QSkBiFhaFXz8yhQqN2A4vQ6k2nChxN4Ol7VAY5KoqMdFoRMnmNdp/1qYXDQig==} @@ -1039,12 +1156,25 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@yr/monotone-cubic-spline@1.0.3': + resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} + acorn-import-phases@1.0.4: resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} engines: {node: '>=10.13.0'} peerDependencies: acorn: ^8.14.0 + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -1058,16 +1188,24 @@ packages: ajv: optional: true + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: ajv: ^8.8.2 + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - alien-signals@3.1.2: - resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + alien-signals@2.0.8: + resolution: {integrity: sha512-844G1VLkk0Pe2SJjY0J8vp8ADI73IM4KliNu2OGlYzWpO28NexEUvjHTcFjFX3VXoiUtwTbHxLNI9ImkcoBqzA==} ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -1077,38 +1215,58 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - apexcharts@5.10.4: - resolution: {integrity: sha512-gt0VUqZ2+mr25ScbUcKZgJr96jKYm4vjOcxEWCEh/E5F4dWqhyo3dBhPRvNNnkKiWxkMd2cBwj3ZYH3rK39fkA==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + apexcharts@3.42.0: + resolution: {integrity: sha512-hYhzZqh2Efny9uiutkGU2M/EarJ4Nn8s6dxZ0C7E7N+SV4d1xjTioXi2NLn4UKVJabZkb3HnpXDoumXgtAymwg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - ast-kit@2.2.0: - resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} - engines: {node: '>=20.19.0'} - - ast-walker-scope@0.8.3: - resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} - engines: {node: '>=20.19.0'} + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios-mock-adapter@2.1.0: - resolution: {integrity: sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==} + axios-mock-adapter@1.22.0: + resolution: {integrity: sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==} peerDependencies: axios: '>= 0.17.0' - axios@1.13.6: - resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + 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.10.0: resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} engines: {node: '>=6.0.0'} hasBin: true - birpc@2.9.0: - resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} @@ -1125,6 +1283,13 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + callsite@1.0.0: + resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -1135,8 +1300,12 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chance@1.1.13: - resolution: {integrity: sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chance@1.1.11: + resolution: {integrity: sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA==} character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -1144,22 +1313,17 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - chevrotain-allstar@0.4.1: - resolution: {integrity: sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==} + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} peerDependencies: - chevrotain: ^12.0.0 + chevrotain: ^11.0.0 - chevrotain@12.0.0: - resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} - engines: {node: '>=22.0.0'} + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} @@ -1197,22 +1361,33 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - confbox@0.2.4: - resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - - copy-anything@4.0.5: - resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} - engines: {node: '>=18'} - cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1369,15 +1544,19 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} - dagre-d3-es@7.0.14: - resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} - date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1387,10 +1566,16 @@ packages: supports-color: optional: true + decache@4.6.2: + resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==} + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} @@ -1402,21 +1587,23 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - dompurify@3.2.7: - resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} - dompurify@3.4.1: - resolution: {integrity: sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dompurify@3.3.2: + resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} + engines: {node: '>=20'} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -1428,6 +1615,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + enhanced-resolve@5.20.0: resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} @@ -1459,14 +1650,71 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@8.10.2: + resolution: {integrity: sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@9.17.0: + resolution: {integrity: sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.48.0: + resolution: {integrity: sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -1482,6 +1730,10 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + event-source-polyfill@1.0.31: resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==} @@ -1489,15 +1741,28 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + 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'} @@ -1507,10 +1772,36 @@ packages: picomatch: optional: true + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-loader@6.2.0: + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@6.1.20: resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} @@ -1533,6 +1824,9 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1553,9 +1847,29 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1563,11 +1877,14 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - harfbuzzjs@0.10.3: - resolution: {integrity: sha512-GJnLUrgLMadlMYrBGEXwYEimObbysy3prWT4HyPpFQERvgTU/OZ+ReUlEPOum6w4RBtFXzXiCCmECOr4sz3qwQ==} + harfbuzzjs@0.4.15: + resolution: {integrity: sha512-p1edvnlc+vpRe2kz7OKzcscf0gyFiDZpco+miDxAiiZ67cu1oNlbuOkmP/A/i1l/w938VrkF2FdZ8scNcnkPrQ==} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -1595,13 +1912,14 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + highlight.js@11.11.1: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} - hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} - hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -1612,8 +1930,27 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - immutable@5.1.5: - resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@4.3.8: + resolution: {integrity: sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -1622,10 +1959,22 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-buffer@2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1638,46 +1987,61 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-what@5.5.0: - resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} - engines: {node: '>=18'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - js-md5@0.8.3: - resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - katex@0.16.45: - resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} + katex@0.16.28: + resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==} hasBin: true + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - langium@4.2.2: - resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -1685,111 +2049,45 @@ packages: layout-base@2.0.1: resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - linkifyjs@4.3.2: - resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} - loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} - engines: {node: '>=14'} + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - magic-string-ast@1.0.3: - resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} - engines: {node: '>=20.19.0'} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + markdown-it-container@4.0.0: resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==} @@ -1811,41 +2109,33 @@ packages: markdown-it-task-checkbox@1.0.6: resolution: {integrity: sha512-7pxkHuvqTOu3iwVGmDPeYjQg+AIS9VQxzyLP9JCg9lBjgPAJXGEkChK6A2iFuj3tS0GV3HG2u5AMNhcQqwxpJw==} - markdown-it-ts@0.0.7: - resolution: {integrity: sha512-XMQ0IcUQeK/dR7vIZGhtk20MAr2jMtdbNgS/cGfl1WjBE1rprodZja6jwk/PTN+rnsVd8mD4fe8wPcUPavHr3Q==} + markdown-it-ts@0.0.3: + resolution: {integrity: sha512-nZpRTJj4S6bN0I5wsNBtgzDKx+HYBBSsvKjGdYw7/tPdrzfo3gUTt3ZbeAjPGeZaC6a4LFi4JdhTVeLm3F6TIQ==} engines: {node: '>=18'} markdown-it@14.1.1: resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true - marked@14.0.0: - resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} - engines: {node: '>= 18'} - hasBin: true - marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} hasBin: true - markstream-vue@0.0.9: - resolution: {integrity: sha512-yIh/qlXJ0DCobBd07oiFnzlWW2T6sADCL46H7v0Fram7lhamzAezXXF1sIbsRkGmqRqdWngpzgdZkEtuRrOnSw==} + markstream-vue@0.0.6: + resolution: {integrity: sha512-5YrpNrTRdbO0YvKPx2sNu4pq+y+UZ1CPbf9Znoydt3ZL7c7zfBhmViCij+VmAmBrr3VLAgD2HToU8PAtuWySxg==} peerDependencies: '@antv/infographic': ^0.2.3 - '@terrastruct/d2': '>=0.1.33' katex: '>=0.16.22' mermaid: '>=11' shiki: ^3.13.0 - stream-markdown: '>=0.0.14' - stream-monaco: '>=0.0.20' + stream-markdown: '>=0.0.13' + stream-monaco: '>=0.0.17' vue: '>=3.0.0' vue-i18n: '>=9' peerDependenciesMeta: '@antv/infographic': optional: true - '@terrastruct/d2': - optional: true katex: optional: true mermaid: @@ -1872,8 +2162,12 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - mermaid@11.14.0: - resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mermaid@11.12.2: + resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -1890,6 +2184,10 @@ packages: micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1898,41 +2196,68 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - monaco-editor@0.55.1: - resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - muggle-string@0.4.1: - resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + muggle-string@0.3.1: + resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + null-loader@4.0.1: + resolution: {integrity: sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} oniguruma-to-es@4.3.4: resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} @@ -1948,6 +2273,10 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -1958,6 +2287,10 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -1968,44 +2301,57 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - perfect-debounce@1.0.0: - resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - - perfect-debounce@2.1.0: - resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - pinia@3.0.4: - resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + pinia@2.1.6: + resolution: {integrity: sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==} peerDependencies: - typescript: '>=4.5.0' - vue: ^3.5.11 + '@vue/composition-api': ^1.4.0 + typescript: '>=4.4.4' + vue: ^2.6.14 || ^3.3.0 peerDependenciesMeta: + '@vue/composition-api': + optional: true typescript: optional: true pinyin-pro@3.28.0: resolution: {integrity: sha512-mMRty6RisoyYNphJrTo3pnvp3w8OMZBrXm9YSWkxhAfxKj1KZk2y8T2PDIZlDDRsvZ0No+Hz6FI4sZpA6Ey25g==} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -2016,10 +2362,27 @@ packages: points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - postcss@8.5.10: - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.0.2: + resolution: {integrity: sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==} + engines: {node: '>=14'} + hasBin: true + property-expr@2.0.6: resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} @@ -2029,6 +2392,9 @@ packages: prosemirror-changeset@2.4.0: resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + prosemirror-commands@1.7.1: resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} @@ -2041,12 +2407,24 @@ packages: prosemirror-history@1.5.0: resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + prosemirror-keymap@1.2.3: resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + prosemirror-markdown@1.13.4: + resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} + + prosemirror-menu@1.3.0: + resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==} + prosemirror-model@1.25.4: resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + prosemirror-schema-list@1.5.1: resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} @@ -2056,6 +2434,13 @@ packages: prosemirror-tables@1.8.5: resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + prosemirror-transform@1.11.0: resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==} @@ -2069,6 +2454,10 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qified@0.6.0: resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} engines: {node: '>=20'} @@ -2078,16 +2467,21 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + querystring@0.2.1: + resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -2109,15 +2503,30 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rolldown@1.0.0-rc.12: - resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} - engines: {node: ^20.19.0 || >=22.12.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: @@ -2126,23 +2535,26 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass-loader@16.0.7: - resolution: {integrity: sha512-w6q+fRHourZ+e+xA1kcsF27iGM6jdB8teexYCfdUw0sYgcDNeZESnDNT9sUmmPm3ooziwUJXGwZJSTF3kOdBfA==} - engines: {node: '>= 18.12.0'} + sass-loader@13.3.2: + resolution: {integrity: sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==} + engines: {node: '>= 14.15.0'} peerDependencies: - '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 + fibers: '>= 3.1.0' node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 sass: ^1.3.0 sass-embedded: '*' webpack: ^5.0.0 peerDependenciesMeta: - '@rspack/core': + fibers: optional: true node-sass: optional: true @@ -2150,26 +2562,51 @@ packages: optional: true sass-embedded: optional: true - webpack: - optional: true - sass@1.98.0: - resolution: {integrity: sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==} + sass@1.66.1: + resolution: {integrity: sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==} engines: {node: '>=14.0.0'} hasBin: true + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + schema-utils@4.3.3: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} - scule@1.3.0: - resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - shiki@3.23.0: - resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + shiki@3.22.0: + resolution: {integrity: sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -2185,21 +2622,23 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - speakingurl@14.0.1: - resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} - engines: {node: '>=0.10.0'} - state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} - stream-markdown-parser@0.0.72: - resolution: {integrity: sha512-I+mbNyg0WZN7cxyia6/XavEWL1pA78qvYqfTEK01cSyN6WaKAVT8H7tn+8hX/C+L1jXTf1jUqPopqz8FAz+peQ==} + stream-markdown-parser@0.0.59-beta.3: + resolution: {integrity: sha512-F++KEcHsXeWjKLKw8id6L1JVqQH22fslEZRNbt1NAMUwr8KTI/vOE3UuXYGnJDyXt0yv5JXbO4KdXrvMlWM0qQ==} - stream-markdown@0.0.14: - resolution: {integrity: sha512-W9P+TU1/zvQFtogwVjI+hju5LwUdbMwjtKM/SxxRbUoMh7yHKO2JwCaZf9yslrP5IKKR1psxJW+N5O4XB3YOEw==} + stream-markdown@0.0.13: + resolution: {integrity: sha512-XYhBEtKA76L6q3Uegvu8QOiUDAV4bfF1TcP3rot8eK0AR1+WGHBB6IfT9GJeLFJ7+qZgrH+WxQp/8lZ6CXjeeg==} peerDependencies: shiki: '>=3.13.0' + stream-monaco@0.0.17: + resolution: {integrity: sha512-xpMiFAQEDLxRjNOXobomP9vEJmv5tWU8oN54+SHqL+eeHMNugU7fpmUIqZRSh53p+a/ZYt+DE75jgHwlKOXEuQ==} + hasBin: true + peerDependencies: + monaco-editor: ^0.52.2 + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2211,20 +2650,63 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - subset-font@2.5.0: - resolution: {integrity: sha512-Vsa8ngQ/ohhUj0an7on49y9jLZ2rK5U+T1FzPM4/ZQY0xUy5mLis6BfFtPGzecTjFgYXQlvY7FlsJF4t3R/6Ug==} + subset-font@2.4.0: + resolution: {integrity: sha512-DA/45nIj4NiseVdfHxVdVGL7hvNo3Ol6HjEm3KSYtPyDcsr6jh8Q37vSgz+A722wMfUd6nL8kgsi7uGv9DExXQ==} - superjson@2.2.6: - resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} - engines: {node: '>=16'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg.draggable.js@2.2.2: + resolution: {integrity: sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==} + engines: {node: '>= 0.8.0'} + + svg.easing.js@2.0.0: + resolution: {integrity: sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==} + engines: {node: '>= 0.8.0'} + + svg.filter.js@2.0.2: + resolution: {integrity: sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==} + engines: {node: '>= 0.8.0'} + + svg.js@2.7.1: + resolution: {integrity: sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==} + + svg.pathmorphing.js@0.1.3: + resolution: {integrity: sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==} + engines: {node: '>= 0.8.0'} + + svg.resize.js@1.4.3: + resolution: {integrity: sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==} + engines: {node: '>= 0.8.0'} + + svg.select.js@2.1.2: + resolution: {integrity: sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==} + engines: {node: '>= 0.8.0'} + + svg.select.js@3.0.1: + resolution: {integrity: sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==} + engines: {node: '>= 0.8.0'} + + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -2250,6 +2732,9 @@ packages: engines: {node: '>=10'} hasBin: true + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -2261,6 +2746,13 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toposort@2.0.2: resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} @@ -2271,8 +2763,22 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} @@ -2282,8 +2788,8 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} hasBin: true @@ -2293,8 +2799,8 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - undici-types@7.19.2: - resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -2311,14 +2817,6 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - unplugin-utils@0.3.1: - resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} - engines: {node: '>=20.19.0'} - - unplugin@3.0.0: - resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} - engines: {node: ^20.19.0 || >=22.12.0} - upath@2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} @@ -2329,14 +2827,20 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - vee-validate@4.15.1: - resolution: {integrity: sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==} + vee-validate@4.11.3: + resolution: {integrity: sha512-YhWORdZRE1GL6vXKj3r9f+Y8fJH5JMwMUJ4DFS44+WcTtiNXggyE3pyJPlZBqS9AgYGZ47EOv4UczkLxHqufiw==} peerDependencies: - vue: ^3.4.26 + vue: ^3.3.4 vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2357,34 +2861,31 @@ packages: peerDependencies: vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - vite@8.0.5: - resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} - engines: {node: ^20.19.0 || >=22.12.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': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 || ^0.28.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true jiti: optional: true less: optional: true + lightningcss: + optional: true sass: optional: true sass-embedded: @@ -2420,8 +2921,18 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-cli-plugin-vuetify@2.5.8: + resolution: {integrity: sha512-uqi0/URJETJBbWlQHD1l0pnY7JN8Ytu+AL1fw50HFlGByPa8/xx+mq19GkFXA9FcwFT01IqEc/TkxMPugchomg==} + peerDependencies: + sass-loader: '*' + vue: '*' + vuetify-loader: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + sass-loader: + optional: true + vuetify-loader: + optional: true vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} @@ -2434,57 +2945,61 @@ packages: '@vue/composition-api': optional: true - vue-i18n@11.3.2: - resolution: {integrity: sha512-gmFrvM+iuf2AH4ygligw/pC7PRJ63AdRNE68E0GPlQ83Mzfyck6g6cRQC3KzkYXr+ZidR91wq+5YBmAMpkgE1A==} + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-i18n@11.2.8: + resolution: {integrity: sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==} engines: {node: '>= 16'} peerDependencies: vue: ^3.0.0 - vue-router@5.0.4: - resolution: {integrity: sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==} + vue-router@4.2.4: + resolution: {integrity: sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==} peerDependencies: - '@pinia/colada': '>=0.21.2' - '@vue/compiler-sfc': ^3.5.17 - pinia: ^3.0.4 - vue: ^3.5.0 - peerDependenciesMeta: - '@pinia/colada': - optional: true - '@vue/compiler-sfc': - optional: true - pinia: - optional: true + vue: ^3.2.0 - vue-tsc@3.2.7: - resolution: {integrity: sha512-zc1tL3HoQni1zGTGrwBVRQb7rGP5SWdu/m4rGB6JcnAC5MT5LFZIxF7Y+EJEnt4hGF23d60rXH7gRjHGb5KQQQ==} + vue-template-compiler@2.7.16: + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + + vue-tsc@1.8.8: + resolution: {integrity: sha512-bSydNFQsF7AMvwWsRXD7cBIXaNs/KSjvzWLymq/UtKE36697sboX4EccSHFVxvgdBlI1frYPc/VMKJNB7DFeDQ==} hasBin: true peerDependencies: - typescript: '>=5.0.0' + typescript: '*' - vue3-apexcharts@1.11.1: - resolution: {integrity: sha512-MbN3vg8bMG19wc0Lm1HkeQvODgLm56DgpIxtNUO0xpf/JCzYWVGE4jzXp2JISzy2s3Kul1yOxNQUYsLvKQ5L9g==} + vue3-apexcharts@1.4.4: + resolution: {integrity: sha512-TH89uZrxGjaDvkaYAISvj8+k6Bf1rUKFillc8oJirs5XZEPiwM1ELKZQ786wz0rfPqkSHHny2lqqUCK7Rw+LcQ==} peerDependencies: - apexcharts: '>=5.10.0' - vue: '>=3.0.0' + apexcharts: '> 3.0.0' + vue: '> 3.0.0' vue3-print-nb@0.1.4: resolution: {integrity: sha512-LExI7viEzplR6ZKQ2b+V4U0cwGYbVD4fut/XHvk3UPGlT5CcvIGs6VlwGp107aKgk6P8Pgx4rco3Rehv2lti3A==} - vue@3.5.31: - resolution: {integrity: sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + vue@3.3.4: + resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} - vuetify@4.0.4: - resolution: {integrity: sha512-sO2ux9RG0C1HKaP1HqDMro3+vbbmUJwzcKXuzaxQmUERAT/0FR0yfbwnj4PrMwWy1qc2WPJq01h4cr86FmNrFA==} + vuetify-loader@2.0.0-alpha.9: + resolution: {integrity: sha512-M4u2XX9coe1U51jLKek54eJTo7wnroNfglh6sQplRTslhQRKQM3k84oh87D0VHqTzoTzlKPHP0sIWdpklwaEEQ==} + engines: {node: '>=12'} + deprecated: vuetify-loader has been renamed to webpack-plugin-vuetify for Vuetify 3 + peerDependencies: + '@vue/compiler-sfc': ^3.2.6 + vuetify: ^3.0.0-alpha.11 + webpack: ^5.0.0 + + vuetify@3.7.11: + resolution: {integrity: sha512-50Z2SNwPXbkGmve4CwxOs4sySZGgLwQYIDsKx+coSrfIBqz8IyXgxRFQdrvgoehIwUjGTNqaPZymuK5rMFkfHA==} + engines: {node: ^12.20 || >=14.13} peerDependencies: typescript: '>=4.7' - vite-plugin-vuetify: '>=2.1.0' - vue: ^3.5.0 - webpack-plugin-vuetify: '>=3.1.0' + vite-plugin-vuetify: '>=1.0.0' + vue: ^3.3.0 + webpack-plugin-vuetify: '>=2.0.0' peerDependenciesMeta: typescript: optional: true @@ -2508,9 +3023,6 @@ packages: resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webpack@5.105.0: resolution: {integrity: sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==} engines: {node: '>=10.13.0'} @@ -2524,21 +3036,32 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + woff2sfnt-sfnt2woff@1.0.0: resolution: {integrity: sha512-edK4COc1c1EpRfMqCZO1xJOvdUtM5dbVb9iz97rScvnTevqEB3GllnLWCmMVp1MfQBdF1DFg/11I0rSyAdS4qQ==} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} - engines: {node: '>= 14.6'} - hasBin: true - yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -2551,8 +3074,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yup@1.7.1: - resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + yup@1.2.0: + resolution: {integrity: sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -2564,14 +3087,6 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -2580,50 +3095,13 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/parser@7.29.2': - dependencies: - '@babel/types': 7.29.0 + '@babel/runtime@7.28.6': {} '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.4.13': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.13 - '@biomejs/cli-darwin-x64': 2.4.13 - '@biomejs/cli-linux-arm64': 2.4.13 - '@biomejs/cli-linux-arm64-musl': 2.4.13 - '@biomejs/cli-linux-x64': 2.4.13 - '@biomejs/cli-linux-x64-musl': 2.4.13 - '@biomejs/cli-win32-arm64': 2.4.13 - '@biomejs/cli-win32-x64': 2.4.13 - - '@biomejs/cli-darwin-arm64@2.4.13': - optional: true - - '@biomejs/cli-darwin-x64@2.4.13': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.4.13': - optional: true - - '@biomejs/cli-linux-arm64@2.4.13': - optional: true - - '@biomejs/cli-linux-x64-musl@2.4.13': - optional: true - - '@biomejs/cli-linux-x64@2.4.13': - optional: true - - '@biomejs/cli-win32-arm64@2.4.13': - optional: true - - '@biomejs/cli-win32-x64@2.4.13': - optional: true - '@braintree/sanitize-url@7.1.2': {} '@cacheable/memory@2.0.8': @@ -2638,68 +3116,153 @@ snapshots: hashery: 1.5.0 keyv: 5.6.0 - '@chevrotain/cst-dts-gen@12.0.0': + '@chevrotain/cst-dts-gen@11.0.3': dependencies: - '@chevrotain/gast': 12.0.0 - '@chevrotain/types': 12.0.0 + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.23 - '@chevrotain/gast@12.0.0': + '@chevrotain/gast@11.0.3': dependencies: - '@chevrotain/types': 12.0.0 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.23 - '@chevrotain/regexp-to-ast@12.0.0': {} + '@chevrotain/regexp-to-ast@11.0.3': {} - '@chevrotain/types@12.0.0': {} + '@chevrotain/types@11.0.3': {} - '@chevrotain/utils@12.0.0': {} + '@chevrotain/utils@11.0.3': {} - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 + '@esbuild/aix-ppc64@0.25.12': optional: true - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 + '@esbuild/android-arm64@0.25.12': optional: true - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 + '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@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)': + dependencies: + eslint: 8.48.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.48.0': {} + '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 - optional: true - - '@floating-ui/core@1.7.5': - dependencies: - '@floating-ui/utils': 0.2.11 '@floating-ui/dom@1.7.5': dependencies: '@floating-ui/core': 1.7.4 '@floating-ui/utils': 0.2.10 - optional: true - '@floating-ui/dom@1.7.6': - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils@0.2.10': {} - '@floating-ui/utils@0.2.10': - optional: true - - '@floating-ui/utils@0.2.11': {} - - '@guolao/vue-monaco-editor@1.6.0(monaco-editor@0.55.1)(vue@3.5.31(typescript@6.0.3))': + '@guolao/vue-monaco-editor@1.6.0(monaco-editor@0.52.2)(vue@3.3.4)': dependencies: '@monaco-editor/loader': 1.7.0 - monaco-editor: 0.55.1 - vue: 3.5.31(typescript@6.0.3) - vue-demi: 0.14.10(vue@3.5.31(typescript@6.0.3)) + monaco-editor: 0.52.2 + vue: 3.3.4 + vue-demi: 0.14.10(vue@3.3.4) + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} '@iconify/types@2.0.0': {} @@ -2709,41 +3272,29 @@ snapshots: '@iconify/types': 2.0.0 mlly: 1.8.0 - '@intlify/core-base@11.3.2': + '@intlify/core-base@11.2.8': dependencies: - '@intlify/devtools-types': 11.3.2 - '@intlify/message-compiler': 11.3.2 - '@intlify/shared': 11.3.2 + '@intlify/message-compiler': 11.2.8 + '@intlify/shared': 11.2.8 - '@intlify/devtools-types@11.3.2': + '@intlify/message-compiler@11.2.8': dependencies: - '@intlify/core-base': 11.3.2 - '@intlify/shared': 11.3.2 - - '@intlify/message-compiler@11.3.2': - dependencies: - '@intlify/shared': 11.3.2 + '@intlify/shared': 11.2.8 source-map-js: 1.2.1 - '@intlify/shared@11.3.2': {} + '@intlify/shared@11.2.8': {} '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/source-map@0.3.11': dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - optional: true '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2760,343 +3311,298 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@mdi/font@7.4.47': {} + '@mdi/font@7.2.96': {} - '@mermaid-js/parser@1.1.0': + '@mermaid-js/parser@0.6.3': dependencies: - langium: 4.2.2 + langium: 3.3.1 '@monaco-editor/loader@1.7.0': dependencies: state-local: 1.0.7 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@nodelib/fs.scandir@2.1.5': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.1 - optional: true + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - '@oxc-project/types@0.122.0': {} + '@nodelib/fs.stat@2.0.5': {} - '@parcel/watcher-android-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-x64@2.5.6': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.6': - optional: true - - '@parcel/watcher-win32-arm64@2.5.6': - optional: true - - '@parcel/watcher-win32-ia32@2.5.6': - optional: true - - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': + '@nodelib/fs.walk@1.2.8': dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.3 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@pkgr/core@0.2.9': {} + + '@popperjs/core@2.11.8': {} + + '@remirror/core-constants@3.0.0': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.12': + '@rollup/rollup-android-arm64@4.59.0': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + '@rollup/rollup-darwin-arm64@4.59.0': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.12': + '@rollup/rollup-darwin-x64@4.59.0': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + '@rollup/rollup-freebsd-arm64@4.59.0': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + '@rollup/rollup-freebsd-x64@4.59.0': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + '@rollup/rollup-linux-arm-musleabihf@4.59.0': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + '@rollup/rollup-linux-arm64-gnu@4.59.0': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + '@rollup/rollup-linux-arm64-musl@4.59.0': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + '@rollup/rollup-linux-loong64-gnu@4.59.0': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + '@rollup/rollup-linux-loong64-musl@4.59.0': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + '@rollup/rollup-linux-ppc64-gnu@4.59.0': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@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': dependencies: - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.12': {} - - '@rolldown/pluginutils@1.0.0-rc.2': {} - - '@shikijs/core@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.23.0': + '@shikijs/engine-javascript@3.22.0': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.4 - '@shikijs/engine-oniguruma@3.23.0': + '@shikijs/engine-oniguruma@3.22.0': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.23.0': + '@shikijs/langs@3.22.0': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/types': 3.22.0 - '@shikijs/themes@3.23.0': + '@shikijs/monaco@3.22.0': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/core': 3.22.0 + '@shikijs/types': 3.22.0 + '@shikijs/vscode-textmate': 10.0.2 + optional: true - '@shikijs/types@3.23.0': + '@shikijs/themes@3.22.0': + dependencies: + '@shikijs/types': 3.22.0 + + '@shikijs/types@3.22.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 '@shikijs/vscode-textmate@10.0.2': {} - '@tiptap/core@3.22.4(@tiptap/pm@3.22.4)': + '@tiptap/core@2.27.2(@tiptap/pm@2.27.2)': dependencies: - '@tiptap/pm': 3.22.4 + '@tiptap/pm': 2.27.2 - '@tiptap/extension-blockquote@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + '@tiptap/extension-blockquote@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-bold@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + '@tiptap/extension-bold@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-bubble-menu@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + '@tiptap/extension-bubble-menu@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': dependencies: - '@floating-ui/dom': 1.7.5 - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 - optional: true + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/pm': 2.27.2 + tippy.js: 6.3.7 - '@tiptap/extension-bullet-list@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-bullet-list@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-code-block@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + '@tiptap/extension-code-block@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/pm': 2.27.2 - '@tiptap/extension-code@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + '@tiptap/extension-code@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-document@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + '@tiptap/extension-document@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-dropcursor@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-dropcursor@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': dependencies: - '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/pm': 2.27.2 - '@tiptap/extension-floating-menu@3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + '@tiptap/extension-floating-menu@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 - optional: true + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/pm': 2.27.2 + tippy.js: 6.3.7 - '@tiptap/extension-gapcursor@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-gapcursor@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': dependencies: - '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/pm': 2.27.2 - '@tiptap/extension-hard-break@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + '@tiptap/extension-hard-break@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-heading@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + '@tiptap/extension-heading@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-horizontal-rule@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + '@tiptap/extension-history@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/pm': 2.27.2 - '@tiptap/extension-italic@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + '@tiptap/extension-horizontal-rule@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/pm': 2.27.2 - '@tiptap/extension-link@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + '@tiptap/extension-italic@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 - linkifyjs: 4.3.2 + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-list-item@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-list-item@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-list-keymap@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-ordered-list@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': + '@tiptap/extension-paragraph@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-ordered-list@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))': + '@tiptap/extension-strike@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-paragraph@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': + '@tiptap/extension-text@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) - '@tiptap/extension-strike@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': - dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - - '@tiptap/extension-text@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': - dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - - '@tiptap/extension-underline@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))': - dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - - '@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)': - dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 - - '@tiptap/pm@3.22.4': + '@tiptap/pm@2.27.2': dependencies: prosemirror-changeset: 2.4.0 + prosemirror-collab: 1.3.1 prosemirror-commands: 1.7.1 prosemirror-dropcursor: 1.8.2 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.3.0 prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.4 prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6) prosemirror-transform: 1.11.0 prosemirror-view: 1.41.6 - '@tiptap/starter-kit@3.20.5': + '@tiptap/starter-kit@2.1.7(@tiptap/pm@2.27.2)': dependencies: - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/extension-blockquote': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-bold': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-bullet-list': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) - '@tiptap/extension-code': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-code-block': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - '@tiptap/extension-document': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-dropcursor': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) - '@tiptap/extension-gapcursor': 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) - '@tiptap/extension-hard-break': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-heading': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-horizontal-rule': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - '@tiptap/extension-italic': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-link': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - '@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - '@tiptap/extension-list-item': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) - '@tiptap/extension-list-keymap': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) - '@tiptap/extension-ordered-list': 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)) - '@tiptap/extension-paragraph': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-strike': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-text': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extension-underline': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4)) - '@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/extension-blockquote': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-bold': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-bullet-list': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-code': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-code-block': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) + '@tiptap/extension-document': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-dropcursor': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) + '@tiptap/extension-gapcursor': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) + '@tiptap/extension-hard-break': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-heading': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-history': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) + '@tiptap/extension-horizontal-rule': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) + '@tiptap/extension-italic': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-list-item': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-ordered-list': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-paragraph': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-strike': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + '@tiptap/extension-text': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) + transitivePeerDependencies: + - '@tiptap/pm' - '@tiptap/vue-3@3.20.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(vue@3.5.31(typescript@6.0.3))': + '@tiptap/vue-3@2.1.7(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(vue@3.3.4)': dependencies: - '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.22.4(@tiptap/pm@3.22.4) - '@tiptap/pm': 3.22.4 - vue: 3.5.31(typescript@6.0.3) - optionalDependencies: - '@tiptap/extension-bubble-menu': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) - '@tiptap/extension-floating-menu': 3.22.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4) + '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) + '@tiptap/extension-bubble-menu': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) + '@tiptap/extension-floating-menu': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) + '@tiptap/pm': 2.27.2 + vue: 3.3.4 - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/chance@1.1.7': {} + '@types/chance@1.1.3': {} '@types/d3-array@3.2.2': {} @@ -3215,20 +3721,21 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.2 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 '@types/estree': 1.0.8 - optional: true '@types/eslint@9.6.1': dependencies: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 - optional: true - '@types/estree@1.0.8': - optional: true + '@types/estree@1.0.8': {} '@types/geojson@7946.0.16': {} @@ -3236,8 +3743,7 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/json-schema@7.0.15': - optional: true + '@types/json-schema@7.0.15': {} '@types/linkify-it@5.0.0': {} @@ -3252,49 +3758,131 @@ snapshots: '@types/mdurl@2.0.0': {} - '@types/node@25.6.0': + '@types/node@20.19.32': dependencies: - undici-types: 7.19.2 + 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': optional: true '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.48.0)(typescript@5.1.6))(eslint@8.48.0)(typescript@5.1.6)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 5.62.0(eslint@8.48.0)(typescript@5.1.6) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.48.0)(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.48.0)(typescript@5.1.6) + debug: 4.4.3 + eslint: 8.48.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare-lite: 1.4.0 + semver: 7.7.4 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@5.62.0(eslint@8.48.0)(typescript@5.1.6)': + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) + debug: 4.4.3 + eslint: 8.48.0 + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/type-utils@5.62.0(eslint@8.48.0)(typescript@5.1.6)': + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.48.0)(typescript@5.1.6) + debug: 4.4.3 + eslint: 8.48.0 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.1.6)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.4 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@8.48.0)(typescript@5.1.6)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.48.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) + eslint: 8.48.0 + eslint-scope: 5.1.1 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + '@ungap/structured-clone@1.3.0': {} - '@upsetjs/venn.js@2.0.0': - optionalDependencies: - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - '@vitejs/plugin-vue@6.0.5(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3))(vue@3.5.31(typescript@6.0.3))': + '@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: - '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3) - vue: 3.5.31(typescript@6.0.3) + vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) + vue: 3.3.4 - '@volar/language-core@2.4.28': + '@volar/language-core@1.10.10': dependencies: - '@volar/source-map': 2.4.28 + '@volar/source-map': 1.10.10 - '@volar/source-map@2.4.28': {} - - '@volar/typescript@2.4.28': + '@volar/source-map@1.10.10': dependencies: - '@volar/language-core': 2.4.28 + muggle-string: 0.3.1 + + '@volar/typescript@1.10.10': + dependencies: + '@volar/language-core': 1.10.10 path-browserify: 1.0.1 - vscode-uri: 3.0.8 - '@vue-macros/common@3.1.2(vue@3.5.31(typescript@6.0.3))': + '@vue/compiler-core@3.3.4': dependencies: - '@vue/compiler-sfc': 3.5.33 - ast-kit: 2.2.0 - local-pkg: 1.1.2 - magic-string-ast: 1.0.3 - unplugin-utils: 0.3.1 - optionalDependencies: - vue: 3.5.31(typescript@6.0.3) + '@babel/parser': 7.29.0 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + source-map-js: 1.2.1 '@vue/compiler-core@3.5.27': dependencies: @@ -3304,177 +3892,147 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-core@3.5.31': + '@vue/compiler-dom@3.3.4': dependencies: - '@babel/parser': 7.29.2 - '@vue/shared': 3.5.31 - entities: 7.0.1 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - - '@vue/compiler-core@3.5.33': - dependencies: - '@babel/parser': 7.29.2 - '@vue/shared': 3.5.33 - entities: 7.0.1 - estree-walker: 2.0.2 - source-map-js: 1.2.1 + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 '@vue/compiler-dom@3.5.27': dependencies: '@vue/compiler-core': 3.5.27 '@vue/shared': 3.5.27 - '@vue/compiler-dom@3.5.31': + '@vue/compiler-sfc@3.3.4': dependencies: - '@vue/compiler-core': 3.5.31 - '@vue/shared': 3.5.31 - - '@vue/compiler-dom@3.5.33': - dependencies: - '@vue/compiler-core': 3.5.33 - '@vue/shared': 3.5.33 - - '@vue/compiler-sfc@3.5.31': - dependencies: - '@babel/parser': 7.29.2 - '@vue/compiler-core': 3.5.31 - '@vue/compiler-dom': 3.5.31 - '@vue/compiler-ssr': 3.5.31 - '@vue/shared': 3.5.31 + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.3.4 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-ssr': 3.3.4 + '@vue/reactivity-transform': 3.3.4 + '@vue/shared': 3.3.4 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.10 + postcss: 8.5.6 source-map-js: 1.2.1 - '@vue/compiler-sfc@3.5.33': + '@vue/compiler-ssr@3.3.4': dependencies: - '@babel/parser': 7.29.2 - '@vue/compiler-core': 3.5.33 - '@vue/compiler-dom': 3.5.33 - '@vue/compiler-ssr': 3.5.33 - '@vue/shared': 3.5.33 - estree-walker: 2.0.2 - magic-string: 0.30.21 - postcss: 8.5.10 - source-map-js: 1.2.1 - - '@vue/compiler-ssr@3.5.31': - dependencies: - '@vue/compiler-dom': 3.5.31 - '@vue/shared': 3.5.31 - - '@vue/compiler-ssr@3.5.33': - dependencies: - '@vue/compiler-dom': 3.5.33 - '@vue/shared': 3.5.33 + '@vue/compiler-dom': 3.3.4 + '@vue/shared': 3.3.4 '@vue/devtools-api@6.6.4': {} - '@vue/devtools-api@7.7.9': + '@vue/eslint-config-prettier@8.0.0(@types/eslint@9.6.1)(eslint@8.48.0)(prettier@3.0.2)': dependencies: - '@vue/devtools-kit': 7.7.9 + eslint: 8.48.0 + eslint-config-prettier: 8.10.2(eslint@8.48.0) + eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@8.48.0))(eslint@8.48.0)(prettier@3.0.2) + prettier: 3.0.2 + transitivePeerDependencies: + - '@types/eslint' - '@vue/devtools-api@8.1.1': + '@vue/eslint-config-typescript@11.0.3(eslint-plugin-vue@9.17.0(eslint@8.48.0))(eslint@8.48.0)(typescript@5.1.6)': dependencies: - '@vue/devtools-kit': 8.1.1 + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.48.0)(typescript@5.1.6))(eslint@8.48.0)(typescript@5.1.6) + '@typescript-eslint/parser': 5.62.0(eslint@8.48.0)(typescript@5.1.6) + eslint: 8.48.0 + eslint-plugin-vue: 9.17.0(eslint@8.48.0) + vue-eslint-parser: 9.4.3(eslint@8.48.0) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color - '@vue/devtools-kit@7.7.9': + '@vue/language-core@1.8.8(typescript@5.1.6)': dependencies: - '@vue/devtools-shared': 7.7.9 - birpc: 2.9.0 - hookable: 5.5.3 - mitt: 3.0.1 - perfect-debounce: 1.0.0 - speakingurl: 14.0.1 - superjson: 2.2.6 - - '@vue/devtools-kit@8.1.1': - dependencies: - '@vue/devtools-shared': 8.1.1 - birpc: 2.9.0 - hookable: 5.5.3 - perfect-debounce: 2.1.0 - - '@vue/devtools-shared@7.7.9': - dependencies: - rfdc: 1.4.1 - - '@vue/devtools-shared@8.1.1': {} - - '@vue/language-core@3.2.7': - dependencies: - '@volar/language-core': 2.4.28 + '@volar/language-core': 1.10.10 + '@volar/source-map': 1.10.10 '@vue/compiler-dom': 3.5.27 + '@vue/reactivity': 3.5.27 '@vue/shared': 3.5.27 - alien-signals: 3.1.2 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - picomatch: 4.0.4 + minimatch: 9.0.5 + muggle-string: 0.3.1 + vue-template-compiler: 2.7.16 + optionalDependencies: + typescript: 5.1.6 - '@vue/reactivity@3.5.31': + '@vue/reactivity-transform@3.3.4': dependencies: - '@vue/shared': 3.5.31 + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + magic-string: 0.30.21 - '@vue/runtime-core@3.5.31': + '@vue/reactivity@3.3.4': dependencies: - '@vue/reactivity': 3.5.31 - '@vue/shared': 3.5.31 + '@vue/shared': 3.3.4 - '@vue/runtime-dom@3.5.31': + '@vue/reactivity@3.5.27': dependencies: - '@vue/reactivity': 3.5.31 - '@vue/runtime-core': 3.5.31 - '@vue/shared': 3.5.31 + '@vue/shared': 3.5.27 + + '@vue/runtime-core@3.3.4': + dependencies: + '@vue/reactivity': 3.3.4 + '@vue/shared': 3.3.4 + + '@vue/runtime-dom@3.3.4': + dependencies: + '@vue/runtime-core': 3.3.4 + '@vue/shared': 3.3.4 csstype: 3.2.3 - '@vue/server-renderer@3.5.31(vue@3.5.31(typescript@6.0.3))': + '@vue/server-renderer@3.3.4(vue@3.3.4)': dependencies: - '@vue/compiler-ssr': 3.5.31 - '@vue/shared': 3.5.31 - vue: 3.5.31(typescript@6.0.3) + '@vue/compiler-ssr': 3.3.4 + '@vue/shared': 3.3.4 + vue: 3.3.4 + + '@vue/shared@3.3.4': {} '@vue/shared@3.5.27': {} - '@vue/shared@3.5.31': {} + '@vue/tsconfig@0.4.0': {} - '@vue/shared@3.5.33': {} + '@vue/typescript@1.8.8(typescript@5.1.6)': + dependencies: + '@volar/typescript': 1.10.10 + '@vue/language-core': 1.8.8(typescript@5.1.6) + transitivePeerDependencies: + - typescript - '@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.31(typescript@6.0.3))': - optionalDependencies: - typescript: 6.0.3 - vue: 3.5.31(typescript@6.0.3) + '@vuetify/loader-shared@1.7.1(vue@3.3.4)(vuetify@3.7.11)': + dependencies: + 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@2.1.3)(vue@3.3.4) - '@vuetify/loader-shared@2.1.2(vue@3.5.31(typescript@6.0.3))(vuetify@4.0.4)': + '@vuetify/loader-shared@2.1.2(vue@3.3.4)(vuetify@3.7.11)': dependencies: upath: 2.0.1 - vue: 3.5.31(typescript@6.0.3) - vuetify: 4.0.4(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.31(typescript@6.0.3)) + 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: '@webassemblyjs/helper-numbers': 1.13.2 '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - optional: true - '@webassemblyjs/floating-point-hex-parser@1.13.2': - optional: true + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - '@webassemblyjs/helper-api-error@1.13.2': - optional: true + '@webassemblyjs/helper-api-error@1.13.2': {} - '@webassemblyjs/helper-buffer@1.14.1': - optional: true + '@webassemblyjs/helper-buffer@1.14.1': {} '@webassemblyjs/helper-numbers@1.13.2': dependencies: '@webassemblyjs/floating-point-hex-parser': 1.13.2 '@webassemblyjs/helper-api-error': 1.13.2 '@xtuc/long': 4.2.2 - optional: true - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - optional: true + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} '@webassemblyjs/helper-wasm-section@1.14.1': dependencies: @@ -3482,20 +4040,16 @@ snapshots: '@webassemblyjs/helper-buffer': 1.14.1 '@webassemblyjs/helper-wasm-bytecode': 1.13.2 '@webassemblyjs/wasm-gen': 1.14.1 - optional: true '@webassemblyjs/ieee754@1.13.2': dependencies: '@xtuc/ieee754': 1.2.0 - optional: true '@webassemblyjs/leb128@1.13.2': dependencies: '@xtuc/long': 4.2.2 - optional: true - '@webassemblyjs/utf8@1.13.2': - optional: true + '@webassemblyjs/utf8@1.13.2': {} '@webassemblyjs/wasm-edit@1.14.1': dependencies: @@ -3507,7 +4061,6 @@ snapshots: '@webassemblyjs/wasm-opt': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 '@webassemblyjs/wast-printer': 1.14.1 - optional: true '@webassemblyjs/wasm-gen@1.14.1': dependencies: @@ -3516,7 +4069,6 @@ snapshots: '@webassemblyjs/ieee754': 1.13.2 '@webassemblyjs/leb128': 1.13.2 '@webassemblyjs/utf8': 1.13.2 - optional: true '@webassemblyjs/wasm-opt@1.14.1': dependencies: @@ -3524,7 +4076,6 @@ snapshots: '@webassemblyjs/helper-buffer': 1.14.1 '@webassemblyjs/wasm-gen': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - optional: true '@webassemblyjs/wasm-parser@1.14.1': dependencies: @@ -3534,37 +4085,49 @@ snapshots: '@webassemblyjs/ieee754': 1.13.2 '@webassemblyjs/leb128': 1.13.2 '@webassemblyjs/utf8': 1.13.2 - optional: true '@webassemblyjs/wast-printer@1.14.1': dependencies: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - optional: true - '@xtuc/ieee754@1.2.0': - optional: true + '@xtuc/ieee754@1.2.0': {} - '@xtuc/long@4.2.2': - optional: true + '@xtuc/long@4.2.2': {} + + '@yr/monotone-cubic-spline@1.0.3': {} acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: acorn: 8.16.0 - optional: true + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} acorn@8.16.0: {} ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 - optional: true + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 ajv-keywords@5.1.0(ajv@8.18.0): dependencies: ajv: 8.18.0 fast-deep-equal: 3.1.3 - optional: true + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 ajv@8.18.0: dependencies: @@ -3572,9 +4135,9 @@ snapshots: fast-uri: 3.1.0 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - optional: true - alien-signals@3.1.2: {} + alien-signals@2.0.8: + optional: true ansi-regex@5.0.1: {} @@ -3582,29 +4145,34 @@ snapshots: dependencies: color-convert: 2.0.1 - apexcharts@5.10.4: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + apexcharts@3.42.0: + dependencies: + '@yr/monotone-cubic-spline': 1.0.3 + svg.draggable.js: 2.2.2 + svg.easing.js: 2.0.0 + svg.filter.js: 2.0.2 + svg.pathmorphing.js: 0.1.3 + svg.resize.js: 1.4.3 + svg.select.js: 3.0.1 argparse@2.0.1: {} - ast-kit@2.2.0: - dependencies: - '@babel/parser': 7.29.0 - pathe: 2.0.3 - - ast-walker-scope@0.8.3: - dependencies: - '@babel/parser': 7.29.0 - ast-kit: 2.2.0 + array-union@2.1.0: {} asynckit@0.4.0: {} - axios-mock-adapter@2.1.0(axios@1.13.6): + axios-mock-adapter@1.22.0(axios@1.13.5): dependencies: - axios: 1.13.6 + axios: 1.13.5 fast-deep-equal: 3.1.3 is-buffer: 2.0.5 - axios@1.13.6: + axios@1.13.5: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 @@ -3612,10 +4180,28 @@ snapshots: transitivePeerDependencies: - debug - baseline-browser-mapping@2.10.0: - optional: true + balanced-match@1.0.2: {} - birpc@2.9.0: {} + baseline-browser-mapping@2.10.0: {} + + big.js@5.2.2: {} + + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 browserslist@4.28.1: dependencies: @@ -3624,10 +4210,8 @@ snapshots: electron-to-chromium: 1.5.307 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) - optional: true - buffer-from@1.1.2: - optional: true + buffer-from@1.1.2: {} cacheable@2.3.3: dependencies: @@ -3642,42 +4226,54 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + callsite@1.0.0: {} + + callsites@3.1.0: {} + camelcase@5.3.1: {} - caniuse-lite@1.0.30001778: - optional: true + caniuse-lite@1.0.30001778: {} ccount@2.0.1: {} - chance@1.1.13: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chance@1.1.11: {} character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} - chevrotain-allstar@0.4.1(chevrotain@12.0.0): + chevrotain-allstar@0.3.1(chevrotain@11.0.3): dependencies: - chevrotain: 12.0.0 + chevrotain: 11.0.3 lodash-es: 4.17.23 - chevrotain@12.0.0: + chevrotain@11.0.3: dependencies: - '@chevrotain/cst-dts-gen': 12.0.0 - '@chevrotain/gast': 12.0.0 - '@chevrotain/regexp-to-ast': 12.0.0 - '@chevrotain/types': 12.0.0 - '@chevrotain/utils': 12.0.0 + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.23 - chokidar@4.0.3: + chokidar@3.6.0: dependencies: - readdirp: 4.1.2 + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - - chrome-trace-event@1.0.4: - optional: true + chrome-trace-event@1.0.4: {} clean-css@5.3.3: dependencies: @@ -3701,21 +4297,18 @@ snapshots: comma-separated-tokens@2.0.3: {} - commander@2.20.3: - optional: true + commander@2.20.3: {} commander@7.2.0: {} commander@8.3.0: {} + commondir@1.0.1: {} + + concat-map@0.0.1: {} + confbox@0.1.8: {} - confbox@0.2.4: {} - - copy-anything@4.0.5: - dependencies: - is-what: 5.5.0 - cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -3724,6 +4317,16 @@ snapshots: dependencies: layout-base: 2.0.1 + crelt@1.0.6: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + csstype@3.2.3: {} cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): @@ -3905,21 +4508,31 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 - dagre-d3-es@7.0.14: + dagre-d3-es@7.0.13: dependencies: d3: 7.9.0 lodash-es: 4.17.23 - date-fns@4.1.0: {} + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.28.6 dayjs@1.11.19: {} + de-indent@1.0.2: {} + debug@4.4.3: dependencies: ms: 2.1.3 + decache@4.6.2: + dependencies: + callsite: 1.0.0 + decamelize@1.2.0: {} + deep-is@0.1.4: {} + delaunator@5.0.1: dependencies: robust-predicates: 3.0.2 @@ -3928,19 +4541,21 @@ snapshots: dequal@2.0.3: {} - detect-libc@2.1.2: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 dijkstrajs@1.0.3: {} - dompurify@3.2.7: - optionalDependencies: - '@types/trusted-types': 2.0.7 + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 - dompurify@3.4.1: + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dompurify@3.3.2: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -3950,16 +4565,16 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.307: - optional: true + electron-to-chromium@1.5.307: {} emoji-regex@8.0.0: {} + emojis-list@3.0.0: {} + enhanced-resolve@5.20.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 - optional: true entities@4.5.0: {} @@ -3969,8 +4584,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.0.0: - optional: true + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: dependencies: @@ -3983,49 +4597,208 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - escalade@3.2.0: - optional: true + esbuild@0.25.12: + optionalDependencies: + '@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: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@8.10.2(eslint@8.48.0): + dependencies: + eslint: 8.48.0 + + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@8.48.0))(eslint@8.48.0)(prettier@3.0.2): + dependencies: + eslint: 8.48.0 + prettier: 3.0.2 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 8.10.2(eslint@8.48.0) + + eslint-plugin-vue@9.17.0(eslint@8.48.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.48.0) + eslint: 8.48.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.4 + vue-eslint-parser: 9.4.3(eslint@8.48.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 - optional: true + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.48.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.48.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.48.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 esrecurse@4.3.0: dependencies: estraverse: 5.3.0 - optional: true - estraverse@4.3.0: - optional: true + estraverse@4.3.0: {} - estraverse@5.3.0: - optional: true + estraverse@5.3.0: {} estree-walker@2.0.2: {} + esutils@2.0.3: {} + event-source-polyfill@1.0.31: {} - events@3.3.0: - optional: true - - exsolve@1.0.8: {} + events@3.3.0: {} fast-deep-equal@3.1.3: {} - fast-uri@3.1.0: - optional: true + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.20.1: + 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 + + file-loader@6.2.0(webpack@5.105.0): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.105.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + flat-cache@6.1.20: dependencies: cacheable: 2.3.3 @@ -4049,6 +4822,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true @@ -4074,20 +4849,49 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - glob-to-regexp@0.4.1: - optional: true + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 gopd@1.2.0: {} - graceful-fs@4.2.11: - optional: true + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} hachure-fill@0.5.2: {} - harfbuzzjs@0.10.3: {} + harfbuzzjs@0.4.15: {} - has-flag@4.0.0: - optional: true + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -4121,9 +4925,9 @@ snapshots: dependencies: '@types/hast': 3.0.4 - highlight.js@11.11.1: {} + he@1.2.0: {} - hookable@5.5.3: {} + highlight.js@11.11.1: {} hookified@1.15.1: {} @@ -4133,148 +4937,141 @@ snapshots: dependencies: safer-buffer: 2.1.2 - immutable@5.1.5: {} + ignore@5.3.2: {} + + immutable@4.3.8: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} internmap@1.0.1: {} internmap@2.0.3: {} + interpret@1.4.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-buffer@2.0.5: {} - is-extglob@2.1.1: - optional: true + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - optional: true - is-what@5.5.0: {} + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} jest-worker@27.5.1: dependencies: - '@types/node': 25.6.0 + '@types/node': 20.19.37 merge-stream: 2.0.0 supports-color: 8.1.1 - optional: true - js-md5@0.8.3: {} + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 - jsesc@3.1.0: {} + json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: - optional: true + json-parse-even-better-errors@2.3.1: {} - json-schema-traverse@1.0.0: - optional: true + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} - katex@0.16.45: + katex@0.16.28: dependencies: commander: 8.3.0 + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + keyv@5.6.0: dependencies: '@keyv/serialize': 1.1.1 khroma@2.1.0: {} - langium@4.2.2: + langium@3.3.1: dependencies: - '@chevrotain/regexp-to-ast': 12.0.0 - chevrotain: 12.0.0 - chevrotain-allstar: 0.4.1(chevrotain@12.0.0) + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) vscode-languageserver: 9.0.1 vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 + vscode-uri: 3.0.8 layout-base@1.0.2: {} layout-base@2.0.1: {} - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: + levn@0.4.1: dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 + prelude-ls: 1.2.1 + type-check: 0.4.0 linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 - linkifyjs@4.3.2: {} + loader-runner@4.3.1: {} - loader-runner@4.3.1: - optional: true - - local-pkg@1.1.2: + loader-utils@2.0.4: dependencies: - mlly: 1.8.0 - pkg-types: 2.3.0 - quansync: 0.2.11 + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 locate-path@5.0.0: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash-es@4.17.23: {} - lodash@4.18.1: {} + lodash.merge@4.6.2: {} - magic-string-ast@1.0.3: - dependencies: - magic-string: 0.30.21 + lodash@4.17.23: {} magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + markdown-it-container@4.0.0: {} markdown-it-footnote@4.0.0: {} @@ -4289,7 +5086,7 @@ snapshots: markdown-it-task-checkbox@1.0.6: {} - markdown-it-ts@0.0.7: + markdown-it-ts@0.0.3: dependencies: entities: 4.5.0 linkify-it: 5.0.0 @@ -4306,21 +5103,20 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 - marked@14.0.0: {} - marked@16.4.2: {} - markstream-vue@0.0.9(katex@0.16.45)(mermaid@11.14.0)(shiki@3.23.0)(stream-markdown@0.0.14(shiki@3.23.0))(vue-i18n@11.3.2(vue@3.5.31(typescript@6.0.3)))(vue@3.5.31(typescript@6.0.3)): + markstream-vue@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): dependencies: - '@floating-ui/dom': 1.7.6 - stream-markdown-parser: 0.0.72 - vue: 3.5.31(typescript@6.0.3) + '@floating-ui/dom': 1.7.5 + stream-markdown-parser: 0.0.59-beta.3 + vue: 3.3.4 optionalDependencies: - katex: 0.16.45 - mermaid: 11.14.0 - shiki: 3.23.0 - stream-markdown: 0.0.14(shiki@3.23.0) - vue-i18n: 11.3.2(vue@3.5.31(typescript@6.0.3)) + 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) math-intrinsics@1.1.0: {} @@ -4338,25 +5134,25 @@ snapshots: mdurl@2.0.0: {} - merge-stream@2.0.0: - optional: true + merge-stream@2.0.0: {} - mermaid@11.14.0: + merge2@1.4.1: {} + + mermaid@11.12.2: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.1.0 + '@mermaid-js/parser': 0.6.3 '@types/d3': 7.4.3 - '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.1 cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) cytoscape-fcose: 2.2.0(cytoscape@3.33.1) d3: 7.9.0 d3-sankey: 0.12.3 - dagre-d3-es: 7.0.14 + dagre-d3-es: 7.0.13 dayjs: 1.11.19 - dompurify: 3.4.1 - katex: 0.16.45 + dompurify: 3.3.2 + katex: 0.16.28 khroma: 2.1.0 lodash-es: 4.17.23 marked: 16.4.2 @@ -4382,39 +5178,63 @@ snapshots: micromark-util-types@2.0.2: {} + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mime-db@1.52.0: {} mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mitt@3.0.1: {} + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 mlly@1.8.0: dependencies: - acorn: 8.16.0 + acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.3 - monaco-editor@0.55.1: - dependencies: - dompurify: 3.2.7 - marked: 14.0.0 + monaco-editor@0.52.2: {} ms@2.1.3: {} - muggle-string@0.4.1: {} + muggle-string@0.3.1: {} nanoid@3.3.11: {} + natural-compare-lite@1.4.0: {} + + natural-compare@1.4.0: {} + neo-async@2.6.2: {} - node-addon-api@7.1.1: - optional: true + node-releases@2.0.36: {} - node-releases@2.0.36: - optional: true + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + null-loader@4.0.1(webpack@5.105.0): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.105.0 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 oniguruma-parser@0.12.1: {} @@ -4424,6 +5244,15 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + orderedmap@2.1.1: {} p-limit@2.3.0: @@ -4438,51 +5267,62 @@ snapshots: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-try@2.2.0: {} package-manager-detector@1.6.0: {} pako@1.0.11: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + path-browserify@1.0.1: {} path-data-parser@0.1.0: {} path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + pathe@2.0.3: {} - perfect-debounce@1.0.0: {} - - perfect-debounce@2.1.0: {} - picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.3: {} - picomatch@4.0.4: {} - - pinia@3.0.4(typescript@6.0.3)(vue@3.5.31(typescript@6.0.3)): + pinia@2.1.6(typescript@5.1.6)(vue@3.3.4): dependencies: - '@vue/devtools-api': 7.7.9 - vue: 3.5.31(typescript@6.0.3) + '@vue/devtools-api': 6.6.4 + vue: 3.3.4 + vue-demi: 0.14.10(vue@3.3.4) optionalDependencies: - typescript: 6.0.3 + typescript: 5.1.6 pinyin-pro@3.28.0: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 mlly: 1.8.0 pathe: 2.0.3 - pkg-types@2.3.0: - dependencies: - confbox: 0.2.4 - exsolve: 1.0.8 - pathe: 2.0.3 - pngjs@5.0.0: {} points-on-curve@0.2.0: {} @@ -4492,12 +5332,25 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss@8.5.10: + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.0.2: {} + property-expr@2.0.6: {} property-information@7.1.0: {} @@ -4506,6 +5359,10 @@ snapshots: dependencies: prosemirror-transform: 1.11.0 + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-commands@1.7.1: dependencies: prosemirror-model: 1.25.4 @@ -4532,15 +5389,37 @@ snapshots: prosemirror-view: 1.41.6 rope-sequence: 1.3.4 + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-keymap@1.2.3: dependencies: prosemirror-state: 1.4.4 w3c-keyname: 2.2.8 + prosemirror-markdown@1.13.4: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.3.0: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + prosemirror-model@1.25.4: dependencies: orderedmap: 2.1.1 + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-schema-list@1.5.1: dependencies: prosemirror-model: 1.25.4 @@ -4561,6 +5440,14 @@ snapshots: prosemirror-transform: 1.11.0 prosemirror-view: 1.41.6 + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + prosemirror-transform@1.11.0: dependencies: prosemirror-model: 1.25.4 @@ -4575,6 +5462,8 @@ snapshots: punycode.js@2.3.1: {} + punycode@2.3.1: {} + qified@0.6.0: dependencies: hookified: 1.15.1 @@ -4585,11 +5474,17 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - quansync@0.2.11: {} + querystring@0.2.1: {} - readdirp@4.1.2: {} + queue-microtask@1.2.3: {} - readdirp@5.0.0: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + rechoir@0.6.2: + dependencies: + resolve: 1.22.11 regex-recursion@6.0.2: dependencies: @@ -4603,38 +5498,56 @@ snapshots: require-directory@2.1.1: {} - require-from-string@2.0.2: - optional: true + require-from-string@2.0.2: {} require-main-filename@2.0.0: {} - rfdc@1.4.1: {} + resolve-from@4.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 robust-predicates@3.0.2: {} - rolldown@1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + rollup@4.59.0: dependencies: - '@oxc-project/types': 0.122.0 - '@rolldown/pluginutils': 1.0.0-rc.12 + '@types/estree': 1.0.8 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-x64': 1.0.0-rc.12 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@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: {} @@ -4645,24 +5558,32 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + rw@1.3.3: {} safer-buffer@2.1.2: {} - sass-loader@16.0.7(sass@1.98.0)(webpack@5.105.0): + sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0): dependencies: neo-async: 2.6.2 - optionalDependencies: - sass: 1.98.0 webpack: 5.105.0 - - sass@1.98.0: - dependencies: - chokidar: 4.0.3 - immutable: 5.1.5 - source-map-js: 1.2.1 optionalDependencies: - '@parcel/watcher': 2.5.6 + sass: 1.66.1 + + sass@1.66.1: + dependencies: + chokidar: 3.6.0 + immutable: 4.3.8 + source-map-js: 1.2.1 + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) schema-utils@4.3.3: dependencies: @@ -4670,40 +5591,52 @@ snapshots: ajv: 8.18.0 ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) - optional: true - scule@1.3.0: {} + semver@6.3.1: {} + + semver@7.7.4: {} set-blocking@2.0.0: {} - shiki@3.23.0: + shebang-command@2.0.0: dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/engine-javascript': 3.23.0 - '@shikijs/engine-oniguruma': 3.23.0 - '@shikijs/langs': 3.23.0 - '@shikijs/themes': 3.23.0 - '@shikijs/types': 3.23.0 + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + shiki@3.22.0: + dependencies: + '@shikijs/core': 3.22.0 + '@shikijs/engine-javascript': 3.22.0 + '@shikijs/engine-oniguruma': 3.22.0 + '@shikijs/langs': 3.22.0 + '@shikijs/themes': 3.22.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + slash@3.0.0: {} + source-map-js@1.2.1: {} source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - optional: true source-map@0.6.1: {} space-separated-tokens@2.0.2: {} - speakingurl@14.0.1: {} - state-local@1.0.7: {} - stream-markdown-parser@0.0.72: + stream-markdown-parser@0.0.59-beta.3: dependencies: markdown-it-container: 4.0.0 markdown-it-footnote: 4.0.0 @@ -4712,11 +5645,19 @@ snapshots: markdown-it-sub: 2.0.0 markdown-it-sup: 2.0.0 markdown-it-task-checkbox: 1.0.6 - markdown-it-ts: 0.0.7 + markdown-it-ts: 0.0.3 - stream-markdown@0.0.14(shiki@3.23.0): + stream-markdown@0.0.13(shiki@3.22.0): dependencies: - shiki: 3.23.0 + shiki: 3.22.0 + + stream-monaco@0.0.17(monaco-editor@0.52.2): + dependencies: + '@shikijs/monaco': 3.22.0 + alien-signals: 2.0.8 + monaco-editor: 0.52.2 + shiki: 3.22.0 + optional: true string-width@4.2.3: dependencies: @@ -4733,26 +5674,63 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-json-comments@3.1.1: {} + stylis@4.3.6: {} - subset-font@2.5.0: + subset-font@2.4.0: dependencies: fontverter: 2.0.0 - harfbuzzjs: 0.10.3 - lodash: 4.18.1 + harfbuzzjs: 0.4.15 + lodash: 4.17.23 p-limit: 3.1.0 - superjson@2.2.6: + supports-color@7.2.0: dependencies: - copy-anything: 4.0.5 + has-flag: 4.0.0 supports-color@8.1.1: dependencies: has-flag: 4.0.0 - optional: true - tapable@2.3.0: - optional: true + supports-preserve-symlinks-flag@1.0.0: {} + + svg.draggable.js@2.2.2: + dependencies: + svg.js: 2.7.1 + + svg.easing.js@2.0.0: + dependencies: + svg.js: 2.7.1 + + svg.filter.js@2.0.2: + dependencies: + svg.js: 2.7.1 + + svg.js@2.7.1: {} + + svg.pathmorphing.js@0.1.3: + dependencies: + svg.js: 2.7.1 + + svg.resize.js@1.4.3: + dependencies: + svg.js: 2.7.1 + svg.select.js: 2.1.2 + + svg.select.js@2.1.2: + dependencies: + svg.js: 2.7.1 + + svg.select.js@3.0.1: + dependencies: + svg.js: 2.7.1 + + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + + tapable@2.3.0: {} terser-webpack-plugin@5.4.0(webpack@5.105.0): dependencies: @@ -4761,7 +5739,6 @@ snapshots: schema-utils: 4.3.3 terser: 5.46.0 webpack: 5.105.0 - optional: true terser@5.46.0: dependencies: @@ -4769,7 +5746,8 @@ snapshots: acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 - optional: true + + text-table@0.2.0: {} tiny-case@1.0.3: {} @@ -4780,26 +5758,44 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toposort@2.0.2: {} trim-lines@3.0.1: {} ts-dedent@2.2.0: {} - tslib@2.8.1: - optional: true + tslib@1.14.1: {} + + tsutils@3.21.0(typescript@5.1.6): + dependencies: + tslib: 1.14.1 + typescript: 5.1.6 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} type-fest@2.19.0: {} type-fest@4.41.0: {} - typescript@6.0.3: {} + typescript@5.1.6: {} uc.micro@2.1.0: {} ufo@1.6.3: {} - undici-types@7.19.2: {} + undici-types@6.21.0: {} unist-util-is@6.0.1: dependencies: @@ -4824,17 +5820,6 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - unplugin-utils@0.3.1: - dependencies: - pathe: 2.0.3 - picomatch: 4.0.3 - - unplugin@3.0.0: - dependencies: - '@jridgewell/remapping': 2.3.5 - picomatch: 4.0.3 - webpack-virtual-modules: 0.6.2 - upath@2.0.1: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -4842,15 +5827,20 @@ snapshots: browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 - optional: true + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} uuid@11.1.0: {} - vee-validate@4.15.1(vue@3.5.31(typescript@6.0.3)): + vee-validate@4.11.3(vue@3.3.4): dependencies: - '@vue/devtools-api': 7.7.9 + '@vue/devtools-api': 6.6.4 type-fest: 4.41.0 - vue: 3.5.31(typescript@6.0.3) + vue: 3.3.4 vfile-message@4.0.3: dependencies: @@ -4862,43 +5852,40 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-vuetify@2.1.3(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3))(vue@3.5.31(typescript@6.0.3))(vuetify@4.0.4): + 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': 2.1.2(vue@3.5.31(typescript@6.0.3))(vuetify@4.0.4) + '@vuetify/loader-shared': 2.1.2(vue@3.3.4)(vuetify@3.7.11) debug: 4.4.3 upath: 2.0.1 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3) - vue: 3.5.31(typescript@6.0.3) - vuetify: 4.0.4(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.31(typescript@6.0.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(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4) transitivePeerDependencies: - supports-color - vite-plugin-webfont-dl@3.12.0(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3)): + vite-plugin-webfont-dl@3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)): dependencies: - axios: 1.13.6 + axios: 1.13.5 clean-css: 5.3.3 flat-cache: 6.1.20 picocolors: 1.1.1 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) transitivePeerDependencies: - debug - vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3): + vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.10 - rolldown: 1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.6.0 + '@types/node': 20.19.32 fsevents: 2.3.3 - sass: 1.98.0 + sass: 1.66.1 terser: 5.46.0 - yaml: 2.8.3 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' vscode-jsonrpc@8.2.0: {} @@ -4917,77 +5904,97 @@ snapshots: vscode-uri@3.0.8: {} - vscode-uri@3.1.0: {} - - vue-demi@0.14.10(vue@3.5.31(typescript@6.0.3)): + vue-cli-plugin-vuetify@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): dependencies: - vue: 3.5.31(typescript@6.0.3) - - vue-i18n@11.3.2(vue@3.5.31(typescript@6.0.3)): - dependencies: - '@intlify/core-base': 11.3.2 - '@intlify/devtools-types': 11.3.2 - '@intlify/shared': 11.3.2 - '@vue/devtools-api': 6.6.4 - vue: 3.5.31(typescript@6.0.3) - - vue-router@5.0.4(@vue/compiler-sfc@3.5.33)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.31(typescript@6.0.3)))(vue@3.5.31(typescript@6.0.3)): - dependencies: - '@babel/generator': 7.29.1 - '@vue-macros/common': 3.1.2(vue@3.5.31(typescript@6.0.3)) - '@vue/devtools-api': 8.1.1 - ast-walker-scope: 0.8.3 - chokidar: 5.0.0 - json5: 2.2.3 - local-pkg: 1.1.2 - magic-string: 0.30.21 - mlly: 1.8.0 - muggle-string: 0.4.1 - pathe: 2.0.3 - picomatch: 4.0.3 - scule: 1.3.0 - tinyglobby: 0.2.15 - unplugin: 3.0.0 - unplugin-utils: 0.3.1 - vue: 3.5.31(typescript@6.0.3) - yaml: 2.8.3 + null-loader: 4.0.1(webpack@5.105.0) + semver: 7.7.4 + shelljs: 0.8.5 + vue: 3.3.4 + webpack: 5.105.0 optionalDependencies: - '@vue/compiler-sfc': 3.5.33 - pinia: 3.0.4(typescript@6.0.3)(vue@3.5.31(typescript@6.0.3)) + sass-loader: 13.3.2(sass@1.66.1)(webpack@5.105.0) + 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) - vue-tsc@3.2.7(typescript@6.0.3): + vue-demi@0.14.10(vue@3.3.4): dependencies: - '@volar/typescript': 2.4.28 - '@vue/language-core': 3.2.7 - typescript: 6.0.3 + vue: 3.3.4 - vue3-apexcharts@1.11.1(apexcharts@5.10.4)(vue@3.5.31(typescript@6.0.3)): + vue-eslint-parser@9.4.3(eslint@8.48.0): dependencies: - apexcharts: 5.10.4 - vue: 3.5.31(typescript@6.0.3) - - vue3-print-nb@0.1.4(typescript@6.0.3): - dependencies: - vue: 3.5.31(typescript@6.0.3) + debug: 4.4.3 + eslint: 8.48.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + lodash: 4.17.23 + semver: 7.7.4 transitivePeerDependencies: - - typescript + - supports-color - vue@3.5.31(typescript@6.0.3): + vue-i18n@11.2.8(vue@3.3.4): dependencies: - '@vue/compiler-dom': 3.5.31 - '@vue/compiler-sfc': 3.5.31 - '@vue/runtime-dom': 3.5.31 - '@vue/server-renderer': 3.5.31(vue@3.5.31(typescript@6.0.3)) - '@vue/shared': 3.5.31 - optionalDependencies: - typescript: 6.0.3 + '@intlify/core-base': 11.2.8 + '@intlify/shared': 11.2.8 + '@vue/devtools-api': 6.6.4 + vue: 3.3.4 - vuetify@4.0.4(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.31(typescript@6.0.3)): + vue-router@4.2.4(vue@3.3.4): dependencies: - vue: 3.5.31(typescript@6.0.3) + '@vue/devtools-api': 6.6.4 + vue: 3.3.4 + + vue-template-compiler@2.7.16: + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + vue-tsc@1.8.8(typescript@5.1.6): + dependencies: + '@vue/language-core': 1.8.8(typescript@5.1.6) + '@vue/typescript': 1.8.8(typescript@5.1.6) + semver: 7.7.4 + typescript: 5.1.6 + + vue3-apexcharts@1.4.4(apexcharts@3.42.0)(vue@3.3.4): + dependencies: + apexcharts: 3.42.0 + vue: 3.3.4 + + vue3-print-nb@0.1.4: + dependencies: + vue: 3.3.4 + + vue@3.3.4: + dependencies: + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-sfc': 3.3.4 + '@vue/runtime-dom': 3.3.4 + '@vue/server-renderer': 3.3.4(vue@3.3.4) + '@vue/shared': 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): + dependencies: + '@vue/compiler-sfc': 3.3.4 + '@vuetify/loader-shared': 1.7.1(vue@3.3.4)(vuetify@3.7.11) + decache: 4.6.2 + file-loader: 6.2.0(webpack@5.105.0) + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + 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@2.1.3)(vue@3.3.4) + webpack: 5.105.0 + transitivePeerDependencies: + - vue + + 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: 6.0.3 - vite-plugin-vuetify: 2.1.3(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@25.6.0)(sass@1.98.0)(terser@5.46.0)(yaml@2.8.3))(vue@3.5.31(typescript@6.0.3))(vuetify@4.0.4) + typescript: 5.1.6 + 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: {} @@ -4995,16 +6002,12 @@ snapshots: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 - optional: true wawoff2@2.0.1: dependencies: argparse: 2.0.1 - webpack-sources@3.3.4: - optional: true - - webpack-virtual-modules@0.6.2: {} + webpack-sources@3.3.4: {} webpack@5.105.0: dependencies: @@ -5037,23 +6040,30 @@ snapshots: - '@swc/core' - esbuild - uglify-js - optional: true which-module@2.0.1: {} + which@2.0.2: + dependencies: + isexe: 2.0.0 + woff2sfnt-sfnt2woff@1.0.0: dependencies: pako: 1.0.11 + word-wrap@1.2.5: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - y18n@4.0.3: {} + wrappy@1.0.2: {} - yaml@2.8.3: {} + xml-name-validator@4.0.0: {} + + y18n@4.0.3: {} yargs-parser@18.1.3: dependencies: @@ -5076,7 +6086,7 @@ snapshots: yocto-queue@0.1.0: {} - yup@1.7.1: + yup@1.2.0: dependencies: property-expr: 2.0.6 tiny-case: 1.0.3 diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index f78c0f90c..c4ec29145 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -1,4 +1,4 @@ -/* Auto-generated MDI subset – 271 icons */ +/* Auto-generated MDI subset – 276 icons */ /* Do not edit manually. Run: pnpm run subset-icons */ @font-face { @@ -28,6 +28,10 @@ content: "\F0009"; } +.mdi-account-edit::before { + content: "\F06BC"; +} + .mdi-account-edit-outline::before { content: "\F0FFB"; } @@ -660,6 +664,10 @@ content: "\F033E"; } +.mdi-lock-check::before { + content: "\F139A"; +} + .mdi-lock-check-outline::before { content: "\F16A8"; } @@ -668,6 +676,10 @@ content: "\F0341"; } +.mdi-lock-plus::before { + content: "\F05FB"; +} + .mdi-lock-plus-outline::before { content: "\F16B2"; } @@ -844,6 +856,10 @@ content: "\F0995"; } +.mdi-progress-download::before { + content: "\F0997"; +} + .mdi-puzzle::before { content: "\F0431"; } @@ -1024,6 +1040,10 @@ content: "\F051B"; } +.mdi-timer-sand::before { + content: "\F051F"; +} + .mdi-tools::before { content: "\F1064"; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index d637a1423..a43b0c09a 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff differ diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 index e714d8858..b1ae12e09 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 differ diff --git a/dashboard/src/components/platform/AddNewPlatform.vue b/dashboard/src/components/platform/AddNewPlatform.vue index fecf8b959..4e8225b6e 100644 --- a/dashboard/src/components/platform/AddNewPlatform.vue +++ b/dashboard/src/components/platform/AddNewPlatform.vue @@ -71,16 +71,98 @@
- - mdi-book-open-variant - {{ tm("dialog.viewTutorial") }} - -
+
+
+ {{ tm("registrationAction.mode.title") }} +
+ + + + + +
+ +
+ +
+
+ + mdi-book-open-variant + {{ tm("dialog.viewTutorial") }} + +
+ +
+
+ +
+
+ {{ tm("registrationAction.mode.title") }} +
+ + + + + +
+ +
+ +
+
+ + mdi-book-open-variant + {{ tm("dialog.viewTutorial") }} + +
+ +
+
+ +
+ +
+ +
+
+ + mdi-book-open-variant + {{ tm("dialog.viewTutorial") }} + +
| null, + larkCreationMode: "", + dingtalkCreationMode: "", aBConfigRadioVal: "0", selectedAbConfId: null as string | null, @@ -667,6 +752,39 @@ export default { if (!this.isPlatformIdValid(cfg?.id as string | undefined)) { return false; } + + if (this.isLarkPlatform && !this.larkCreationMode) { + return false; + } + + if (this.isLarkPlatform && this.larkCreationMode === "scan") { + const cfg = this.selectedPlatformConfig as Record | null; + const appId = cfg?.app_id as string | undefined; + const appSecret = cfg?.app_secret as string | undefined; + if (!appId || !appSecret) { + return false; + } + } + + if (this.isDingtalkPlatform && !this.dingtalkCreationMode) { + return false; + } + + if (this.isDingtalkPlatform && this.dingtalkCreationMode === "scan") { + const cfg = this.selectedPlatformConfig as Record | null; + const clientId = cfg?.client_id as string | undefined; + const clientSecret = cfg?.client_secret as string | undefined; + if (!clientId || !clientSecret) { + return false; + } + } + + const weixinOcToken = this.selectedPlatformConfig?.weixin_oc_token as string | undefined; + if (this.isWeixinOcPlatform && !weixinOcToken) { + return false; + } + + // 如果是使用现有配置文件模式 if (this.aBConfigRadioVal === "0") { return !!this.selectedAbConfId; } @@ -725,16 +843,28 @@ export default { }, ]; }, + isLarkPlatform(): boolean { + return this.selectedPlatformConfig?.type === "lark"; + }, + isWeixinOcPlatform(): boolean { + return this.selectedPlatformConfig?.type === "weixin_oc"; + }, + isDingtalkPlatform(): boolean { + return this.selectedPlatformConfig?.type === "dingtalk"; + } }, watch: { selectedPlatformType(newType: string | null) { - const templates = this.platformTemplates as Record; - if (newType && templates[newType]) { + if (newType && this.platformTemplates[newType]) { this.selectedPlatformConfig = JSON.parse( - JSON.stringify(templates[newType]), + JSON.stringify(this.platformTemplates[newType]), ) as Record; + this.larkCreationMode = ""; + this.dingtalkCreationMode = ""; } else { this.selectedPlatformConfig = null; + this.larkCreationMode = ""; + this.dingtalkCreationMode = ""; } }, selectedAbConfId(newConfigId: string | null) { @@ -838,6 +968,8 @@ export default { resetForm(): void { this.selectedPlatformType = null; this.selectedPlatformConfig = null; + this.larkCreationMode = ""; + this.dingtalkCreationMode = ""; this.aBConfigRadioVal = "0"; this.selectedAbConfId = "default"; @@ -1144,6 +1276,56 @@ export default { this.$emit("show-toast", { message, type: "error" } as ToastPayload); }, + buildRandomPlatformIdSuffix(): string { + const letters = "abcdefghijklmnopqrstuvwxyz"; + let suffix = "_"; + for (let i = 0; i < 4; i += 1) { + suffix += letters[Math.floor(Math.random() * letters.length)]; + } + return suffix; + }, + + handlePlatformRegistrationCreated(data: Record): void { + if (!this.selectedPlatformConfig || !data) { + return; + } + const rawId = this.selectedPlatformConfig.id as string | undefined; + const currentId = String(rawId || "").trim(); + const platformType = this.selectedPlatformConfig.type as string | undefined; + if (!currentId) { + return; + } + + let suffix = ""; + const platformIdSuffix = data.platform_id_suffix as string | undefined; + const explicitSuffix = String(platformIdSuffix || "").trim().replace(/[!:]/g, "_"); + const botName = data.bot_name as string | undefined; + if (explicitSuffix) { + suffix = explicitSuffix.startsWith("_") || explicitSuffix.startsWith("-") + ? explicitSuffix + : `_${explicitSuffix}`; + } else if (botName) { + const safeBotName = String(botName || "").trim().replace(/[!:]/g, "_"); + if (safeBotName) { + suffix = `-${safeBotName}`; + } + } else if (platformType === "weixin_oc" || platformType === "dingtalk") { + suffix = this.buildRandomPlatformIdSuffix(); + } + + if (!suffix) { + return; + } + + if ((platformType === "weixin_oc" || platformType === "dingtalk") && /_[a-z]{4}$/.test(currentId)) { + return; + } + + this.selectedPlatformConfig.id = currentId.endsWith(suffix) + ? currentId + : `${currentId}${suffix}`; + }, + isPlatformIdValid(id: string | null | undefined): boolean { if (!id) { return false; @@ -1416,6 +1598,30 @@ export default { padding: 16px 16px 24px 16px; } +.platform-action-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.creation-mode-group .v-label { + opacity: 0.9; +} + +.creation-mode-title { + font-size: 14px; + font-weight: 600; + color: rgba(0, 0, 0, 0.78); +} + +.registration-inline { + display: flex; + align-items: flex-start; + justify-content: flex-start; + width: 320px; +} + @media (max-width: 700px) { .add-platform-body { padding: 16px !important; diff --git a/dashboard/src/components/platform/PlatformRegistrationAction.vue b/dashboard/src/components/platform/PlatformRegistrationAction.vue new file mode 100644 index 000000000..ba40ac60c --- /dev/null +++ b/dashboard/src/components/platform/PlatformRegistrationAction.vue @@ -0,0 +1,397 @@ + + + + + diff --git a/dashboard/src/components/shared/ConsoleDisplayer.vue b/dashboard/src/components/shared/ConsoleDisplayer.vue index e46c5de61..b0ee26f8b 100644 --- a/dashboard/src/components/shared/ConsoleDisplayer.vue +++ b/dashboard/src/components/shared/ConsoleDisplayer.vue @@ -397,6 +397,7 @@ export default { border-radius: 8px; height: 100%; overflow-y: auto; + overflow-x: auto; padding: 16px; } @@ -416,7 +417,7 @@ export default { :deep(.console-log-line--structured) { display: grid; - grid-template-columns: max-content 10ch minmax(0, 1fr); + grid-template-columns: max-content max-content minmax(0, 1fr); column-gap: 8px; align-items: start; white-space: normal; @@ -437,6 +438,16 @@ export default { overflow-wrap: anywhere; } +@media (max-width: 768px) { + :deep(.console-log-line--structured) { + grid-template-columns: 1fr; + } + :deep(.console-log-prefix:empty), + :deep(.console-log-level:empty) { + display: none; + } +} + :deep(.fade-in) { animation: fadeIn 0.3s; } diff --git a/dashboard/src/components/shared/Logo.vue b/dashboard/src/components/shared/Logo.vue index 27fb76f4e..2b058ff11 100644 --- a/dashboard/src/components/shared/Logo.vue +++ b/dashboard/src/components/shared/Logo.vue @@ -50,7 +50,7 @@ const formatTitle = (title: string) => { diff --git a/dashboard/src/router/AuthRoutes.ts b/dashboard/src/router/AuthRoutes.ts index 7f3007e22..e0e03f373 100644 --- a/dashboard/src/router/AuthRoutes.ts +++ b/dashboard/src/router/AuthRoutes.ts @@ -10,6 +10,11 @@ const AuthRoutes = { path: "/auth/login", component: () => import("@/views/authentication/auth/LoginPage.vue"), }, + { + name: "Setup", + path: "/auth/setup", + component: () => import("@/views/authentication/auth/SetupPage.vue"), + }, ], }; diff --git a/dashboard/src/router/index.ts b/dashboard/src/router/index.ts index cb0e13ed1..0979add68 100644 --- a/dashboard/src/router/index.ts +++ b/dashboard/src/router/index.ts @@ -24,7 +24,8 @@ router.beforeEach(async (to, from) => { loadingStore.start(); } - const publicPages = ["/auth/login"]; + const publicPages = ["/auth/login", "/auth/setup"]; + const authRequired = !publicPages.includes(to.path); const auth: AuthStore = useAuthStore(); diff --git a/dashboard/src/scss/_variables.scss b/dashboard/src/scss/_variables.scss index fbc6ec55a..fc97ffa63 100644 --- a/dashboard/src/scss/_variables.scss +++ b/dashboard/src/scss/_variables.scss @@ -9,7 +9,7 @@ $color-pack: false; // Global font size and border radius $font-size-root: 1rem; $border-radius-root: 8px; -$cjk-sans-fallback: 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Microsoft YaHei' !default; +$cjk-sans-fallback: 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Microsoft YaHei', 'Noto Sans SC' !default; $cjk-mono-fallback: 'PingFang SC', 'PingFang TC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Microsoft YaHei' !default; $code-text-color: #111827 !default; @@ -19,7 +19,8 @@ $code-text-color: #111827 !default; --astrbot-code-color: #{$code-text-color}; } -$body-font-family: 'Roboto', $cjk-sans-fallback, sans-serif !default; +$body-font-family: 'Outfit', 'Noto Sans', $cjk-sans-fallback, sans-serif !default; + $heading-font-family: $body-font-family !default; $btn-font-weight: 400 !default; $btn-letter-spacing: 0 !default; diff --git a/dashboard/src/scss/layout/_container.scss b/dashboard/src/scss/layout/_container.scss index 8f095211a..994fbc71f 100644 --- a/dashboard/src/scss/layout/_container.scss +++ b/dashboard/src/scss/layout/_container.scss @@ -90,7 +90,7 @@ body { } .Outfit { - font-family: 'Outfit', $cjk-sans-fallback, sans-serif !important; + font-family: $body-font-family !important; } } diff --git a/dashboard/src/stores/auth.ts b/dashboard/src/stores/auth.ts index c8ac749ad..62b1a2240 100644 --- a/dashboard/src/stores/auth.ts +++ b/dashboard/src/stores/auth.ts @@ -9,6 +9,43 @@ export const useAuthStore = defineStore("auth", { returnUrl: null as string | null, }), actions: { + async finishAuthenticatedSession(data: any): Promise { + this.username = data.username; + localStorage.setItem("user", this.username); + localStorage.setItem("token", data.token); + const passwordUpgradeRequired = !!data?.password_upgrade_required; + const passwordWarning = + !!data?.change_pwd_hint || + (!!data?.legacy_pwd_hint && !passwordUpgradeRequired); + if (passwordWarning) { + localStorage.setItem("change_pwd_hint", "true"); + if (data?.legacy_pwd_hint && !passwordUpgradeRequired) { + localStorage.setItem("legacy_pwd_hint", "true"); + } else { + localStorage.removeItem("legacy_pwd_hint"); + } + } else { + localStorage.removeItem("change_pwd_hint"); + localStorage.removeItem("legacy_pwd_hint"); + } + if (passwordUpgradeRequired) { + localStorage.setItem("password_upgrade_required", "true"); + } else { + localStorage.removeItem("password_upgrade_required"); + } + + const onboardingCompleted = await this.checkOnboardingCompleted(); + this.returnUrl = null; + if (passwordWarning) { + router.push("/auth/setup"); + return; + } + if (onboardingCompleted) { + router.push("/dashboard/default"); + } else { + router.push("/welcome"); + } + }, async login(username: string, password: string): Promise { try { const challengeRes = await axios.post("/api/auth/login/challenge"); @@ -36,28 +73,25 @@ export const useAuthStore = defineStore("auth", { return Promise.reject(res.data.message); } - this.username = res.data.data.username; - localStorage.setItem("user", this.username); - localStorage.setItem("token", res.data.data.token); - if (res.data.data?.change_pwd_hint || res.data.data?.legacy_pwd_hint) { - localStorage.setItem("change_pwd_hint", "true"); - if (res.data.data?.legacy_pwd_hint) { - localStorage.setItem("legacy_pwd_hint", "true"); - } else { - localStorage.removeItem("legacy_pwd_hint"); - } - } else { - localStorage.removeItem("change_pwd_hint"); - localStorage.removeItem("legacy_pwd_hint"); + await this.finishAuthenticatedSession(res.data.data); + } catch (error) { + return Promise.reject(error); + } + }, + async setup(username: string, password: string, confirmPassword: string): Promise { + try { + const setupEndpoint = this.has_token() ? "/api/auth/setup-authenticated" : "/api/auth/setup"; + const res = await axios.post(setupEndpoint, { + username: username, + password: password, + confirm_password: confirmPassword, + }); + + if (res.data.status === "error") { + return Promise.reject(res.data.message); } - const onboardingCompleted = await this.checkOnboardingCompleted(); - this.returnUrl = null; - if (onboardingCompleted) { - router.push("/dashboard/default"); - } else { - router.push("/welcome"); - } + await this.finishAuthenticatedSession(res.data.data); } catch (error) { return Promise.reject(error); } @@ -99,6 +133,7 @@ export const useAuthStore = defineStore("auth", { localStorage.removeItem("token"); localStorage.removeItem("change_pwd_hint"); localStorage.removeItem("legacy_pwd_hint"); + localStorage.removeItem("password_upgrade_required"); void axios.post("/api/auth/logout").catch(() => undefined); router.push("/auth/login"); }, diff --git a/dashboard/src/views/ConsolePage.vue b/dashboard/src/views/ConsolePage.vue index 378bf2ae5..a18c327a2 100644 --- a/dashboard/src/views/ConsolePage.vue +++ b/dashboard/src/views/ConsolePage.vue @@ -73,6 +73,11 @@ export default { status: '' } }, + mounted() { + if (this.$refs.consoleDisplayer) { + this.$refs.consoleDisplayer.autoScroll = this.autoScrollEnabled; + } + }, watch: { autoScrollEnabled(val) { localStorage.setItem('console_auto_scroll', val); diff --git a/dashboard/src/views/authentication/auth/LoginPage.vue b/dashboard/src/views/authentication/auth/LoginPage.vue index 152919fe2..d45cceb02 100644 --- a/dashboard/src/views/authentication/auth/LoginPage.vue +++ b/dashboard/src/views/authentication/auth/LoginPage.vue @@ -12,6 +12,7 @@ import { useTheme } from "vuetify"; import { useI18n, useModuleI18n } from "@/i18n/composables"; import { useToast } from "@/utils/toast"; import { getApiBaseUrlValidationError, normalizeConfiguredApiBaseUrl } from "@/utils/request"; +import axios from "@/utils/request"; const vuetifyTheme = useTheme(); const isDark = computed( @@ -104,7 +105,7 @@ function toggleTheme() { customizer.TOGGLE_DARK_MODE(); } -onMounted(() => { +onMounted(async () => { // 应用URL参数(用于分享预设配置) applyUrlParams(); @@ -114,6 +115,19 @@ onMounted(() => { return; } + try { + const setupStatus = await axios.get("/api/auth/setup-status"); + if ( + setupStatus.data?.data?.setup_required && + setupStatus.data?.data?.skip_default_password_auth + ) { + router.push("/auth/setup"); + return; + } + } catch { + // Keep the normal login flow if setup status is unavailable. + } + // 添加一个小延迟以获得更好的动画效果 setTimeout(() => { cardVisible.value = true; diff --git a/dashboard/src/views/authentication/auth/SetupPage.vue b/dashboard/src/views/authentication/auth/SetupPage.vue new file mode 100644 index 000000000..ae8f11d03 --- /dev/null +++ b/dashboard/src/views/authentication/auth/SetupPage.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/dashboard/src/views/authentication/authForms/AuthLogin.vue b/dashboard/src/views/authentication/authForms/AuthLogin.vue index ce66a1331..1944d5494 100644 --- a/dashboard/src/views/authentication/authForms/AuthLogin.vue +++ b/dashboard/src/views/authentication/authForms/AuthLogin.vue @@ -90,13 +90,13 @@ async function validate(_values: any, { setErrors }: any) { required variant="outlined" hide-details="auto" - :append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'" + :append-inner-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'" :type="show1 ? 'text' : 'password'" class="pwd-input" prepend-inner-icon="mdi-lock" :disabled="loading" autocomplete="current-password" - @click:append="show1 = !show1" + @click:append-inner="show1 = !show1" />
diff --git a/dashboard/src/views/authentication/authForms/AuthSetup.vue b/dashboard/src/views/authentication/authForms/AuthSetup.vue new file mode 100644 index 000000000..1063782e5 --- /dev/null +++ b/dashboard/src/views/authentication/authForms/AuthSetup.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index cad68ec06..458bc8dab 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -343,6 +343,7 @@ export default defineConfig({ { text: "Mattermost", link: "/mattermost" }, { text: "Misskey", link: "/misskey" }, { text: "Discord", link: "/discord" }, + { text: "KOOK", link: "/kook" }, { text: "Satori", base: "/en/platform/satori", @@ -357,7 +358,6 @@ export default defineConfig({ collapsed: false, items: [ { text: "Matrix", link: "/matrix" }, - { text: "KOOK", link: "/kook" }, { text: "VoceChat", link: "/vocechat" }, ], }, diff --git a/docs/en/deploy/astrbot/cli.md b/docs/en/deploy/astrbot/cli.md index cc8cd8b1a..7f4c8fc6e 100644 --- a/docs/en/deploy/astrbot/cli.md +++ b/docs/en/deploy/astrbot/cli.md @@ -86,7 +86,7 @@ If there are no errors, you will see a log message similar to `🌈 Dashboard st > [!TIP] > If you are deploying AstrBot on a server, you need to replace `localhost` with your server's IP address. > -> The default username and password are `astrbot` and `astrbot`. +> New users must use the random password printed in the startup logs to log in for the first time. Use the username shown in the logs (usually `astrbot`) and change it after first login. Next, you need to deploy any messaging platform to use AstrBot on that platform. diff --git a/docs/en/deploy/astrbot/compshare.md b/docs/en/deploy/astrbot/compshare.md index c985830a6..08b07b7db 100644 --- a/docs/en/deploy/astrbot/compshare.md +++ b/docs/en/deploy/astrbot/compshare.md @@ -35,7 +35,7 @@ You can find the public IP in Console -> Basic Network (Public). ![WebUI](https://www-s.ucloud.cn/2025/07/7e9fc6edc1dfa916abc069f4cecc24cf_1753940381771.png) -Login with username `astrbot` and password `astrbot`. +Use the random password printed in startup logs for first-time login, and use the username shown in the logs (usually `astrbot`). Change it immediately after login. After logging in, you can reset your password and continue setup. diff --git a/docs/en/deploy/astrbot/docker.md b/docs/en/deploy/astrbot/docker.md index 7ce973a53..e4a347214 100644 --- a/docs/en/deploy/astrbot/docker.md +++ b/docs/en/deploy/astrbot/docker.md @@ -84,7 +84,7 @@ If there are no errors, you will see a log message similar to `🌈 Dashboard st > [!TIP] > Since Docker isolates the network environment, you cannot use `localhost` to access the dashboard. > -> The default username and password are `astrbot` and `astrbot`. +> New users must use the random password printed in the startup logs to log in for the first time. Use the username shown in the logs (usually `astrbot`) and change the password after first login. > > If deployed on a cloud server, you need to open ports `6180-6200` and `11451` in the cloud provider's console. diff --git a/docs/en/deploy/astrbot/kubernetes.md b/docs/en/deploy/astrbot/kubernetes.md index 39f51f1f2..5d34fe03b 100644 --- a/docs/en/deploy/astrbot/kubernetes.md +++ b/docs/en/deploy/astrbot/kubernetes.md @@ -194,4 +194,4 @@ Edit the `02-deployment.yaml` file and add `volumes` and `volumeMounts` under `s After deploying and exposing the service, you can access the AstrBot admin panel through the corresponding IP and port. -> The default username and password are `astrbot` and `astrbot`. +> New users must use the random password printed in the startup logs for the first login. Use the username shown in the logs (usually `astrbot`) and change it after logging in. diff --git a/docs/en/deploy/astrbot/launcher.md b/docs/en/deploy/astrbot/launcher.md index 87ef896a3..85a6aa031 100644 --- a/docs/en/deploy/astrbot/launcher.md +++ b/docs/en/deploy/astrbot/launcher.md @@ -66,7 +66,7 @@ If everything works, you will see AstrBot logs. Without errors, you should see a log like `🌈 Management panel started, accessible at` with several URLs. Open one URL to access AstrBot WebUI. > [!TIP] -> Default username and password: `astrbot` / `astrbot`. +> First-time logins use the random password generated on startup and printed to logs. Use that password (and the username shown in the logs, usually `astrbot`) to log in, then change it immediately. > > If WebUI returns 404: > Download `dist.zip` from [release](https://github.com/AstrBotDevs/AstrBot/releases), extract it into `AstrBot/data`, then restart the computer if needed. diff --git a/docs/en/dev/astrbot-config.md b/docs/en/dev/astrbot-config.md index e27111e76..eb5fc8903 100644 --- a/docs/en/dev/astrbot-config.md +++ b/docs/en/dev/astrbot-config.md @@ -116,7 +116,7 @@ The default AstrBot configuration is as follows: "dashboard": { "enable": True, "username": "astrbot", - "password": "77b90590a8945a7d36c963981a307dc9", + "password": "", "jwt_secret": "", "host": "0.0.0.0", "port": 6185, @@ -492,11 +492,11 @@ List of addresses that bypass the proxy. E.g., `["localhost", "127.0.0.1"]`. AstrBot WebUI configuration. -Please do not change the `password` value arbitrarily. It is an `md5` encoded password. Change the password in the control panel. +Please do not change the `password` value arbitrarily. It is an `md5` encoded password generated from the random initial password. Check the startup logs for that initial password on first run, then change it in the control panel. - `enable`: Whether to enable the AstrBot WebUI. Default is `true`. -- `username`: Username for the AstrBot WebUI. Default is `astrbot`. -- `password`: Password for the AstrBot WebUI. Default is the `md5` encoded value of `astrbot`. Do not modify directly unless you know what you are doing. +- `username`: Username for the AstrBot WebUI. +- `password`: Password for the AstrBot WebUI. It is initialized from a random password generated on first startup (logged at startup). Do not modify directly unless you know what you are doing. - `jwt_secret`: JWT secret key. AstrBot generates this randomly at initialization. Do not modify unless you know what you are doing. - `host`: Address the AstrBot WebUI listens on. Default is `0.0.0.0`. - `port`: Port the AstrBot WebUI listens on. Default is `6185`. diff --git a/docs/en/dev/plugin-platform-adapter.md b/docs/en/dev/plugin-platform-adapter.md index acbe77a41..4772b6e18 100644 --- a/docs/en/dev/plugin-platform-adapter.md +++ b/docs/en/dev/plugin-platform-adapter.md @@ -2,63 +2,63 @@ outline: deep --- -# 开发一个平台适配器 +# Developing a Platform Adapter -AstrBot 支持以插件的形式接入平台适配器,你可以自行接入 AstrBot 没有的平台。如飞书、钉钉甚至是哔哩哔哩私信、Minecraft。 +AstrBot supports integrating platform adapters in plugin form, allowing you to connect platforms that AstrBot does not natively support — such as Lark, DingTalk, Bilibili private messages, or even Minecraft. -我们以一个平台 `FakePlatform` 为例展开讲解。 +We will use a platform called `FakePlatform` as an example. -首先,在插件目录下新增 `fake_platform_adapter.py` 和 `fake_platform_event.py` 文件。前者主要是平台适配器的实现,后者是平台事件的定义。 +First, add `fake_platform_adapter.py` and `fake_platform_event.py` to your plugin directory. The former handles the platform adapter implementation, while the latter defines the platform event. -## 平台适配器 +## Platform Adapter -假设 FakePlatform 的客户端 SDK 是这样: +Assume FakePlatform's client SDK looks like this: ```py import asyncio class FakeClient(): - '''模拟一个消息平台,这里 5 秒钟下发一个消息''' + '''Simulates a messaging platform that sends a message every 5 seconds''' def __init__(self, token: str, username: str): self.token = token self.username = username # ... - + async def start_polling(self): while True: await asyncio.sleep(5) await getattr(self, 'on_message_received')({ 'bot_id': '123', - 'content': '新消息', + 'content': 'new message', 'username': 'zhangsan', 'userid': '123', 'message_id': 'asdhoashd', 'group_id': 'group123', }) - + async def send_text(self, to: str, message: str): - print('发了消息:', to, message) - + print('Message sent:', to, message) + async def send_image(self, to: str, image_path: str): - print('发了消息:', to, image_path) + print('Image sent:', to, image_path) ``` -我们创建 `fake_platform_adapter.py`: +Now create `fake_platform_adapter.py`: ```py import asyncio from astrbot.api.platform import Platform, AstrBotMessage, MessageMember, PlatformMetadata, MessageType from astrbot.api.event import MessageChain -from astrbot.api.message_components import Plain, Image, Record # 消息链中的组件,可以根据需要导入 -from astrbot.core.platform.astr_message_event import MessageSesion +from astrbot.api.message_components import Plain, Image, Record # Message chain components, import as needed +from astrbot.core.platform.message_session import MessageSesion from astrbot.api.platform import register_platform_adapter from astrbot import logger from .client import FakeClient from .fake_platform_event import FakePlatformEvent - -# 注册平台适配器。第一个参数为平台名,第二个为描述。第三个为默认配置。 -@register_platform_adapter("fake", "fake 适配器", default_config_tmpl={ + +# Register the platform adapter. First param: platform name, second: description, third: default config. +@register_platform_adapter("fake", "fake adapter", default_config_tmpl={ "token": "your_token", "username": "bot_username" }) @@ -66,52 +66,53 @@ class FakePlatformAdapter(Platform): def __init__(self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue) -> None: super().__init__(event_queue) - self.config = platform_config # 上面的默认配置,用户填写后会传到这里 - self.settings = platform_settings # platform_settings 平台设置。 - + self.config = platform_config # The default config above; filled in by the user and passed here + self.settings = platform_settings # platform_settings: platform settings + async def send_by_session(self, session: MessageSesion, message_chain: MessageChain): - # 必须实现 + # Must be implemented await super().send_by_session(session, message_chain) - + def meta(self) -> PlatformMetadata: - # 必须实现,直接像下面一样返回即可。 + # Must be implemented. Simply return as shown below. return PlatformMetadata( "fake", - "fake 适配器", + "fake adapter", ) async def run(self): - # 必须实现,这里是主要逻辑。 + # Must be implemented. This is the main logic. - # FakeClient 是我们自己定义的,这里只是示例。这个是其回调函数 + # FakeClient is defined by us — this is just an example. This is its callback function. async def on_received(data): logger.info(data) - abm = await self.convert_message(data=data) # 转换成 AstrBotMessage - await self.handle_msg(abm) - - # 初始化 FakeClient + abm = await self.convert_message(data=data) # Convert to AstrBotMessage + await self.handle_msg(abm) + + # Initialize FakeClient self.client = FakeClient(self.config['token'], self.config['username']) self.client.on_message_received = on_received - await self.client.start_polling() # 持续监听消息,这是个堵塞方法。 + await self.client.start_polling() # Continuously listens for messages; this is a blocking call. async def convert_message(self, data: dict) -> AstrBotMessage: - # 将平台消息转换成 AstrBotMessage - # 这里就体现了适配程度,不同平台的消息结构不一样,这里需要根据实际情况进行转换。 + # Convert the platform message to AstrBotMessage. + # The degree of adaptation is reflected here. Different platforms have different message + # structures; convert accordingly. abm = AstrBotMessage() - abm.type = MessageType.GROUP_MESSAGE # 还有 friend_message,对应私聊。具体平台具体分析。重要! - abm.group_id = data['group_id'] # 如果是私聊,这里可以不填 - abm.message_str = data['content'] # 纯文本消息。重要! - abm.sender = MessageMember(user_id=data['userid'], nickname=data['username']) # 发送者。重要! - abm.message = [Plain(text=data['content'])] # 消息链。如果有其他类型的消息,直接 append 即可。重要! - abm.raw_message = data # 原始消息。 + abm.type = MessageType.GROUP_MESSAGE # Also friend_message for private chats. Analyze per platform. Important! + abm.group_id = data['group_id'] # Can be omitted for private chats + abm.message_str = data['content'] # Plain text message. Important! + abm.sender = MessageMember(user_id=data['userid'], nickname=data['username']) # Sender. Important! + abm.message = [Plain(text=data['content'])] # Message chain. Append other message types as needed. Important! + abm.raw_message = data # Raw message. abm.self_id = data['bot_id'] - abm.session_id = data['userid'] # 会话 ID。重要! - abm.message_id = data['message_id'] # 消息 ID。 - + abm.session_id = data['userid'] # Session ID. Important! + abm.message_id = data['message_id'] # Message ID. + return abm - + async def handle_msg(self, message: AstrBotMessage): - # 处理消息 + # Handle the message message_event = FakePlatformEvent( message_str=message.message_str, message_obj=message, @@ -119,11 +120,11 @@ class FakePlatformAdapter(Platform): session_id=message.session_id, client=self.client ) - self.commit_event(message_event) # 提交事件到事件队列。不要忘记! + self.commit_event(message_event) # Submit the event to the event queue. Don't forget this! ``` -`fake_platform_event.py`: +`fake_platform_event.py`: ```py from astrbot.api.event import AstrMessageEvent, MessageChain @@ -136,15 +137,15 @@ class FakePlatformEvent(AstrMessageEvent): def __init__(self, message_str: str, message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, client: FakeClient): super().__init__(message_str, message_obj, platform_meta, session_id) self.client = client - + async def send(self, message: MessageChain): - for i in message.chain: # 遍历消息链 - if isinstance(i, Plain): # 如果是文字类型的 + for i in message.chain: # Iterate over the message chain + if isinstance(i, Plain): # If it's a text message await self.client.send_text(to=self.get_sender_id(), message=i.text) - elif isinstance(i, Image): # 如果是图片类型的 + elif isinstance(i, Image): # If it's an image img_url = i.file img_path = "" - # 下面的三个条件可以直接参考一下。 + # The three conditions below can be used as a reference. if img_url.startswith("file:///"): img_path = img_url[8:] elif i.file and i.file.startswith("http"): @@ -152,14 +153,14 @@ class FakePlatformEvent(AstrMessageEvent): else: img_path = img_url - # 请善于 Debug! - + # Make good use of debugging! + await self.client.send_image(to=self.get_sender_id(), image_path=img_path) - await super().send(message) # 需要最后加上这一段,执行父类的 send 方法。 + await super().send(message) # Must be called at the end to invoke the parent class's send method. ``` -最后,main.py 只需这样,在初始化的时候导入 fake_platform_adapter 模块。装饰器会自动注册。 +Finally, in `main.py`, simply import the `fake_platform_adapter` module during initialization. The decorator will handle registration automatically. ```py from astrbot.api.star import Context, Star @@ -169,17 +170,17 @@ class MyPlugin(Star): from .fake_platform_adapter import FakePlatformAdapter # noqa ``` -搞好后,运行 AstrBot: +Once set up, run AstrBot: ![image](https://files.astrbot.app/docs/source/images/plugin-platform-adapter/QQ_1738155926221.png) -这里出现了我们创建的 fake。 +The `fake` adapter we created now appears here. ![image](https://files.astrbot.app/docs/source/images/plugin-platform-adapter/QQ_1738155982211.png) -启动后,可以看到正常工作: +After starting, you can see it working correctly: ![image](https://files.astrbot.app/docs/source/images/plugin-platform-adapter/QQ_1738156166893.png) -有任何疑问欢迎加群询问~ +If you have any questions, feel free to join the community group and ask~ diff --git a/docs/en/dev/star/guides/ai.md b/docs/en/dev/star/guides/ai.md index 4c28c9d2e..cf7914bca 100644 --- a/docs/en/dev/star/guides/ai.md +++ b/docs/en/dev/star/guides/ai.md @@ -1,4 +1,3 @@ - # AI AstrBot provides built-in support for multiple Large Language Model (LLM) providers and offers a unified interface, making it convenient for plugin developers to access various LLM services. @@ -67,6 +66,58 @@ class BilibiliTool(FunctionTool[AstrAgentContext]): return "1. Video Title: How to Use AstrBot\nVideo Link: xxxxxx" ``` +## Registering Tools with AstrBot + +Once a Tool is defined, if you want it to be automatically invoked during user conversations, register it in your plugin's `__init__` method: + +```py +class MyPlugin(Star): + def __init__(self, context: Context): + super().__init__(context) + # >= v4.5.1: + self.context.add_llm_tools(BilibiliTool(), SecondTool(), ...) + + # < v4.5.1: + tool_mgr = self.context.provider_manager.llm_tools + tool_mgr.func_list.append(BilibiliTool()) +``` + +> [!WARNING] +> `context.register_llm_tool()` is deprecated. Do not use it in new plugins. +> +> If you must use it for legacy compatibility, `func_args` must be a **list of dicts** in this format: +> ```py +> func_args = [{"type": "string", "name": "arg_name", "description": "..."}, ...] +> ``` +> Passing a list of strings or any other format will raise `AttributeError: 'str' object has no attribute 'pop'`. + +### Registering Tools via Decorator + +Alternatively, you can use the `@filter.llm_tool` decorator to define and register a tool in one step. Make sure to follow the exact format below, including the docstring — AstrBot parses the docstring to generate the parameter schema: + +```py{3,4,5,6,7} +@filter.llm_tool(name="get_weather") # If name is omitted, the function name is used +async def get_weather(self, event: AstrMessageEvent, location: str) -> MessageEventResult: + '''Get weather information. + + Args: + location(string): The location to query + ''' + resp = self.get_weather_from_api(location) + yield event.plain_result("Weather: " + resp) +``` + +In `location(string): The location to query`, `location` is the parameter name, `string` is the type, and the remainder is the description. + +Supported types: `string`, `number`, `object`, `boolean`, `array`. Since v4.5.7, array subtypes are supported, e.g. `array[string]`. + +> [!WARNING] +> **The `Args:` block is required and must be formatted correctly.** +> +> The `@filter.llm_tool` decorator generates the parameter schema by parsing the function's docstring — it does **not** read Python type annotations. If the docstring is missing an `Args:` block, or the format does not follow `param_name(type): description`, the generated schema will be empty. Any arguments passed by the LLM will be silently dropped, causing the function to fail with a missing-argument error. +> +> Additionally, passing `parameters=...` directly to the decorator is **not supported** and will be silently ignored. If you need manual control over the schema, use the `@dataclass` + `add_llm_tools()` approach above. + ## Invoking Agents > [!TIP] @@ -257,7 +308,7 @@ curr_cid = await conv_mgr.get_curr_conversation_id(uid) conversation = await conv_mgr.get_conversation(uid, curr_cid) # Conversation ``` -::: details Conversation 类型定义 +::: details Conversation Type Definition ```py @dataclass @@ -288,83 +339,83 @@ class Conversation: #### `new_conversation` -- **Usage** +- **Usage** Create a new conversation in the current session and automatically switch to it. -- **Arguments** - - `unified_msg_origin: str` – In the format `platform_name:message_type:session_id` - - `platform_id: str | None` – Platform identifier, defaults to parsing from `unified_msg_origin` - - `content: list[dict] | None` – Initial message history - - `title: str | None` – Conversation title +- **Arguments** + - `unified_msg_origin: str` – In the format `platform_name:message_type:session_id` + - `platform_id: str | None` – Platform identifier, defaults to parsing from `unified_msg_origin` + - `content: list[dict] | None` – Initial message history + - `title: str | None` – Conversation title - `persona_id: str | None` – Associated persona ID -- **Returns** +- **Returns** `str` – Newly generated UUID conversation ID #### `switch_conversation` -- **Usage** +- **Usage** Switch the session to a specified conversation. -- **Arguments** - - `unified_msg_origin: str` +- **Arguments** + - `unified_msg_origin: str` - `conversation_id: str` -- **Returns** +- **Returns** `None` #### `delete_conversation` -- **Usage** +- **Usage** Delete a conversation from the session; if `conversation_id` is `None`, deletes the current conversation. -- **Arguments** - - `unified_msg_origin: str` +- **Arguments** + - `unified_msg_origin: str` - `conversation_id: str | None` -- **Returns** +- **Returns** `None` #### `get_curr_conversation_id` -- **Usage** +- **Usage** Get the conversation ID currently in use by the session. -- **Arguments** +- **Arguments** - `unified_msg_origin: str` -- **Returns** +- **Returns** `str | None` – Current conversation ID, returns `None` if it doesn't exist #### `get_conversation` -- **Usage** +- **Usage** Get the complete object for a specified conversation; automatically creates it if it doesn't exist and `create_if_not_exists=True`. -- **Arguments** - - `unified_msg_origin: str` - - `conversation_id: str` +- **Arguments** + - `unified_msg_origin: str` + - `conversation_id: str` - `create_if_not_exists: bool = False` -- **Returns** +- **Returns** `Conversation | None` #### `get_conversations` -- **Usage** +- **Usage** Retrieve the complete list of conversations for a user or platform. -- **Arguments** - - `unified_msg_origin: str | None` – When `None`, does not filter by user +- **Arguments** + - `unified_msg_origin: str | None` – When `None`, does not filter by user - `platform_id: str | None` -- **Returns** +- **Returns** `List[Conversation]` #### `update_conversation` -- **Usage** +- **Usage** Update the title, history, or persona_id of a conversation. -- **Arguments** - - `unified_msg_origin: str` - - `conversation_id: str | None` – Uses the current conversation when `None` - - `history: list[dict] | None` - - `title: str | None` +- **Arguments** + - `unified_msg_origin: str` + - `conversation_id: str | None` – Uses the current conversation when `None` + - `history: list[dict] | None` + - `title: str | None` - `persona_id: str | None` -- **Returns** +- **Returns** `None` ## Persona Manager -`PersonaManager` is responsible for unified loading, caching, and providing CRUD interfaces for all Personas, while maintaining compatibility with the legacy persona format (v3) from before AstrBot 4.x. +`PersonaManager` is responsible for unified loading, caching, and providing CRUD interfaces for all Personas, while maintaining compatibility with the legacy persona format (v3) from before AstrBot 4.x. During initialization, it automatically reads all personas from the database and generates v3-compatible data for seamless use with legacy code. ```py @@ -386,59 +437,59 @@ persona_mgr = self.context.persona_manager #### `get_all_personas` -- **Usage** +- **Usage** Retrieve all personas from the database at once. -- **Returns** +- **Returns** `list[Persona]` – Persona list, may be empty #### `create_persona` -- **Usage** +- **Usage** Create a new persona and immediately write it to the database; automatically refreshes the local cache upon success. -- **Arguments** - - `persona_id: str` – New persona ID (unique) - - `system_prompt: str` – System prompt - - `begin_dialogs: list[str]` – Optional, opening dialogs (even number of entries, alternating user/assistant) +- **Arguments** + - `persona_id: str` – New persona ID (unique) + - `system_prompt: str` – System prompt + - `begin_dialogs: list[str]` – Optional, opening dialogs (even number of entries, alternating user/assistant) - `tools: list[str]` – Optional, list of allowed tools; `None`=all tools, `[]`=disable all -- **Returns** +- **Returns** `Persona` – Newly created persona object -- **Raises** +- **Raises** `ValueError` – If `persona_id` already exists #### `update_persona` -- **Usage** +- **Usage** Update any fields of an existing persona and synchronize to database and cache. -- **Arguments** - - `persona_id: str` – Persona ID to update - - `system_prompt: str` – Optional, new system prompt - - `begin_dialogs: list[str]` – Optional, new opening dialogs +- **Arguments** + - `persona_id: str` – Persona ID to update + - `system_prompt: str` – Optional, new system prompt + - `begin_dialogs: list[str]` – Optional, new opening dialogs - `tools: list[str]` – Optional, new tool list; semantics same as `create_persona` -- **Returns** +- **Returns** `Persona` – Updated persona object -- **Raises** +- **Raises** `ValueError` – If `persona_id` doesn't exist #### `delete_persona` -- **Usage** +- **Usage** Delete the specified persona and clean up both database and cache. -- **Arguments** +- **Arguments** - `persona_id: str` – Persona ID to delete -- **Raises** +- **Raises** `ValueError` – If `persona_id` doesn't exist #### `get_default_persona_v3` -- **Usage** - Get the default persona (v3 format) to use based on the current session configuration. +- **Usage** + Get the default persona (v3 format) to use based on the current session configuration. Falls back to `DEFAULT_PERSONALITY` if configuration doesn't specify one or the specified persona doesn't exist. -- **Arguments** +- **Arguments** - `umo: str | MessageSession | None` – Session identifier, used to read user-level configuration -- **Returns** +- **Returns** `Personality` – Default persona object in v3 format -::: details Persona / Personality type definitions +::: details Persona / Personality Type Definition ```py diff --git a/docs/en/dev/star/guides/listen-message-event.md b/docs/en/dev/star/guides/listen-message-event.md index e798475ad..9e58326ca 100644 --- a/docs/en/dev/star/guides/listen-message-event.md +++ b/docs/en/dev/star/guides/listen-message-event.md @@ -1,4 +1,3 @@ - # Handling Message Events Event listeners can receive message content delivered by the platform and implement features such as commands, command groups, and event listening. @@ -97,7 +96,7 @@ AstrBot will automatically parse command parameters for you. ```python @filter.command("add") -def add(self, event: AstrMessageEvent, a: int, b: int): +async def add(self, event: AstrMessageEvent, a: int, b: int): # /add 1 2 -> Result is: 3 yield event.plain_result(f"Wow! The answer is {a + b}!") ``` @@ -108,7 +107,7 @@ Command groups help you organize commands. ```python @filter.command_group("math") -def math(self): +def math(): pass @math.command("add") @@ -160,7 +159,7 @@ async def sub(self, event: AstrMessageEvent, a: int, b: int): yield event.plain_result(f"Result is: {a - b}") @calc.command("help") -def calc_help(self, event: AstrMessageEvent): +async def calc_help(self, event: AstrMessageEvent): # /math calc help yield event.plain_result("This is a calculator plugin with add and sub commands.") ``` @@ -173,7 +172,7 @@ You can add different aliases for commands or command groups: ```python @filter.command("help", alias={'帮助', 'helpme'}) -def help(self, event: AstrMessageEvent): +async def help(self, event: AstrMessageEvent): yield event.plain_result("This is a calculator plugin with add and sub commands.") ``` @@ -209,7 +208,7 @@ async def on_aiocqhttp(self, event: AstrMessageEvent): yield event.plain_result("Received a message") ``` -In the current version, `PlatformAdapterType` includes `AIOCQHTTP`, `QQOFFICIAL`, `GEWECHAT`, and `ALL`. +In the current version, `PlatformAdapterType` supports the following values: `AIOCQHTTP`, `QQOFFICIAL`, `QQOFFICIAL_WEBHOOK`, `TELEGRAM`, `WECOM`, `WECOM_AI_BOT`, `LARK`, `DINGTALK`, `DISCORD`, `SLACK`, `KOOK`, `VOCECHAT`, `WEIXIN_OFFICIAL_ACCOUNT`, `SATORI`, `MISSKEY`, `LINE`, `MATRIX`, `WEIXIN_OC`, `MATTERMOST`, `WEBCHAT`, `ALL`. #### Admin Commands @@ -420,13 +419,14 @@ You can implement some message decoration here, such as converting to voice, con ```python from astrbot.api.event import filter, AstrMessageEvent +import astrbot.api.message_components as Comp @filter.on_decorating_result() async def on_decorating_result(self, event: AstrMessageEvent): result = event.get_result() chain = result.chain print(chain) # Print the message chain - chain.append(Plain("!")) # Add an exclamation mark at the end of the message chain + chain.append(Comp.Plain("!")) # Add an exclamation mark at the end of the message chain ``` > You cannot use yield to send messages here. This hook is only for decorating event.get_result().chain. If you need to send, please use the `event.send()` method directly. diff --git a/docs/en/dev/star/guides/simple.md b/docs/en/dev/star/guides/simple.md index f2dc11be9..d7ed40f95 100644 --- a/docs/en/dev/star/guides/simple.md +++ b/docs/en/dev/star/guides/simple.md @@ -40,19 +40,3 @@ Explanation: All handler functions must be written within the plugin class. To keep content concise, in subsequent sections, we may omit the plugin class definition. ``` - -解释如下: - -- 插件需要继承 `Star` 类。 -- `Context` 类用于插件与 AstrBot Core 交互,可以由此调用 AstrBot Core 提供的各种 API。 -- 具体的处理函数 `Handler` 在插件类中定义,如这里的 `helloworld` 函数。 -- `AstrMessageEvent` 是 AstrBot 的消息事件对象,存储了消息发送者、消息内容等信息。 -- `AstrBotMessage` 是 AstrBot 的消息对象,存储了消息平台下发的消息的具体内容。可以通过 `event.message_obj` 获取。 - -> [!TIP] -> -> `Handler` 一定需要在插件类中注册,前两个参数必须为 `self` 和 `event`。如果文件行数过长,可以将服务写在外部,然后在 `Handler` 中调用。 -> -> 插件类所在的文件名需要命名为 `main.py`。 - -所有的处理函数都需写在插件类中。为了精简内容,在之后的章节中,我们可能会忽略插件类的定义。 diff --git a/docs/en/dev/star/plugin-new.md b/docs/en/dev/star/plugin-new.md index b3e6845db..e50e96964 100644 --- a/docs/en/dev/star/plugin-new.md +++ b/docs/en/dev/star/plugin-new.md @@ -102,8 +102,10 @@ The values in `support_platforms` must be keys from `ADAPTER_NAME_2_TYPE`. Curre - `aiocqhttp` - `qq_official` +- `qq_official_webhook` - `telegram` - `wecom` +- `wecom_ai_bot` - `lark` - `dingtalk` - `discord` @@ -111,9 +113,12 @@ The values in `support_platforms` must be keys from `ADAPTER_NAME_2_TYPE`. Curre - `kook` - `vocechat` - `weixin_official_account` +- `weixin_oc` - `satori` - `misskey` - `line` +- `matrix` +- `mattermost` ### Declare AstrBot Version Range (Optional) diff --git a/docs/en/dev/star/plugin-publish.md b/docs/en/dev/star/plugin-publish.md index 29864fd32..4bb2a68e9 100644 --- a/docs/en/dev/star/plugin-publish.md +++ b/docs/en/dev/star/plugin-publish.md @@ -7,3 +7,14 @@ AstrBot uses GitHub to host plugins, so you'll need to push your plugin code to You can submit your plugin by visiting the [AstrBot Plugin Marketplace](https://plugins.astrbot.app). Once on the website, click the `+` button in the bottom-right corner, fill in the basic information, author details, repository information, and other required fields. Then click the `Submit to GITHUB` button. You will be redirected to the AstrBot repository's Issue submission page. Please verify that all information is correct, then click the `Create` button to complete the plugin publication process. ![fill out the form](https://files.astrbot.app/docs/source/images/plugin-publish/image.png) + +> ⚠️ **Size Limit**: The plugin zip package submitted to the marketplace **must not exceed 16MB**. If it exceeds this limit, the CI/CD pipeline will automatically reject the submission. +> +> To ensure your plugin passes review and publication smoothly, we recommend the following: +> +> - **Compress static assets**: Compress images, audio, and other resource files in your plugin to reduce their size. +> - **Clean up unnecessary files**: Avoid including directories like `.git`, `__pycache__`, `node_modules`, or development configuration files in your plugin repository. Add a `.gitignore` file to your repository root to exclude them. +> - **Optimize dependency size**: If your plugin depends on large libraries, consider trimming them down or importing only what is needed. +> - **Use `.gitattributes` or a release branch**: Reduce the zip package size by including only the files necessary for distribution. +> +> If your plugin truly cannot be compressed to under 16MB due to business requirements, please contact the maintainer to manually bypass this limit. diff --git a/docs/en/faq.md b/docs/en/faq.md index 3955f544f..c9f0b10f9 100644 --- a/docs/en/faq.md +++ b/docs/en/faq.md @@ -6,17 +6,81 @@ Download `dist.zip` from the [release](https://github.com/AstrBotDevs/AstrBot/releases) page, extract it, and move it to `AstrBot/data`. If it still doesn't work, try restarting your computer (based on community feedback). +### First Login Account and Random Password + +On first startup, the WebUI account is `astrbot` by default, and the default password is randomly generated (it is not a fixed hardcoded value). Check the startup logs and log in with the random initial password shown there: + +```text +[00:27:40.590] [Core] [INFO] [dashboard.server:523]: + ✨✨✨ + AstrBot v4.24.3 WebUI is ready + + ➜ Local: http://localhost:6185 + ➜ Initial username: astrbot + ➜ Initial password: UiYVpZxnW8k22IWqf0ru5pOy + ➜ Change it after logging in + ✨✨✨ +Set dashboard.host in data/cmd_config.json to enable remote access. +``` + +`UiYVpZxnW8k22IWqf0ru5pOy` is the default password. + ### Forgot Dashboard Password -If you forgot your AstrBot dashboard password, you can modify the `"dashboard"` field in the `AstrBot/data/cmd_config.json` configuration file, where `"username"` is your username and `"password"` is your password encrypted with MD5. +If you forgot your AstrBot dashboard password, find the `"dashboard"` field in `AstrBot/data/cmd_config.json`, for example: -To modify your account credentials, follow these steps: +```json + "dashboard": { + "enable": true, + "username": "astrbot", + "password": "81e0c3dxxxxxxxxxxx78862e78", + "pbkdf2_password": "pbkdf2_sha256$600000$1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "password_storage_upgraded": true, + "password_change_required": true, + "jwt_secret": "5e1b0280bcxxxxxxxxxxxxxxxxf4a", + "host": "127.0.0.1", + "port": 6185, + "disable_access_log": true, + "ssl": { + "enable": false, + "cert_file": "", + "key_file": "", + "ca_certs": "" + } + }, +``` -1. Modify the `"username"` field, keeping the `""` quotation marks. If you don't want to change the username, skip this step -2. Visit the website: [Online MD5 Generator](https://www.metools.info/code/c26.html) -3. Enter your new password in the input text box -4. Select MD5 encryption (32-bit), make sure to choose the 32-bit option -5. Paste the converted string into the configuration file, keeping the `""` quotation marks +Delete the `username`, `password`, `pbkdf2_password`, `password_storage_upgraded`, `password_change_required`, and `jwt_secret` fields (with their values), then save. +The segment should look like: + +```json + "dashboard": { + "enable": true, + "host": "127.0.0.1", + "port": 6185, + "disable_access_log": true, + "ssl": { + "enable": false, + "cert_file": "", + "key_file": "", + "ca_certs": "" + } + }, +``` + +After restart, AstrBot will automatically generate a random password with the fixed username `astrbot`; check the startup logs. + +### Correct Password Cannot Log In After Upgrading AstrBot + +If you are sure the dashboard password is correct but still cannot log in after upgrading AstrBot, the old WebUI static files may be incompatible with the newer backend. + +Solution: + +1. Stop AstrBot. +2. Delete the `dist` folder under AstrBot's `data` directory: `AstrBot/data/dist`. +3. Restart AstrBot. + +After restart, AstrBot will reload or download WebUI files that match the current version. ## Bot Core Related diff --git a/docs/en/platform/dingtalk.md b/docs/en/platform/dingtalk.md index 9b21f7cdf..a4bb53fa2 100644 --- a/docs/en/platform/dingtalk.md +++ b/docs/en/platform/dingtalk.md @@ -16,6 +16,20 @@ Proactive message push: Supported. ## Create and Configure the App +DingTalk supports two setup methods: one-click QR creation in AstrBot, or manually creating an app in DingTalk Open Platform. + +### Option 1: One-click QR Creation + +AstrBot version requirement: >= v4.25.0. + +Open AstrBot Dashboard -> `Bots` -> `+ Create Bot`, then select `DingTalk`. + +Under `Creation Method`, select `One-click QR setup`, scan the QR code with the DingTalk mobile app, then create or bind a bot on the DingTalk authorization page. After creation succeeds, AstrBot automatically fills in `ClientID` and `ClientSecret`. Click `Save` to finish. + +After QR creation succeeds, continue checking the event subscription, version release, and group installation steps below. + +### Option 2: Manual Creation + Go to the [DingTalk Open Platform](https://open-dev.dingtalk.com/fe/app), then create an app: ![image](https://files.astrbot.app/docs/source/images/dingtalk/image-4.png) @@ -36,7 +50,7 @@ Go to Credentials & Basic Information, then copy `ClientID` and `ClientSecret`. Open AstrBot Dashboard -> `Bots` -> `+ Create Bot`, then create a DingTalk adapter. -Fill in `ClientID` and `ClientSecret`, then click Save. AstrBot will request authorization from DingTalk Open Platform automatically. +If you want AstrBot to create the app for you, select `One-click QR setup` and complete the scan. If you already created the app yourself, select `Manual setup`, fill in `ClientID` and `ClientSecret`, then click Save. AstrBot will request authorization from DingTalk Open Platform automatically. Back in DingTalk Open Platform, open Event Subscriptions, select `Stream mode push`, and click Save. If successful, you will see a connected status. diff --git a/docs/en/platform/lark.md b/docs/en/platform/lark.md index b6e4fe091..173d0d8ed 100644 --- a/docs/en/platform/lark.md +++ b/docs/en/platform/lark.md @@ -20,6 +20,31 @@ The Lark client version must be >= 7.20. Lower versions only display the title a ## Creating a Bot +Lark supports two setup methods: one-click QR creation in AstrBot, or manually creating a custom enterprise app in the Lark Developer Console. + +### Option 1: One-click QR Creation + +AstrBot version requirement: >= 4.25.0. + +Open the AstrBot management panel, click `Bots` in the left sidebar, click `+ Create Bot`, and select `lark`. + +Under `Creation Method`, select `One-click QR Creation`, choose the China or international edition as needed, then scan the QR code with the Lark mobile app and confirm. After creation succeeds, AstrBot automatically fills in the app's `app_id`, `app_secret`, and domain configuration. + +> [!IMPORTANT] +> After an app is created through QR scanning, group chats receive only messages that @ mention the bot or messages triggered by a wake prefix such as `/` by default. If you need the bot to receive all group messages, enable the additional permissions in the Lark Developer Console. +> +> Replace `` in the URL below with your Lark app ID, then open it to jump to the permission enablement page: +> +> To find the App ID, go back to AstrBot's `Bots` page, find the Lark bot you just created, click `Edit`, and check the dialog that opens. +> +> ```text +> https://open.feishu.cn/app//auth?q=contact:contact.base:readonly,im:message.p2p_msg:readonly,im:message.group_at_msg:readonly,im:message:send,im:message,im:message:send_as_bot,im:resource:upload,im:resource,cardkit:card:write,im:message.group_at_msg:readonly,im:message.group_msg&op_from=openapi&token_type=tenant +> ``` + +After QR creation succeeds, continue checking the event subscription, permissions, version release, and group installation steps below. + +### Option 2: Manual Creation + Navigate to the [Developer Console](https://open.feishu.cn/app) and create a custom enterprise application. ![Create Custom Enterprise Application](https://files.astrbot.app/docs/source/images/lark/image.png) @@ -38,6 +63,7 @@ Click on "Credentials & Basic Info" to obtain your app_id and app_secret. 2. Click on `Bots` in the left sidebar 3. In the right panel, click `+ Create Bot` 4. Select `lark` +5. If you want AstrBot to create the app for you, select `One-click QR Creation` and complete the scan. If you already created the app yourself, select `Manual Creation` Fill in the configuration fields as follows: @@ -45,7 +71,6 @@ Fill in the configuration fields as follows: - Enable: Check this option - app_id: The app_id you obtained earlier - app_secret: The app_secret you obtained earlier -- Bot name: Your Lark bot's name For the domain field, if you're using Lark China, keep the default value. If you're using Lark International, set it to `https://open.larksuite.com`. If you're using a self-hosted enterprise Lark instance, enter your Lark instance's domain. @@ -92,6 +117,11 @@ Next, click on "Permission Management," click "Enable Permissions," and enter `i Enter `im:resource:upload,im:resource` again to enable image upload permissions. +If you want to use the bot in group chats, additionally enable `im:message.group_at_msg:readonly` and `im:message.group_msg`. + +> [!TIP] +> Apps created through one-click QR creation are suitable for @ mentions and wake-prefix triggers by default. To receive every group message, make sure `im:message.group_msg` is enabled. You can also use the permission URL above to quickly open the corresponding page. + If you want to use streaming output, additionally enable `Create and update cards (cardkit:card:write)`. The final set of permissions should look like this: diff --git a/docs/en/platform/weixin_oc.md b/docs/en/platform/weixin_oc.md index 2068c022c..44a93b432 100644 --- a/docs/en/platform/weixin_oc.md +++ b/docs/en/platform/weixin_oc.md @@ -25,6 +25,8 @@ AstrBot supports connecting a personal WeChat account through the `Personal WeCh 2. Click `Bots` in the left sidebar. 3. Click `+ Create Bot` in the upper-right corner. 4. Select `Personal WeChat`. +5. The login QR code is shown directly. Scan it with WeChat on your phone and confirm the login inside WeChat. +6. After login succeeds, click `Save`. ## Configuration Notes @@ -44,15 +46,12 @@ Leave the remaining options at their default values unless you explicitly know y ## QR Login -1. Fill in the configuration and click `Save`. -2. Return to the bot list. AstrBot will automatically request a login QR code from WeChat. -3. On the bot card, click `View QR Code` to open the QR dialog. -4. Scan it with WeChat on your phone, then confirm the login inside WeChat. +After you select `Personal WeChat`, AstrBot automatically requests a login QR code from WeChat and shows it directly in the create-bot dialog. Scan it with WeChat on your phone and confirm the login. When the QR area shows the login-success state, click `Save` to finish creating the bot. -After login succeeds, AstrBot will automatically persist the login state. On later restarts, if the session is still valid, you usually do not need to scan again. +After login succeeds and the bot is saved, AstrBot will automatically persist the login state. On later restarts, if the session is still valid, you usually do not need to scan again. > [!NOTE] -> If the QR code expires, AstrBot will automatically request a new one. Please scan the refreshed QR code instead of the old one. +> If the QR code expires, close and reopen the create-bot dialog, or select `Personal WeChat` again to request a new QR code. ## Verification diff --git a/docs/en/providers/302ai.md b/docs/en/providers/302ai.md index 50a3abe77..8685e3b05 100644 --- a/docs/en/providers/302ai.md +++ b/docs/en/providers/302ai.md @@ -1,21 +1,21 @@ -# 接入 302.AI +# Connect 302.AI -302.AI 是企业级 AI 应用平台,支持快捷接入全球各类 AI 模型。 +[302.AI](https://302.ai) is an enterprise-grade AI application platform that provides quick access to a wide range of AI models worldwide. -## 使用 +## Getting Started -点击[此链接](https://share.302.ai/rr1M3l) 注册账户。 +Click [this link](https://share.302.ai/rr1M3l) to register an account. -注册完毕之后,点击[此链接](https://302.ai/apis/)选择需要接入的模型。 +After registering, click [this link](https://302.ai/apis/) to select the model you want to use. -根据需求,进入[此链接](https://dash.302.ai/charge) 充值对应的金额。 +If needed, visit [this link](https://dash.302.ai/charge) to top up your account balance. -## 接入 +## Connect -打开 AstrBot 控制台 -> 服务提供商页面,点击新增提供商,找到并点击 `302.AI`(需要版本 >= 3.5.18) +Open the AstrBot dashboard → Service Providers page, click **Add Provider**, find and click `302.AI` (requires version >= 3.5.18). -修改 ID,并将 API Key 和模型名称填入对话框表单,点击保存,即可完成创建。 +Set an ID, fill in the API Key and model name in the dialog form, then click **Save** to complete the setup. -## 使用 +## Usage -对机器人输入 `/provider` 指令,将提供商切换到刚刚添加的 302.AI 提供商,即可使用。 +Send the `/provider` command to the bot to switch to the 302.AI provider you just added. diff --git a/docs/en/providers/ppio.md b/docs/en/providers/ppio.md index fceb61dd1..db9ed03b4 100644 --- a/docs/en/providers/ppio.md +++ b/docs/en/providers/ppio.md @@ -1,43 +1,41 @@ -# 接入 PPIO 派欧云 +# Connect PPIO Cloud -PPIO 派欧云是中国领先的独立分布式云计算服务商,您可以在派欧云上使用稳定、低价甚至免费的模型服务。 +PPIO Cloud is a leading independent distributed cloud computing provider in China, offering stable, affordable, and even free model services. -## 准备 +## Preparation -打开 [PPIO 派欧云官网](https://ppio.cn/user/register?invited_by=AIOONE),并注册账户(通过此链接注册的账户将会获得 15 元人民币的代金券)。 +Open the [PPIO Cloud website](https://ppio.cn/user/register?invited_by=AIOONE) and register an account (accounts registered through this link will receive a ¥15 voucher). -进入 [模型 API 服务](https://ppio.cn/model-api/console),找到你想接入的模型。你可以通过筛选器选择不同厂商或者免费的模型。 +Go to [Model API Service](https://ppio.cn/model-api/console) and find the model you want to use. You can filter by provider or select free models. ![image](https://files.astrbot.app/docs/source/images/ppio/image-1.png) -找到你想要接入的模型后,点击模型卡片,侧边会展开一个模型详情卡片,找到下方的 API 接入指南,如果您还没创建过 Key 可以点击创建。 +Once you find the model, click its card to expand a detail panel on the right. Scroll down to the API integration guide — if you haven't created a key yet, click to create one. ![image](https://files.astrbot.app/docs/source/images/ppio/image-3.png) -打开 AstrBot 控制台 -> 服务提供商页面,点击新增提供商,找到并点击 `PPIO派欧云`(需要版本 >= 3.5.10,旧版本也可使用,见下文)。 +Open the AstrBot dashboard → Service Providers page, click **Add Provider**, find and click `PPIO Cloud` (requires version >= 3.5.10; older versions are also supported, see below). ![image](https://files.astrbot.app/docs/source/images/ppio/image.png) -将 API Key 和模型名称填入对话框表单,点击保存,即可完成创建。 +Fill in the API Key and model name in the dialog form, then click **Save** to complete the setup. > [!TIP] -> 如果您是 AstrBot 旧版本(< 3.5.10)的用户,请打开 AstrBot 控制台 -> 服务提供商页面,点击新增提供商,找到 `OpenAI`,点击进入。 -> 1. 将 ID 命名为 `ppio`(随意) -> 2. 然后将 `API Base URL` 设置为 `https://api.ppinfra.com/v3/openai` -> 3. 然后将 API Key 和模型名称填入对话框表单,点击保存,即可完成创建。 +> If you are using an older version of AstrBot (< 3.5.10), open the AstrBot dashboard → Service Providers page, click **Add Provider**, find `OpenAI`, and click to enter. +> 1. Set the ID to `ppio` (any name works) +> 2. Set `API Base URL` to `https://api.ppinfra.com/v3/openai` +> 3. Fill in the API Key and model name in the dialog form, then click **Save** to complete the setup. +## Usage -## 使用 +Send the `/provider` command to the bot to switch to the PPIO Cloud provider you just added. -对机器人输入 `/provider` 指令,将提供商切换到刚刚添加的 PPIO 派欧云提供商,即可使用。 +## FAQ -## 常见问题 - -#### 显示 `400` 错误 +#### `400` Error ```log Error code: 400 - {'code': 400, 'message': '"auto" tool choice requires --enable-auto-tool-choice and --tool-call-parser to be set', 'type': 'BadRequestError'} ``` - -请暂时使用 `/tool off_all` 禁用所有的函数调用工具即可使用,或者换用其他模型。 \ No newline at end of file +Temporarily disable all function calling tools with `/tool off_all`, or switch to a different model. diff --git a/docs/en/providers/provider-lmstudio.md b/docs/en/providers/provider-lmstudio.md index 969f7d7c8..4c0d99a7c 100644 --- a/docs/en/providers/provider-lmstudio.md +++ b/docs/en/providers/provider-lmstudio.md @@ -1,38 +1,37 @@ -# 接入 LM Studio 使用 DeepSeek-R1 等模型 +# Connect LM Studio to Use DeepSeek-R1 and Other Models -LMStudio 允许在本地电脑上部署模型(需要电脑硬件配置符合要求) +LM Studio allows you to deploy models locally on your computer (hardware requirements must be met). -### 下载并安装 LMStudio +### Download and Install LM Studio -https://lmstudio.ai/download + -### 下载并运行模型 +### Download and Run a Model -https://lmstudio.ai/models + -跟随 LMStudio 下载并运行想要的模型,如 deepseek-r1-qwen-7b: +Follow the LM Studio instructions to download and run your desired model, e.g. `deepseek-r1-qwen-7b`: ```bash lms get deepseek-r1-qwen-7b ``` -### 配置 AstrBot +### Configure AstrBot -在 AstrBot 上: +In AstrBot: -点击 配置->服务提供商配置->加号->openai +Go to **Configuration → Service Providers → + → OpenAI** -API Base URL 填写 `http://localhost:1234/v1` +Set `API Base URL` to `http://localhost:1234/v1` -API Key 填写 `lm-studio` +Set `API Key` to `lm-studio` -> 对于 Mac/Windows 使用 Docker Desktop 部署 AstrBot 部署的用户,API Base URL 请填写为 `http://host.docker.internal:1234/v1`。 -> 对于 Linux 使用 Docker 部署 AstrBot 部署的用户,API Base URL 请填写为 `http://172.17.0.1:1234/v1`,或者将 `172.17.0.1` 替换为你的公网 IP IP(确保宿主机系统放行了 1234 端口)。 +> For users deploying AstrBot via Docker Desktop on Mac or Windows, set `API Base URL` to `http://host.docker.internal:1234/v1`. +> +> For users deploying AstrBot via Docker on Linux, set `API Base URL` to `http://172.17.0.1:1234/v1`, or replace `172.17.0.1` with your server's public IP (make sure port 1234 is open on the host). -如果 LM Studio 使用了 Docker 部署,请确保 1234 端口已经映射到宿主机。 +If LM Studio itself is deployed in Docker, ensure port 1234 is mapped to the host. -模型名填写上一步选好的 +Set the model name to the one you selected in the previous step, then save the configuration. -保存配置即可。 - -> 输入 /provider 查看 AstrBot 配置的模型 \ No newline at end of file +> Run `/provider` to view the models configured in AstrBot. diff --git a/docs/en/use/astrbot-agent-sandbox.md b/docs/en/use/astrbot-agent-sandbox.md index 775418c27..31115d20d 100644 --- a/docs/en/use/astrbot-agent-sandbox.md +++ b/docs/en/use/astrbot-agent-sandbox.md @@ -328,7 +328,7 @@ If you choose `Shipyard Neo`, the main configuration items are: - If AstrBot can access Bay's `credentials.json`, you may leave it empty and let AstrBot auto-discover it - `Shipyard Neo Profile` - For example `python-default` or `browser-python` - - If not explicitly specified, AstrBot will try to choose a profile with richer capabilities, preferring one that includes the `browser` capability, and fall back to `python-default` if needed + - If left empty, AstrBot will try to choose a profile with richer capabilities, preferring one that includes the `browser` capability, and fall back to `python-default` if needed - `Shipyard Neo Sandbox TTL` - The upper lifetime limit of the sandbox, defaulting to 3600 seconds (1 hour) diff --git a/docs/en/use/function-calling.md b/docs/en/use/function-calling.md index 1526cbab9..2208ddd11 100644 --- a/docs/en/use/function-calling.md +++ b/docs/en/use/function-calling.md @@ -51,4 +51,4 @@ Below are some common tool calling demos: ## MCP -Please refer to this documentation: [AstrBot - MCP](/use/mcp). +Please refer to this documentation: [AstrBot - MCP](/en/use/mcp). diff --git a/docs/en/use/mcp.md b/docs/en/use/mcp.md index 1681d3132..a2b95f562 100644 --- a/docs/en/use/mcp.md +++ b/docs/en/use/mcp.md @@ -99,4 +99,4 @@ That's it. Reference links: 1. Learn how to use MCP here: [Model Context Protocol](https://modelcontextprotocol.io/introduction) -2. Get commonly used MCP servers here: [awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers/blob/main/README-zh.md#what-is-mcp), [Model Context Protocol servers](https://github.com/modelcontextprotocol/servers), [MCP.so](https://mcp.so) +2. Get commonly used MCP servers here: [awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers/blob/main/README.md#what-is-mcp), [Model Context Protocol servers](https://github.com/modelcontextprotocol/servers), [MCP.so](https://mcp.so) diff --git a/docs/en/use/skills.md b/docs/en/use/skills.md index a57a5b12c..4221cc047 100644 --- a/docs/en/use/skills.md +++ b/docs/en/use/skills.md @@ -20,7 +20,7 @@ You can upload Skills with the following requirements: 1. The upload must be a `.zip` archive. 2. **After extraction, it must contain a single Skill folder. The folder name will be used as the identifier for the Skill in AstrBot—please name it using English characters.** -3. The Skill folder must include a file named `SKILL.md`, and its contents should preferably follow the Anthropic Skills specification. You can refer to Anthropic's documentation: https://code.claude.com/docs/zh-CN/skills +3. The Skill folder must include a file named `SKILL.md`, and its contents should preferably follow the Anthropic Skills specification. You can refer to Anthropic's documentation: https://code.claude.com/docs/en/skills ## Using Skills in AstrBot diff --git a/docs/en/use/webui.md b/docs/en/use/webui.md index dd17994c0..ec963e861 100644 --- a/docs/en/use/webui.md +++ b/docs/en/use/webui.md @@ -13,7 +13,7 @@ After starting AstrBot, you can access the admin panel by visiting `http://local ## Login -The default username and password are both `astrbot`. +For first-time login, AstrBot generates a random initial password and prints it in startup logs. Please read the startup log line containing the WebUI credential and use that password to log in (username is usually `astrbot`). ## ChatUI diff --git a/docs/zh/deploy/astrbot/cli.md b/docs/zh/deploy/astrbot/cli.md index e0fdec806..13c0f9cf0 100644 --- a/docs/zh/deploy/astrbot/cli.md +++ b/docs/zh/deploy/astrbot/cli.md @@ -85,7 +85,7 @@ python main.py > [!TIP] > 如果你正在服务器上部署 AstrBot,需要将 `localhost` 替换为你的服务器 IP 地址。 > -> 默认用户名和密码是 `astrbot` 和 `astrbot`。 +> 首次登录请使用启动日志中打印的随机初始密码(用户名通常为 `astrbot`)。登录后请立即修改密码。 接下来,你需要部署任何一个消息平台,才能够实现在消息平台上使用 AstrBot。 diff --git a/docs/zh/deploy/astrbot/compshare.md b/docs/zh/deploy/astrbot/compshare.md index f6c4d80db..664870b12 100644 --- a/docs/zh/deploy/astrbot/compshare.md +++ b/docs/zh/deploy/astrbot/compshare.md @@ -34,7 +34,7 @@ Both services started in the background. ![WebUI 界面](https://www-s.ucloud.cn/2025/07/7e9fc6edc1dfa916abc069f4cecc24cf_1753940381771.png) -使用用户名:astrbot 和密码 astrbot 进行登录。 +首次登录时请使用启动日志内的随机初始密码(用户名通常是 astrbot),登录后请立即修改密码。 登录成功后,可以重新设置密码,并进入 AstrBot 的页面。 @@ -86,4 +86,4 @@ AstrBot 支持接入优云智算提供的模型 API。 ## 更多功能 -更多功能请参考 [AstrBot 官方文档](https://docs.astrbot.app)。 \ No newline at end of file +更多功能请参考 [AstrBot 官方文档](https://docs.astrbot.app)。 diff --git a/docs/zh/deploy/astrbot/docker.md b/docs/zh/deploy/astrbot/docker.md index 742fd3612..1733499d1 100644 --- a/docs/zh/deploy/astrbot/docker.md +++ b/docs/zh/deploy/astrbot/docker.md @@ -98,7 +98,7 @@ sudo docker logs -f astrbot > [!TIP] > 由于 Docker 隔离了网络环境,所以不能使用 `localhost` 访问管理面板。 > -> 默认用户名和密码是 `astrbot` 和 `astrbot`。 +> 首次登录请使用启动日志中打印的随机初始密码(用户名通常为 `astrbot`)。登录后请立即修改密码。 > > 如果部署在云服务器上,需要在相应厂商控制台里放行对应端口。 diff --git a/docs/zh/deploy/astrbot/kubernetes.md b/docs/zh/deploy/astrbot/kubernetes.md index 180b1eab7..6d2ed5288 100644 --- a/docs/zh/deploy/astrbot/kubernetes.md +++ b/docs/zh/deploy/astrbot/kubernetes.md @@ -194,4 +194,4 @@ image: m.daocloud.io/docker.io/soulter/astrbot:latest 部署并暴露服务后,您就可以通过相应的 IP 和端口访问 AstrBot 管理面板了。 -> 默认用户名和密码是 `astrbot` 和 `astrbot`。 +> 首次登录请使用启动日志中打印的随机初始密码(用户名通常为 `astrbot`)。登录后请立即修改密码。 diff --git a/docs/zh/deploy/astrbot/launcher.md b/docs/zh/deploy/astrbot/launcher.md index 9b0864d81..69fe72e9a 100644 --- a/docs/zh/deploy/astrbot/launcher.md +++ b/docs/zh/deploy/astrbot/launcher.md @@ -64,7 +64,7 @@ MacOS 用户下载安装好后,可能会遇到 "已损坏,无法打开" 的 如果没有报错,你会看到一条日志显示类似 `🌈 管理面板已启动,可访问` 并附带了几条链接。打开其中一个链接即可访问 AstrBot 管理面板。 > [!TIP] -> 默认用户名和密码是 `astrbot` 和 `astrbot`。 +> 首次登录请使用启动日志中打印的随机初始密码(用户名通常为 `astrbot`)。登录后请立即修改密码。 > > **当管理面板打开时遇到 404 错误:** > 在 [release](https://github.com/AstrBotDevs/AstrBot/releases) 页面下载dist.zip,解压拖到 AstrBot/data 下。还不行请重启电脑(来自群里的反馈) @@ -98,4 +98,4 @@ windows 搜索 Python,打开文件位置: **方法 2:** -重装 python,并且在安装时勾选 `Add Python to PATH`,然后重启电脑。 \ No newline at end of file +重装 python,并且在安装时勾选 `Add Python to PATH`,然后重启电脑。 diff --git a/docs/zh/dev/astrbot-config.md b/docs/zh/dev/astrbot-config.md index 9b13cf84d..28ee553e1 100644 --- a/docs/zh/dev/astrbot-config.md +++ b/docs/zh/dev/astrbot-config.md @@ -116,7 +116,7 @@ AstrBot 默认配置如下: "dashboard": { "enable": True, "username": "astrbot", - "password": "77b90590a8945a7d36c963981a307dc9", + "password": "", "jwt_secret": "", "host": "0.0.0.0", "port": 6185, @@ -492,11 +492,11 @@ HTTP 代理。如 `http://localhost:7890`。 AstrBot WebUI 配置。 -请不要随意修改 `password` 的值。它是一个经过 `md5` 编码的密码。请在控制面板修改密码。 +请不要随意修改 `password` 的值。它是随机初始密码经过 `md5` 编码后的值。首次启动时请在日志中获取初始密码,并在控制面板中修改。 - `enable`: 是否启用 AstrBot WebUI。默认为 `true`。 -- `username`: AstrBot WebUI 的用户名。默认为 `astrbot`。 -- `password`: AstrBot WebUI 的密码。默认为 `astrbot` 的 `md5` 编码值。请勿直接修改,除非您知道自己在做什么。 +- `username`: AstrBot WebUI 的用户名。 +- `password`: AstrBot WebUI 的密码。首次启动会随机生成初始密码(已在日志中打印),这里保存的是该密码的 `md5` 值。请勿直接修改,除非您知道自己在做什么。 - `jwt_secret`: JWT 的密钥。AstrBot 会在初始化时随机生成。请勿修改,除非您知道自己在做什么。 - `host`: AstrBot WebUI 监听的地址。默认为 `0.0.0.0`。 - `port`: AstrBot WebUI 监听的端口。默认为 `6185`。 diff --git a/docs/zh/dev/star/guides/ai.md b/docs/zh/dev/star/guides/ai.md index 17364386b..ceba128ea 100644 --- a/docs/zh/dev/star/guides/ai.md +++ b/docs/zh/dev/star/guides/ai.md @@ -1,4 +1,3 @@ - # AI AstrBot 内置了对多种大语言模型(LLM)提供商的支持,并且提供了统一的接口,方便插件开发者调用各种 LLM 服务。 @@ -82,9 +81,18 @@ class MyPlugin(Star): tool_mgr.func_list.append(BilibiliTool()) ``` +> [!WARNING] +> `context.register_llm_tool()` 已被弃用,请勿在新插件中使用。 +> +> 如需通过该方法注册(旧插件兼容),`func_args` 必须是 **字典列表**,格式为: +> ```py +> func_args = [{"type": "string", "name": "arg_name", "description": "参数描述"}, ...] +> ``` +> 传入字符串列表或其他格式会导致 `AttributeError: 'str' object has no attribute 'pop'`。 + ### 通过装饰器定义 Tool 和注册 Tool -除了上述的通过 `@dataclass` 定义 Tool 的方式之外,你也可以使用装饰器的方式注册 tool 到 AstrBot。如果请务必按照以下格式编写一个工具(包括函数注释,AstrBot 会解析该函数注释,请务必将注释格式写对) +除了上述的通过 `@dataclass` 定义 Tool 的方式之外,你也可以使用装饰器的方式注册 tool 到 AstrBot。请务必按照以下格式编写一个工具(包括函数注释,AstrBot 会解析该函数注释,请务必将注释格式写对): ```py{3,4,5,6,7} @filter.llm_tool(name="get_weather") # 如果 name 不填,将使用函数名 @@ -102,6 +110,13 @@ async def get_weather(self, event: AstrMessageEvent, location: str) -> MessageEv 支持的参数类型有 `string`, `number`, `object`, `boolean`, `array`。在 v4.5.7 之后,支持对 `array` 类型参数指定子类型,例如 `array[string]`。 +> [!WARNING] +> **`Args:` 段是必须的,且格式不能写错。** +> +> `@filter.llm_tool` 装饰器通过解析函数的 docstring 来生成工具的参数 schema,**不会**读取函数签名中的类型注解。如果 docstring 缺少 `Args:` 段,或格式不符合 `参数名(类型): 描述` 的规范,框架生成的参数 schema 将为空,LLM 传入的参数会被静默丢弃,最终导致函数因缺少参数而报错。 +> +> 此外,装饰器**不支持**通过 `parameters=...` 显式传入参数 schema,该写法会被忽略。如需手动控制 schema,请使用上方的 `@dataclass` + `add_llm_tools()` 方式。 + ## 调用 Agent > [!TIP] @@ -352,83 +367,83 @@ await conv_mgr.add_message_pair( #### `new_conversation` -- __Usage__ +- __Usage__ 在当前会话中新建一条对话,并自动切换为该对话。 -- __Arguments__ - - `unified_msg_origin: str` – 形如 `platform_name:message_type:session_id` - - `platform_id: str | None` – 平台标识,默认从 `unified_msg_origin` 解析 - - `content: list[dict] | None` – 初始历史消息 - - `title: str | None` – 对话标题 +- __Arguments__ + - `unified_msg_origin: str` – 形如 `platform_name:message_type:session_id` + - `platform_id: str | None` – 平台标识,默认从 `unified_msg_origin` 解析 + - `content: list[dict] | None` – 初始历史消息 + - `title: str | None` – 对话标题 - `persona_id: str | None` – 绑定的 persona ID -- __Returns__ +- __Returns__ `str` – 新生成的 UUID 对话 ID #### `switch_conversation` -- __Usage__ +- __Usage__ 将会话切换到指定的对话。 -- __Arguments__ - - `unified_msg_origin: str` +- __Arguments__ + - `unified_msg_origin: str` - `conversation_id: str` -- __Returns__ +- __Returns__ `None` #### `delete_conversation` -- __Usage__ +- __Usage__ 删除会话中的某条对话;若 `conversation_id` 为 `None`,则删除当前对话。 -- __Arguments__ - - `unified_msg_origin: str` +- __Arguments__ + - `unified_msg_origin: str` - `conversation_id: str | None` -- __Returns__ +- __Returns__ `None` #### `get_curr_conversation_id` -- __Usage__ +- __Usage__ 获取当前会话正在使用的对话 ID。 -- __Arguments__ +- __Arguments__ - `unified_msg_origin: str` -- __Returns__ +- __Returns__ `str | None` – 当前对话 ID,不存在时返回 `None` #### `get_conversation` -- __Usage__ +- __Usage__ 获取指定对话的完整对象;若不存在且 `create_if_not_exists=True` 则自动创建。 -- __Arguments__ - - `unified_msg_origin: str` - - `conversation_id: str` +- __Arguments__ + - `unified_msg_origin: str` + - `conversation_id: str` - `create_if_not_exists: bool = False` -- __Returns__ +- __Returns__ `Conversation | None` #### `get_conversations` -- __Usage__ +- __Usage__ 拉取用户或平台下的全部对话列表。 -- __Arguments__ - - `unified_msg_origin: str | None` – 为 `None` 时不过滤用户 +- __Arguments__ + - `unified_msg_origin: str | None` – 为 `None` 时不过滤用户 - `platform_id: str | None` -- __Returns__ +- __Returns__ `List[Conversation]` #### `update_conversation` -- __Usage__ +- __Usage__ 更新对话的标题、历史记录或 persona_id。 -- __Arguments__ - - `unified_msg_origin: str` - - `conversation_id: str | None` – 为 `None` 时使用当前对话 - - `history: list[dict] | None` - - `title: str | None` +- __Arguments__ + - `unified_msg_origin: str` + - `conversation_id: str | None` – 为 `None` 时使用当前对话 + - `history: list[dict] | None` + - `title: str | None` - `persona_id: str | None` -- __Returns__ +- __Returns__ `None` ## 人格设定管理器 -`PersonaManager` 负责统一加载、缓存并提供所有人格(Persona)的增删改查接口,同时兼容 AstrBot 4.x 之前的旧版人格格式(v3)。 +`PersonaManager` 负责统一加载、缓存并提供所有人格(Persona)的增删改查接口,同时兼容 AstrBot 4.x 之前的旧版人格格式(v3)。 初始化时会自动从数据库读取全部人格,并生成一份 v3 兼容数据,供旧代码无缝使用。 ```py @@ -450,56 +465,56 @@ persona_mgr = self.context.persona_manager #### `get_all_personas` -- __Usage__ +- __Usage__ 一次性获取数据库中所有人格。 -- __Returns__ +- __Returns__ `list[Persona]` – 人格列表,可能为空 #### `create_persona` -- __Usage__ +- __Usage__ 新建人格并立即写入数据库,成功后自动刷新本地缓存。 -- __Arguments__ - - `persona_id: str` – 新人格 ID(唯一) - - `system_prompt: str` – 系统提示词 - - `begin_dialogs: list[str]` – 可选,开场对话(偶数条,user/assistant 交替) +- __Arguments__ + - `persona_id: str` – 新人格 ID(唯一) + - `system_prompt: str` – 系统提示词 + - `begin_dialogs: list[str]` – 可选,开场对话(偶数条,user/assistant 交替) - `tools: list[str]` – 可选,允许使用的工具列表;`None`=全部工具,`[]`=禁用全部 -- __Returns__ +- __Returns__ `Persona` – 新建后的人格对象 -- __Raises__ +- __Raises__ `ValueError` – 若 `persona_id` 已存在 #### `update_persona` -- __Usage__ +- __Usage__ 更新现有人格的任意字段,并同步到数据库与缓存。 -- __Arguments__ - - `persona_id: str` – 待更新的人格 ID - - `system_prompt: str` – 可选,新的系统提示词 - - `begin_dialogs: list[str]` – 可选,新的开场对话 +- __Arguments__ + - `persona_id: str` – 待更新的人格 ID + - `system_prompt: str` – 可选,新的系统提示词 + - `begin_dialogs: list[str]` – 可选,新的开场对话 - `tools: list[str]` – 可选,新的工具列表;语义同 `create_persona` -- __Returns__ +- __Returns__ `Persona` – 更新后的人格对象 -- __Raises__ +- __Raises__ `ValueError` – 若 `persona_id` 不存在 #### `delete_persona` -- __Usage__ +- __Usage__ 删除指定人格,同时清理数据库与缓存。 -- __Arguments__ +- __Arguments__ - `persona_id: str` – 待删除的人格 ID -- __Raises__ +- __Raises__ `ValueError` – 若 `persona_id` 不存在 #### `get_default_persona_v3` -- __Usage__ - 根据当前会话配置,获取应使用的默认人格(v3 格式)。 +- __Usage__ + 根据当前会话配置,获取应使用的默认人格(v3 格式)。 若配置未指定或指定的人格不存在,则回退到 `DEFAULT_PERSONALITY`。 -- __Arguments__ +- __Arguments__ - `umo: str | MessageSession | None` – 会话标识,用于读取用户级配置 -- __Returns__ +- __Returns__ `Personality` – v3 格式的默认人格对象 ::: details Persona / Personality 类型定义 diff --git a/docs/zh/dev/star/guides/listen-message-event.md b/docs/zh/dev/star/guides/listen-message-event.md index b8187b00b..edb7c7e0a 100644 --- a/docs/zh/dev/star/guides/listen-message-event.md +++ b/docs/zh/dev/star/guides/listen-message-event.md @@ -1,4 +1,3 @@ - # 处理消息事件 事件监听器可以收到平台下发的消息内容,可以实现指令、指令组、事件监听等功能。 @@ -97,9 +96,9 @@ AstrBot 会自动帮你解析指令的参数。 ```python @filter.command("add") -def add(self, event: AstrMessageEvent, a: int, b: int): +async def add(self, event: AstrMessageEvent, a: int, b: int): # /add 1 2 -> 结果是: 3 - yield event.plain_result(f"Wow! The anwser is {a + b}!") + yield event.plain_result(f"Wow! The answer is {a + b}!") ``` ## 指令组 @@ -108,7 +107,7 @@ def add(self, event: AstrMessageEvent, a: int, b: int): ```python @filter.command_group("math") -def math(self): +def math(): pass @math.command("add") @@ -160,7 +159,7 @@ async def sub(self, event: AstrMessageEvent, a: int, b: int): yield event.plain_result(f"结果是: {a - b}") @calc.command("help") -def calc_help(self, event: AstrMessageEvent): +async def calc_help(self, event: AstrMessageEvent): # /math calc help yield event.plain_result("这是一个计算器插件,拥有 add, sub 指令。") ``` @@ -173,7 +172,7 @@ def calc_help(self, event: AstrMessageEvent): ```python @filter.command("help", alias={'帮助', 'helpme'}) -def help(self, event: AstrMessageEvent): +async def help(self, event: AstrMessageEvent): yield event.plain_result("这是一个计算器插件,拥有 add, sub 指令。") ``` @@ -209,7 +208,7 @@ async def on_aiocqhttp(self, event: AstrMessageEvent): yield event.plain_result("收到了一条信息") ``` -当前版本下,`PlatformAdapterType` 有 `AIOCQHTTP`, `QQOFFICIAL`, `GEWECHAT`, `ALL`。 +当前版本下,`PlatformAdapterType` 支持以下值:`AIOCQHTTP`、`QQOFFICIAL`、`QQOFFICIAL_WEBHOOK`、`TELEGRAM`、`WECOM`、`WECOM_AI_BOT`、`LARK`、`DINGTALK`、`DISCORD`、`SLACK`、`KOOK`、`VOCECHAT`、`WEIXIN_OFFICIAL_ACCOUNT`、`SATORI`、`MISSKEY`、`LINE`、`MATRIX`、`WEIXIN_OC`、`MATTERMOST`、`WEBCHAT`、`ALL`。 #### 管理员指令 @@ -437,13 +436,14 @@ async def on_agent_done(self, event: AstrMessageEvent, run_context: ContextWrapp ```python from astrbot.api.event import filter, AstrMessageEvent +import astrbot.api.message_components as Comp @filter.on_decorating_result() async def on_decorating_result(self, event: AstrMessageEvent): result = event.get_result() chain = result.chain print(chain) # 打印消息链 - chain.append(Plain("!")) # 在消息链的最后添加一个感叹号 + chain.append(Comp.Plain("!")) # 在消息链的最后添加一个感叹号 ``` > 这里不能使用 yield 来发送消息。这个钩子只是用来装饰 event.get_result().chain 的。如需发送,请直接使用 `event.send()` 方法。 diff --git a/docs/zh/dev/star/guides/simple.md b/docs/zh/dev/star/guides/simple.md index d3314133f..b700bef06 100644 --- a/docs/zh/dev/star/guides/simple.md +++ b/docs/zh/dev/star/guides/simple.md @@ -4,7 +4,7 @@ ```python from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult -from astrbot.api.star import Context, Star, register +from astrbot.api.star import Context, Star from astrbot.api import logger # 使用 astrbot 提供的 logger 接口 class MyPlugin(Star): diff --git a/docs/zh/dev/star/plugin-new.md b/docs/zh/dev/star/plugin-new.md index 04d61deae..2ebf5f392 100644 --- a/docs/zh/dev/star/plugin-new.md +++ b/docs/zh/dev/star/plugin-new.md @@ -104,8 +104,10 @@ support_platforms: - `aiocqhttp` - `qq_official` +- `qq_official_webhook` - `telegram` - `wecom` +- `wecom_ai_bot` - `lark` - `dingtalk` - `discord` @@ -113,9 +115,12 @@ support_platforms: - `kook` - `vocechat` - `weixin_official_account` +- `weixin_oc` - `satori` - `misskey` - `line` +- `matrix` +- `mattermost` ### 声明 AstrBot 版本范围(Optional) diff --git a/docs/zh/dev/star/plugin-publish.md b/docs/zh/dev/star/plugin-publish.md index 45997acc5..57f2f0a38 100644 --- a/docs/zh/dev/star/plugin-publish.md +++ b/docs/zh/dev/star/plugin-publish.md @@ -7,3 +7,14 @@ AstrBot 使用 GitHub 托管插件,因此你需要先将插件代码推送到 你可以前往 [AstrBot 插件市场](https://plugins.astrbot.app) 提交你的插件。进入该网站后,点击右下角的 `+` 按钮,填写好基本信息、作者信息、仓库信息等内容后,点击 `提交到 GITHUB` 按钮,你将会被导航到 AstrBot 仓库的 Issue 提交页面,请确认信息无误后点击 `Create` 按钮提交,即可完成插件发布。 ![fill out the form](https://files.astrbot.app/docs/source/images/plugin-publish/image.png) + +> ⚠️ **大小限制**:发布到插件市场的插件压缩包(zip)大小**不得超过 16MB**。如果超过此限制,CI/CD 流水线将自动拒绝该发布请求。 +> +> 为确保你的插件能顺利通过审核和发布,建议采取以下措施: +> +> - **压缩图片等静态资源**:对插件中的图片、音频等资源文件进行压缩,减小体积。 +> - **清理不必要的文件**:避免将 `.git` 目录、`__pycache__`、`node_modules`、开发用配置文件等非必需文件提交到插件仓库中。建议在仓库根目录添加 `.gitignore` 来排除它们。 +> - **优化依赖体积**:如果插件包含体积较大的依赖库,可考虑精简或按需引入。 +> - **使用 `.gitattributes` 或发布分支**:通过只包含发布所需文件的策略来减小 zip 包体积。 +> +> 如果插件确实因业务需要无法压缩到 16MB 以内,可以联系维护者手动 bypass 此限制。 diff --git a/docs/zh/faq.md b/docs/zh/faq.md index b78311862..7bc9cb431 100644 --- a/docs/zh/faq.md +++ b/docs/zh/faq.md @@ -6,17 +6,82 @@ 在 [release](https://github.com/AstrBotDevs/AstrBot/releases) 页面下载 `dist.zip`,解压拖到 `AstrBot/data` 下。还不行请重启电脑(来自群里的反馈) + +### 首次登录的默认账号和随机密码 + +首次启动时,WebUI 的默认账号为 `astrbot`,默认密码会随机生成,不会写死为固定值。请在启动日志中查找以下内容并使用日志中的随机初始密码登录: + +```text +[00:27:40.590] [Core] [INFO] [dashboard.server:523]: + ✨✨✨ + AstrBot v4.24.3 WebUI is ready + + ➜ Local: http://localhost:6185 + ➜ Initial username: astrbot + ➜ Initial password: UiYVpZxnW8k22IWqf0ru5pOy + ➜ Change it after logging in + ✨✨✨ +Set dashboard.host in data/cmd_config.json to enable remote access. +``` + +其中的 `UiYVpZxnW8k22IWqf0ru5pOy` 就是默认密码。在使用默认密码登录后,会自动进入设置账户环节。 + ### 管理面板的密码忘记了 -如果你忘记了 AstrBot 管理面板的密码,你可以在 `AstrBot/data/cmd_config.json` 配置文件中找到 `"dashboard"` 字段进行修改,其中 `"username"` 是你的用户名,`"password"` 是你的密码(经过 MD5 加密)。 +如果你忘记了 AstrBot 管理面板的密码,你可以在 `AstrBot/data/cmd_config.json` 配置文件中找到 `"dashboard"` 字段,如下: -如果想要修改账号密码,你可以这样做: +```json + "dashboard": { + "enable": true, + "username": "astrbot", + "password": "81e0c3dxxxxxxxxxxx78862e78", + "pbkdf2_password": "pbkdf2_sha256$600000$1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "password_storage_upgraded": true, + "password_change_required": true, + "jwt_secret": "5e1b0280bcxxxxxxxxxxxxxxxxf4a", + "host": "127.0.0.1", + "port": 6185, + "disable_access_log": true, + "ssl": { + "enable": false, + "cert_file": "", + "key_file": "", + "ca_certs": "" + } + }, +``` -1. 修改 `"username"` 字段,注意保留 `""`;如果不想修改用户名,可以不修改 -2. 进入网站:[在线 MD5 生成](https://www.metools.info/code/c26.html) -3. 在转换前文本框输入你的新密码 -4. 选择 MD5 加密(32 位),请确认选择 32 位选项 -5. 将转换后的字符粘贴至配置文件,注意保留 `""`, 且字母使用小写 +删除 `username`, `password`, `pbkdf2_password`, `password_storage_upgraded`, `password_change_required`, `jwt_secret` 五个字段(连同值一起),然后保存。上述片段修改类似如下: + + +```json + "dashboard": { + "enable": true, + "host": "127.0.0.1", + "port": 6185, + "disable_access_log": true, + "ssl": { + "enable": false, + "cert_file": "", + "key_file": "", + "ca_certs": "" + } + }, +``` + +重启后 AstrBot 将会自动生成随机的密码以及固定的用户名 `astrbot`,请在日志查看。 + +### 升级 AstrBot 后密码正确但无法登录 + +如果你确认管理面板密码正确,但升级 AstrBot 后仍然无法登录,可能是旧版 WebUI 静态文件缓存与新版后端不兼容。 + +解决方案: + +1. 停止 AstrBot。 +2. 删除 AstrBot 的 `data` 目录下的 `dist` 文件夹,即 `AstrBot/data/dist`。 +3. 重新启动 AstrBot。 + +重启后,AstrBot 会重新加载或下载匹配当前版本的 WebUI 文件。 ## AstrBot 使用相关 diff --git a/docs/zh/platform/dingtalk.md b/docs/zh/platform/dingtalk.md index dcc141065..9139abd79 100644 --- a/docs/zh/platform/dingtalk.md +++ b/docs/zh/platform/dingtalk.md @@ -16,6 +16,20 @@ ## 创建和配置应用 +钉钉支持两种创建方式:在 AstrBot 中扫码一键创建,或在钉钉开放平台手动创建应用。 + +### 方式一:扫码一键创建 + +需要 AstrBot 版本 >= v4.25.0。 + +打开 AstrBot 管理面板 -> `机器人` -> `+ 创建机器人`,选择 `钉钉(DingTalk)`。 + +在 `选择创建方式` 中选择 `扫码一键创建`,使用手机钉钉扫描页面中的二维码,并在钉钉页面中创建或绑定机器人。创建成功后,AstrBot 会自动写入 `ClientID` 和 `ClientSecret`,此时点击 `保存` 即可。 + +扫码创建完成后,仍建议检查后文的事件订阅、版本发布和拉入群组步骤。 + +### 方式二:手动创建 + 前往 [钉钉开放平台](https://open-dev.dingtalk.com/fe/app),点击创建应用: ![image](https://files.astrbot.app/docs/source/images/dingtalk/image-4.png) @@ -36,7 +50,7 @@ 打开 AstrBot 管理面板 -> `机器人` -> `+ 创建机器人`,创建一个钉钉适配器。 -将刚刚复制的 `ClientID` 和 `ClientSecret` 填入,点击保存,AstrBot 将会自动向钉钉开放平台请求。 +如果使用扫码一键创建,选择 `扫码一键创建` 并完成扫码;如果使用自己创建的钉钉应用,选择 `手动创建`,将刚刚复制的 `ClientID` 和 `ClientSecret` 填入。点击保存后,AstrBot 将会自动向钉钉开放平台请求。 回到钉钉开放平台,点击事件订阅,选择 `Stream 模式推送`,点击保存,如果没有意外情况,将会看到 连接接入成功 字样。 diff --git a/docs/zh/platform/lark.md b/docs/zh/platform/lark.md index 9379c623c..7078594b1 100644 --- a/docs/zh/platform/lark.md +++ b/docs/zh/platform/lark.md @@ -20,6 +20,31 @@ ## 创建机器人 +飞书(Lark)支持两种创建方式:在 AstrBot 中扫码一键创建,或在飞书开发者后台手动创建企业自建应用。 + +### 方式一:扫码一键创建 + +需要版本 >4.25.0。 + +进入 AstrBot 管理面板,点击左边栏 `机器人`,然后点击 `+ 创建机器人`,选择 `lark(飞书)`。 + +在 `选择创建方式` 中选择 `扫码一键创建`,按需选择国内版或海外版,然后使用手机飞书扫描页面中的二维码并确认。创建成功后,AstrBot 会自动写入该应用的 `app_id`、`app_secret` 和域名配置。 + +> [!IMPORTANT] +> 通过扫码方式创建后,群聊下默认仅会接收 @ 机器人和通过唤醒前缀(例如 `/`)触发的消息。如果你希望机器人接收群聊中的所有消息,需要前往飞书开发者后台为应用开通额外权限。 +> +> 可以将下面链接中的 `` 替换为你的飞书应用 App ID 后打开,一键进入权限开通页: +> +> App ID 获取方式:回到 AstrBot 的 `机器人` 页,找到刚刚创建的飞书机器人,点击 `编辑`,弹出的对话框中可以看到 App ID。 +> +> ```text +> https://open.feishu.cn/app//auth?q=contact:contact.base:readonly,im:message.p2p_msg:readonly,im:message.group_at_msg:readonly,im:message:send,im:message,im:message:send_as_bot,im:resource:upload,im:resource,cardkit:card:write,im:message.group_at_msg:readonly,im:message.group_msg&op_from=openapi&token_type=tenant +> ``` + +扫码创建完成后,建议继续检查后文的事件订阅、权限、版本发布和拉入群组步骤。 + +### 方式二:手动创建 + 前往 [开发者后台](https://open.feishu.cn/app) ,创建企业自建应用。 ![创建企业自建应用](https://files.astrbot.app/docs/source/images/lark/image.png) @@ -38,6 +63,7 @@ 2. 点击左边栏 `机器人` 3. 然后在右边的界面中,点击 `+ 创建机器人` 4. 选择 `lark(飞书)` +5. 如果使用扫码一键创建,选择 `扫码一键创建` 并完成扫码;如果使用自己创建的企业自建应用,选择 `手动创建` 弹出的配置项填写: @@ -45,7 +71,6 @@ - 启用(enable): 勾选。 - app_id: 获取的 app_id - app_secret: 获取的 app_secret -- 飞书机器人的名字 对于 domain,如果您使用国内版飞书,保持默认即可;如果您正在用国际版飞书,请设置为 `https://open.larksuite.com`;如果您使用企业自部署飞书,请填写您的飞书实例的域名。 @@ -94,6 +119,9 @@ 如果需要在群聊里使用,请额外开通 `im:message.group_at_msg:readonly` 和 `im:message.group_msg` 权限。 +> [!TIP] +> 扫码一键创建的应用默认适合 @ 机器人和唤醒前缀触发。如果要接收群聊所有消息,请确认已经开通 `im:message.group_msg`。你也可以使用上文提供的权限开通链接快速进入对应页面。 + 如果需要使用流式输出,请额外开通 `创建与更新卡片(cardkit:card:write)` 权限。 最终开通的权限如下图: diff --git a/docs/zh/platform/weixin_oc.md b/docs/zh/platform/weixin_oc.md index 7fc64dbb7..c39a9f390 100644 --- a/docs/zh/platform/weixin_oc.md +++ b/docs/zh/platform/weixin_oc.md @@ -23,6 +23,8 @@ AstrBot 支持通过 `个人微信` 适配器接入微信个人号。该适配 2. 点击左侧栏 `机器人`。 3. 点击右上角 `+ 创建机器人`。 4. 选择 `个人微信`。 +5. 页面会直接显示登录二维码,使用手机微信扫码,并在微信内确认登录。 +6. 登录成功后点击 `保存`。 ## 配置项说明 @@ -42,18 +44,12 @@ AstrBot 支持通过 `个人微信` 适配器接入微信个人号。该适配 ## 扫码登录 -1. 填好配置后点击 `保存`。 -2. 返回机器人列表,AstrBot 会自动向微信接口申请登录二维码。 -3. 在**机器人卡片**中点击 “查看二维码” 按钮,会弹出二维码对话框。(点击保存之后可能需要等 5 到 10 秒左右才会出现这个按钮) -4. 使用手机微信扫码,并在微信内确认登录。 +选择 `个人微信` 后,AstrBot 会自动向微信接口申请登录二维码,并直接显示在创建机器人弹窗中。使用手机微信扫码确认后,二维码会显示登录成功状态,此时点击 `保存` 即可完成创建。 -![微信二维码入口](weixin_qr_entry.png) - -登录成功后,AstrBot 会自动保存登录态。后续重启时,如果登录态仍有效,通常不需要再次扫码。 +登录成功并保存后,AstrBot 会自动保存登录态。后续重启时,如果登录态仍有效,通常不需要再次扫码。 > [!NOTE] -> 1. 如果二维码过期,AstrBot 会自动重新申请新的二维码。刷新后请使用新的二维码重新扫码。 -> 2. 如果 WebUI 没看到 “查看二维码” 按钮,可以前往终端或者 WebUI 控制台,找到 `请使用手机微信扫码登录,二维码有效期 5 分钟,过期后会自动刷新。` 对应的日志,附近会显示二维码扫码链接和终端直接输出的二维码,直选择一种方式扫码即可。 +> 如果二维码过期,请关闭并重新打开创建机器人弹窗,或重新选择 `个人微信` 以获取新的二维码。 ## 验证 @@ -77,4 +73,3 @@ AstrBot 支持通过 `个人微信` 适配器接入微信个人号。该适配 - 该适配器通过扫码登录个人微信,接入方式与微信公众号、企业微信不同。 - 不需要配置公网回调地址,也不需要开启统一 Webhook 模式。 - diff --git a/docs/zh/use/astrbot-agent-sandbox.md b/docs/zh/use/astrbot-agent-sandbox.md index ff59c6a7b..fd8069141 100644 --- a/docs/zh/use/astrbot-agent-sandbox.md +++ b/docs/zh/use/astrbot-agent-sandbox.md @@ -434,7 +434,7 @@ docker pull soulter/shipyard-ship:latest - 如果是官方联合部署,且 AstrBot 能访问 Bay 的 `credentials.json`,可以留空自动发现 - `Shipyard Neo Profile` - 例如 `python-default`、`browser-python` - - 如果未手动指定,AstrBot 会优先尝试选择能力更完整、且优先带有 `browser` capability 的 profile,失败时再回退到 `python-default` + - 如果留空,AstrBot 会优先尝试选择能力更完整、且优先带有 `browser` capability 的 profile,失败时再回退到 `python-default` - `Shipyard Neo Sandbox TTL` - sandbox 生命周期上限,默认值为 3600 秒(1 小时) diff --git a/docs/zh/use/knowledge-base.md b/docs/zh/use/knowledge-base.md index 5f4088e51..383c89078 100644 --- a/docs/zh/use/knowledge-base.md +++ b/docs/zh/use/knowledge-base.md @@ -55,6 +55,6 @@ AstrBot 支持多知识库管理。在聊天时,您可以**自由指定知识 1. 打开 [硅基流动官网](https://cloud.siliconflow.cn/i/zMCYMSt2),注册账户并完成实名认证。 2. 打开 [API 密钥](https://cloud.siliconflow.cn/me/account/ak)。 5. 填写 AstrBot OpenAI Embedding 模型提供商配置: - 1. API Key 为刚刚申请的 PPIO 的 API Key + 1. API Key 为刚刚申请的硅基流动的 API Key 2. embedding api base 填写 `https://api.siliconflow.cn/v1` 3. model 填写你选择的模型,此例子中为 `BAAI/bge-m3`。 diff --git a/docs/zh/use/webui.md b/docs/zh/use/webui.md index 972538056..96df0d8d4 100644 --- a/docs/zh/use/webui.md +++ b/docs/zh/use/webui.md @@ -13,7 +13,7 @@ AstrBot 管理面板具有管理插件、查看日志、可视化配置、查看 ## 登录 -默认用户名和密码是 `astrbot` 和 `astrbot`。 +新用户首次登录时,AstrBot 会生成一个随机初始密码并写入启动日志。请先在启动日志中查找并使用该密码登录(用户名通常为 `astrbot`),登录后请立即修改密码。 ## ChatUI diff --git a/main.py b/main.py index f4e38e402..c682ec36f 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,9 @@ from astrbot.core.utils.astrbot_path import ( # noqa: E402 ) from astrbot.core.utils.io import ( # noqa: E402 download_dashboard, + get_bundled_dashboard_dist_path, get_dashboard_version, + should_use_bundled_dashboard_dist, ) # 将父目录添加到 sys.path @@ -77,6 +79,13 @@ async def check_dashboard_files(webui_dir: str | None = None): data_dist_path = os.path.join(get_astrbot_data_path(), "dist") if os.path.exists(data_dist_path): v = await get_dashboard_version() + if should_use_bundled_dashboard_dist(data_dist_path, VERSION): + bundled_dist = get_bundled_dashboard_dist_path() + logger.info( + "Using bundled WebUI because data/dist is older than core version v%s.", + VERSION, + ) + return str(bundled_dist) if v is not None: # 存在文件 if v == f"v{VERSION}": diff --git a/pyproject.toml b/pyproject.toml index 59a8eb501..056880540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "AstrBot" -version = "4.24.2" +version = "dev" description = "Easy-to-use multi-platform LLM chatbot and development framework" readme = "README.md" license = { text = "AGPL-3.0-or-later" } diff --git a/tests/test_api_key_open_api.py b/tests/test_api_key_open_api.py index f6a3fe459..a2479680a 100644 --- a/tests/test_api_key_open_api.py +++ b/tests/test_api_key_open_api.py @@ -11,9 +11,15 @@ from werkzeug.datastructures import FileStorage from astrbot.core import LogBroker from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db.sqlite import SQLiteDatabase +from astrbot.core.utils.auth_password import ( + hash_dashboard_password, + hash_legacy_dashboard_password, +) from astrbot.dashboard.routes.route import Response from astrbot.dashboard.server import AstrBotDashboard +_TEST_DASHBOARD_PASSWORD = "AstrbotTest123" + def _get_open_api_route(app: Quart): rule = next( @@ -67,6 +73,24 @@ async def core_lifecycle_td(tmp_path_factory): core_lifecycle = AstrBotCoreLifecycle(log_broker, db) await core_lifecycle.initialize() + generated_password = getattr( + core_lifecycle.astrbot_config, + "_generated_dashboard_password", + None, + ) + dashboard_password = generated_password or _TEST_DASHBOARD_PASSWORD + if not generated_password: + core_lifecycle.astrbot_config["dashboard"]["pbkdf2_password"] = ( + hash_dashboard_password(dashboard_password) + ) + core_lifecycle.astrbot_config["dashboard"]["password"] = ( + hash_legacy_dashboard_password(dashboard_password) + ) + object.__setattr__( + core_lifecycle, + "_dashboard_plain_password", + dashboard_password, + ) try: yield core_lifecycle finally: @@ -88,10 +112,13 @@ def app(core_lifecycle_td: AstrBotCoreLifecycle): def _resolve_dashboard_password(core_lifecycle_td: AstrBotCoreLifecycle) -> str: - password = core_lifecycle_td.astrbot_config["dashboard"]["password"] - if isinstance(password, str) and (password.startswith("pbkdf2_sha256$") or password.startswith("$argon2")): - return "astrbot-test-password" - return str(password) + generated_password = getattr(core_lifecycle_td, "_dashboard_plain_password", None) + if generated_password: + return generated_password + password = core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"] + if isinstance(password, str) and password.startswith("pbkdf2_sha256$"): + return "astrbot" + return password @pytest_asyncio.fixture(scope="module") diff --git a/tests/test_cli_init.py b/tests/test_cli_init.py new file mode 100644 index 000000000..f68ddf059 --- /dev/null +++ b/tests/test_cli_init.py @@ -0,0 +1,54 @@ +import json + +import pytest + +from astrbot.cli.commands import cmd_init +from astrbot.core.utils.auth_password import verify_dashboard_password + + +@pytest.mark.asyncio +async def test_init_without_initial_password_env_does_not_create_config( + monkeypatch, + tmp_path, +): + async def fake_check_dashboard(_data_path): + return None + + monkeypatch.delenv(cmd_init.DASHBOARD_INITIAL_PASSWORD_ENV, raising=False) + monkeypatch.setattr(cmd_init, "check_dashboard", fake_check_dashboard) + (tmp_path / ".astrbot").touch() + + await cmd_init.initialize_astrbot(tmp_path) + + assert not (tmp_path / "data" / "cmd_config.json").exists() + + +@pytest.mark.asyncio +async def test_init_uses_initial_password_env_to_create_config( + monkeypatch, + tmp_path, +): + async def fake_check_dashboard(_data_path): + return None + + initial_password = "AstrBotInitialPassword123" + monkeypatch.setenv(cmd_init.DASHBOARD_INITIAL_PASSWORD_ENV, initial_password) + monkeypatch.setattr(cmd_init, "check_dashboard", fake_check_dashboard) + (tmp_path / ".astrbot").touch() + + await cmd_init.initialize_astrbot(tmp_path) + + config_path = tmp_path / "data" / "cmd_config.json" + config = json.loads(config_path.read_text(encoding="utf-8-sig")) + dashboard_config = config["dashboard"] + + assert verify_dashboard_password( + dashboard_config["pbkdf2_password"], + initial_password, + ) + assert verify_dashboard_password( + dashboard_config["password"], + initial_password, + ) + assert dashboard_config["password_change_required"] is True + assert dashboard_config["password_storage_upgraded"] is True diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 5898b5c5a..7050d4fdd 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -22,7 +22,19 @@ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db.sqlite import SQLiteDatabase from astrbot.core.star.star import StarMetadata, star_registry from astrbot.core.star.star_handler import star_handlers_registry +from astrbot.core.utils.auth_password import ( + hash_dashboard_password, + hash_legacy_dashboard_password, + verify_dashboard_password, +) from astrbot.core.utils.pip_installer import PipInstallError +from astrbot.dashboard.password_state import ( + get_dashboard_password_hash, + is_password_change_required, + is_password_storage_upgraded, + set_password_change_required, + set_password_storage_upgraded, +) from astrbot.dashboard.routes.auth import DASHBOARD_JWT_COOKIE_NAME from astrbot.dashboard.routes.plugin import PluginRoute from astrbot.dashboard.server import AstrBotDashboard @@ -32,6 +44,7 @@ from tests.fixtures.helpers import ( create_mock_updater_update, ) +_TEST_DASHBOARD_PASSWORD = "AstrbotTest123" PLUGIN_PAGE_DEMO_NAME = "astrbot_plugin_page_demo" PLUGIN_PAGE_DEMO_PAGE_NAME = "bridge-demo" @@ -142,6 +155,34 @@ async def core_lifecycle_td(tmp_path_factory): log_broker = LogBroker() core_lifecycle = AstrBotCoreLifecycle(log_broker, db) await core_lifecycle.initialize() + generated_password = getattr( + core_lifecycle.astrbot_config, + "_generated_dashboard_password", + None, + ) + dashboard_password = generated_password or _TEST_DASHBOARD_PASSWORD + if not generated_password: + core_lifecycle.astrbot_config["dashboard"]["pbkdf2_password"] = ( + hash_dashboard_password(dashboard_password) + ) + core_lifecycle.astrbot_config["dashboard"]["password"] = ( + hash_legacy_dashboard_password(dashboard_password) + ) + await set_password_storage_upgraded( + core_lifecycle.db, + core_lifecycle.astrbot_config, + True, + ) + await set_password_change_required( + core_lifecycle.db, + core_lifecycle.astrbot_config, + False, + ) + object.__setattr__( + core_lifecycle, + "_dashboard_plain_password", + dashboard_password, + ) try: yield core_lifecycle finally: @@ -166,12 +207,71 @@ def app(core_lifecycle_td: AstrBotCoreLifecycle): def _resolve_dashboard_password(core_lifecycle_td: AstrBotCoreLifecycle) -> str: """Return a login password compatible with both hashed and plain defaults.""" - password = core_lifecycle_td.astrbot_config["dashboard"]["password"] - if isinstance(password, str) and (password.startswith("pbkdf2_sha256$") or password.startswith("$argon2")): - return "astrbot-test-password" + generated_password = getattr(core_lifecycle_td, "_dashboard_plain_password", None) + if generated_password: + return generated_password + password = core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"] + if isinstance(password, str) and password.startswith("pbkdf2_sha256$"): + return "astrbot" return password +def test_dashboard_uses_bundled_dist_when_data_dist_is_stale( + core_lifecycle_td: AstrBotCoreLifecycle, + monkeypatch, + tmp_path, +): + data_dir = tmp_path / "data" + user_dist = data_dir / "dist" + bundled_dist = tmp_path / "bundled-dist" + user_dist.mkdir(parents=True) + bundled_dist.mkdir() + + monkeypatch.setattr( + "astrbot.dashboard.server.get_astrbot_data_path", + lambda: str(data_dir), + ) + monkeypatch.setattr( + "astrbot.dashboard.server.get_bundled_dashboard_dist_path", + lambda: bundled_dist, + ) + monkeypatch.setattr( + "astrbot.dashboard.server.should_use_bundled_dashboard_dist", + lambda *_args, **_kwargs: True, + ) + + shutdown_event = asyncio.Event() + server = AstrBotDashboard(core_lifecycle_td, core_lifecycle_td.db, shutdown_event) + + assert server.data_path == str(bundled_dist) + + +async def _set_dashboard_password_change_required( + core_lifecycle_td: AstrBotCoreLifecycle, + required: bool, +) -> None: + await set_password_change_required( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + required, + ) + + +async def _restore_dashboard_password_state( + core_lifecycle_td: AstrBotCoreLifecycle, + dashboard_config: dict, +) -> None: + core_lifecycle_td.astrbot_config["dashboard"] = dashboard_config + await set_password_change_required( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + False, + ) + await set_password_storage_upgraded( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + bool(dashboard_config.get("pbkdf2_password")), + ) @pytest_asyncio.fixture(scope="module") async def authenticated_header(app: Quart, core_lifecycle_td: AstrBotCoreLifecycle): """Handles login and returns an authenticated header.""" @@ -180,7 +280,7 @@ async def authenticated_header(app: Quart, core_lifecycle_td: AstrBotCoreLifecyc "/api/auth/login", json={ "username": core_lifecycle_td.astrbot_config["dashboard"]["username"], - + "password": _resolve_dashboard_password(core_lifecycle_td), }, ) @@ -211,7 +311,7 @@ async def test_auth_login( "/api/auth/login", json={ "username": core_lifecycle_td.astrbot_config["dashboard"]["username"], - + "password": _resolve_dashboard_password(core_lifecycle_td), }, ) @@ -241,7 +341,7 @@ async def test_auth_login_secure_cookie_override( "/api/auth/login", json={ "username": core_lifecycle_td.astrbot_config["dashboard"]["username"], - "password": core_lifecycle_td.astrbot_config["dashboard"]["password"], + "password": _resolve_dashboard_password(core_lifecycle_td), }, ) assert response.status_code == 200 @@ -256,6 +356,437 @@ async def test_auth_login_secure_cookie_override( assert "SameSite=Strict" in jwt_cookie_header +@pytest.mark.asyncio +async def test_legacy_md5_dashboard_password_keeps_legacy_auth_until_edit( + app: Quart, + core_lifecycle_td: AstrBotCoreLifecycle, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + legacy_password = "AstrbotLegacy123" + changed_password = "AstrbotChanged123" + + try: + core_lifecycle_td.astrbot_config["dashboard"]["username"] = "astrbot" + core_lifecycle_td.astrbot_config["dashboard"]["password"] = ( + hash_legacy_dashboard_password(legacy_password) + ) + core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"] = "" + await _set_dashboard_password_change_required(core_lifecycle_td, False) + await set_password_storage_upgraded( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + False, + ) + + response = await test_client.post( + "/api/auth/login", + json={"username": "astrbot", "password": legacy_password}, + ) + data = await response.get_json() + assert data["status"] == "ok" + assert data["data"]["change_pwd_hint"] is False + assert data["data"]["legacy_pwd_hint"] is True + assert data["data"]["password_upgrade_required"] is True + + response = await test_client.post( + "/api/auth/account/edit", + json={ + "password": legacy_password, + "new_password": "", + "confirm_password": "", + "new_username": "astrbot-admin", + }, + ) + data = await response.get_json() + assert data["status"] == "error" + assert ( + await is_password_storage_upgraded( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + ) + is False + ) + + response = await test_client.post( + "/api/auth/account/edit", + json={ + "password": legacy_password, + "new_password": changed_password, + "confirm_password": changed_password, + "new_username": "astrbot", + }, + ) + data = await response.get_json() + assert data["status"] == "ok" + assert ( + await is_password_storage_upgraded( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + ) + is True + ) + assert verify_dashboard_password( + core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"], + changed_password, + ) + assert verify_dashboard_password( + core_lifecycle_td.astrbot_config["dashboard"]["password"], + changed_password, + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + +@pytest.mark.asyncio +async def test_legacy_md5_login_failure_includes_upgrade_faq_hint( + app: Quart, + core_lifecycle_td: AstrBotCoreLifecycle, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + legacy_password = "AstrbotLegacy123" + + try: + core_lifecycle_td.astrbot_config["dashboard"]["username"] = "astrbot" + core_lifecycle_td.astrbot_config["dashboard"]["password"] = ( + hash_legacy_dashboard_password(legacy_password) + ) + core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"] = "" + await _set_dashboard_password_change_required(core_lifecycle_td, False) + await set_password_storage_upgraded( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + False, + ) + + response = await test_client.post( + "/api/auth/login", + json={"username": "astrbot", "password": "WrongPassword123"}, + ) + data = await response.get_json() + + assert data["status"] == "error" + assert data["message"].startswith("Incorrect username or password.") + assert "用户名或密码错误" in data["message"] + assert "https://docs.astrbot.app/en/faq.html" in data["message"] + assert "https://docs.astrbot.app/faq.html" in data["message"] + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + +@pytest.mark.asyncio +async def test_password_storage_flag_repairs_after_rollback_clears_pbkdf2( + app: Quart, + core_lifecycle_td: AstrBotCoreLifecycle, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + legacy_password = "AstrbotRollback123" + + try: + core_lifecycle_td.astrbot_config["dashboard"]["username"] = "astrbot" + core_lifecycle_td.astrbot_config["dashboard"]["password"] = ( + hash_legacy_dashboard_password(legacy_password) + ) + core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"] = "" + await _set_dashboard_password_change_required(core_lifecycle_td, False) + await set_password_storage_upgraded( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + True, + ) + + response = await test_client.post( + "/api/auth/login", + json={"username": "astrbot", "password": legacy_password}, + ) + data = await response.get_json() + + assert data["status"] == "ok" + assert data["data"]["legacy_pwd_hint"] is True + assert data["data"]["password_upgrade_required"] is True + assert ( + await is_password_storage_upgraded( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + ) + is False + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + +def test_password_hash_lookup_falls_back_to_legacy_when_pbkdf2_missing( + core_lifecycle_td: AstrBotCoreLifecycle, +): + dashboard_config = copy.deepcopy(core_lifecycle_td.astrbot_config["dashboard"]) + legacy_hash = hash_legacy_dashboard_password("AstrbotRollback123") + + try: + core_lifecycle_td.astrbot_config["dashboard"]["password"] = legacy_hash + core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"] = "" + + assert ( + get_dashboard_password_hash( + core_lifecycle_td.astrbot_config, + upgraded=True, + ) + == legacy_hash + ) + finally: + core_lifecycle_td.astrbot_config["dashboard"] = dashboard_config + + +@pytest.mark.asyncio +async def test_generated_password_requires_password_change_until_changed( + app: Quart, + core_lifecycle_td: AstrBotCoreLifecycle, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + changed_password = "AstrbotChanged123" + + try: + await _set_dashboard_password_change_required(core_lifecycle_td, True) + + response = await test_client.post( + "/api/auth/login", + json={ + "username": core_lifecycle_td.astrbot_config["dashboard"]["username"], + "password": _resolve_dashboard_password(core_lifecycle_td), + }, + ) + data = await response.get_json() + assert data["status"] == "ok" + assert data["data"]["change_pwd_hint"] is True + + response = await test_client.post( + "/api/auth/account/edit", + json={ + "password": _resolve_dashboard_password(core_lifecycle_td), + "new_password": "", + "confirm_password": "", + "new_username": core_lifecycle_td.astrbot_config["dashboard"][ + "username" + ], + }, + ) + data = await response.get_json() + assert data["status"] == "error" + assert ( + await is_password_change_required( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + ) + is True + ) + + response = await test_client.post( + "/api/auth/account/edit", + json={ + "password": _resolve_dashboard_password(core_lifecycle_td), + "new_password": changed_password, + "confirm_password": changed_password, + "new_username": core_lifecycle_td.astrbot_config["dashboard"][ + "username" + ], + }, + ) + data = await response.get_json() + assert data["status"] == "ok" + assert ( + await is_password_change_required( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + ) + is False + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + +@pytest.mark.asyncio +async def test_local_setup_can_skip_default_password_auth( + app: Quart, + core_lifecycle_td: AstrBotCoreLifecycle, + monkeypatch: pytest.MonkeyPatch, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + setup_password = "AstrbotSetup123" + setup_username = "astrbot-admin" + + try: + monkeypatch.setenv("ASTRBOT_DASHBOARD_SKIP_DEFAULT_PASSWORD_AUTH", "true") + core_lifecycle_td.astrbot_config["dashboard"]["host"] = "127.0.0.1" + await _set_dashboard_password_change_required(core_lifecycle_td, True) + + response = await test_client.get("/api/auth/setup-status") + data = await response.get_json() + assert data["status"] == "ok" + assert data["data"]["setup_required"] is True + assert data["data"]["skip_default_password_auth"] is True + + response = await test_client.post( + "/api/auth/setup", + json={ + "username": setup_username, + "password": setup_password, + "confirm_password": setup_password, + }, + ) + data = await response.get_json() + assert data["status"] == "ok" + assert data["data"]["username"] == setup_username + assert data["data"]["token"] + assert ( + await is_password_change_required( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + ) + is False + ) + assert ( + core_lifecycle_td.astrbot_config["dashboard"]["username"] == setup_username + ) + assert verify_dashboard_password( + core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"], + setup_password, + ) + assert verify_dashboard_password( + core_lifecycle_td.astrbot_config["dashboard"]["password"], + setup_password, + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + +@pytest.mark.asyncio +async def test_authenticated_default_password_login_can_complete_setup( + app: Quart, + core_lifecycle_td: AstrBotCoreLifecycle, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + setup_password = "AstrbotSetup123" + setup_username = "astrbot-admin" + + try: + await _set_dashboard_password_change_required(core_lifecycle_td, True) + + login_response = await test_client.post( + "/api/auth/login", + json={ + "username": core_lifecycle_td.astrbot_config["dashboard"]["username"], + "password": _resolve_dashboard_password(core_lifecycle_td), + }, + ) + login_data = await login_response.get_json() + assert login_data["status"] == "ok" + assert login_data["data"]["change_pwd_hint"] is True + token = login_data["data"]["token"] + + response = await test_client.post( + "/api/auth/setup-authenticated", + headers={"Authorization": f"Bearer {token}"}, + json={ + "username": setup_username, + "password": setup_password, + "confirm_password": setup_password, + }, + ) + data = await response.get_json() + assert data["status"] == "ok" + assert data["data"]["username"] == setup_username + assert ( + await is_password_change_required( + core_lifecycle_td.db, + core_lifecycle_td.astrbot_config, + ) + is False + ) + assert verify_dashboard_password( + core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"], + setup_password, + ) + assert verify_dashboard_password( + core_lifecycle_td.astrbot_config["dashboard"]["password"], + setup_password, + ) + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + +@pytest.mark.asyncio +async def test_setup_skip_requires_local_host( + app: Quart, + core_lifecycle_td: AstrBotCoreLifecycle, + monkeypatch: pytest.MonkeyPatch, +): + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config["dashboard"] + ) + test_client = app.test_client() + + try: + monkeypatch.setenv("ASTRBOT_DASHBOARD_SKIP_DEFAULT_PASSWORD_AUTH", "true") + core_lifecycle_td.astrbot_config["dashboard"]["host"] = "0.0.0.0" + await _set_dashboard_password_change_required(core_lifecycle_td, True) + + response = await test_client.get("/api/auth/setup-status") + data = await response.get_json() + assert data["status"] == "ok" + assert data["data"]["setup_required"] is True + assert data["data"]["skip_default_password_auth"] is False + + response = await test_client.post( + "/api/auth/setup", + json={ + "username": "astrbot-admin", + "password": "AstrbotSetup123", + "confirm_password": "AstrbotSetup123", + }, + ) + data = await response.get_json() + assert data["status"] == "error" + finally: + await _restore_dashboard_password_state( + core_lifecycle_td, + original_dashboard_config, + ) + + @pytest.mark.asyncio async def test_plugin_web_api_supports_dynamic_route( app: Quart, @@ -411,7 +942,7 @@ async def test_plugin_page_content_supports_cookie_auth( "/api/auth/login", json={ "username": core_lifecycle_td.astrbot_config["dashboard"]["username"], - "password": core_lifecycle_td.astrbot_config["dashboard"]["password"], + "password": _resolve_dashboard_password(core_lifecycle_td), }, ) assert login_response.status_code == 200 @@ -577,7 +1108,7 @@ async def test_logout_clears_cookie_for_plugin_page( "/api/auth/login", json={ "username": core_lifecycle_td.astrbot_config["dashboard"]["username"], - "password": core_lifecycle_td.astrbot_config["dashboard"]["password"], + "password": _resolve_dashboard_password(core_lifecycle_td), }, ) assert response.status_code == 200 @@ -1036,7 +1567,7 @@ async def test_t2i_set_active_template_syncs_all_configs( "/api/t2i/templates/create", json={ "name": template_name, - "content": "{{ content }}", + "content": "{{ text }}", }, headers=authenticated_header, ) @@ -1103,7 +1634,7 @@ async def test_t2i_reset_default_template_syncs_all_configs( "/api/t2i/templates/create", json={ "name": template_name, - "content": "{{ content }} reset", + "content": "{{ text }} reset", }, headers=authenticated_header, ) @@ -1176,7 +1707,7 @@ async def test_t2i_update_active_template_reloads_all_schedulers( "/api/t2i/templates/create", json={ "name": template_name, - "content": "{{ content }} v1", + "content": "{{ text }} v1", }, headers=authenticated_header, ) @@ -1197,7 +1728,7 @@ async def test_t2i_update_active_template_reloads_all_schedulers( response = await test_client.put( f"/api/t2i/templates/{template_name}", - json={"content": "{{ content }} v2"}, + json={"content": "{{ text }} v2"}, headers=authenticated_header, ) assert response.status_code == 200 @@ -1279,13 +1810,22 @@ async def test_do_update( # Use a temporary path for the mock update to avoid side effects temp_release_dir = tmp_path_factory.mktemp("release") release_path = temp_release_dir / "astrbot" + calls = [] async def mock_update(*args, **kwargs): """Mocks the update process by creating a directory in the temp path.""" + calls.append("core") + callback = kwargs.get("progress_callback") + if callback: + callback({"downloaded": 10, "total": 10, "percent": 1, "speed": 1}) os.makedirs(release_path, exist_ok=True) async def mock_download_dashboard(*args, **kwargs): """Mocks the dashboard download to prevent network access.""" + calls.append("dashboard") + callback = kwargs.get("progress_callback") + if callback: + callback({"downloaded": 10, "total": 10, "percent": 1, "speed": 1}) return async def mock_pip_install(*args, **kwargs): @@ -1305,12 +1845,22 @@ async def test_do_update( response = await test_client.post( "/api/update/do", headers=authenticated_header, - json={"version": "v3.4.0", "reboot": False}, + json={"version": "v3.4.0", "reboot": False, "progress_id": "test-progress"}, ) assert response.status_code == 200 data = await response.get_json() assert data["status"] == "ok" assert os.path.exists(release_path) + assert calls[:2] == ["dashboard", "core"] + + progress_response = await test_client.get( + "/api/update/progress?id=test-progress", + headers=authenticated_header, + ) + progress_data = await progress_response.get_json() + assert progress_data["status"] == "ok" + assert progress_data["data"]["status"] == "success" + assert progress_data["data"]["overall_percent"] == 100 @pytest.mark.asyncio diff --git a/tests/test_dingtalk_app_registration.py b/tests/test_dingtalk_app_registration.py new file mode 100644 index 000000000..63ea95905 --- /dev/null +++ b/tests/test_dingtalk_app_registration.py @@ -0,0 +1,47 @@ +from astrbot.core.platform.sources.dingtalk.app_registration import ( + DEFAULT_DINGTALK_REGISTRATION_BASE_URL, + DEFAULT_DINGTALK_REGISTRATION_SOURCE, + dingtalk_registration_base_url, + dingtalk_registration_poll_result, + dingtalk_registration_source, +) + + +def test_dingtalk_registration_defaults(monkeypatch): + monkeypatch.delenv("DINGTALK_REGISTRATION_BASE_URL", raising=False) + monkeypatch.delenv("DINGTALK_REGISTRATION_SOURCE", raising=False) + + assert dingtalk_registration_base_url() == DEFAULT_DINGTALK_REGISTRATION_BASE_URL + assert dingtalk_registration_source() == DEFAULT_DINGTALK_REGISTRATION_SOURCE + + +def test_dingtalk_registration_poll_result_maps_waiting_and_success(): + assert dingtalk_registration_poll_result({"status": "WAITING"}) == { + "status": "pending" + } + + assert dingtalk_registration_poll_result( + { + "status": "SUCCESS", + "client_id": "client-id", + "client_secret": "client-secret", + } + ) == { + "status": "created", + "client_id": "client-id", + "client_secret": "client-secret", + } + + +def test_dingtalk_registration_poll_result_maps_fail_and_expired(): + assert dingtalk_registration_poll_result( + {"status": "FAIL", "fail_reason": "denied"} + ) == { + "status": "error", + "message": "denied", + } + + assert dingtalk_registration_poll_result({"status": "EXPIRED"}) == { + "status": "expired", + "message": "钉钉扫码已过期,请重新创建", + } diff --git a/tests/test_discord_command_sync.py b/tests/test_discord_command_sync.py new file mode 100644 index 000000000..2dee1cadb --- /dev/null +++ b/tests/test_discord_command_sync.py @@ -0,0 +1,57 @@ +import asyncio +from unittest.mock import Mock + +import pytest + +from tests.fixtures.mocks.discord import ( + MockDiscordBuilder, + mock_discord_modules, # noqa: F401 +) + + +class DiscordSyncError(Exception): + def __init__(self, message: str, code: int | None = None) -> None: + super().__init__(message) + self.code = code + + +def _build_adapter(monkeypatch: pytest.MonkeyPatch): + from astrbot.core.platform.sources.discord import discord_platform_adapter + from astrbot.core.platform.sources.discord.discord_platform_adapter import ( + DiscordPlatformAdapter, + ) + + monkeypatch.setattr(discord_platform_adapter, "star_handlers_registry", []) + monkeypatch.setattr( + discord_platform_adapter.discord, + "HTTPException", + DiscordSyncError, + raising=False, + ) + + adapter = DiscordPlatformAdapter( + {"discord_command_register": True}, + {}, + asyncio.Queue(), + ) + adapter.client = MockDiscordBuilder.create_client() + return adapter + + +@pytest.mark.asyncio +async def test_discord_command_sync_ignores_daily_quota(monkeypatch): + from astrbot.core.platform.sources.discord import discord_platform_adapter + + adapter = _build_adapter(monkeypatch) + warning = Mock() + monkeypatch.setattr(discord_platform_adapter.logger, "warning", warning) + adapter.client.sync_commands.side_effect = DiscordSyncError( + "Max number of daily application command creates reached", + code=30034, + ) + + await adapter._collect_and_register_commands() + + adapter.client.sync_commands.assert_awaited_once() + warning.assert_called_once() + assert "30034" in warning.call_args.args[0] diff --git a/tests/test_kb_import.py b/tests/test_kb_import.py index a6c2f8054..4824e8a3f 100644 --- a/tests/test_kb_import.py +++ b/tests/test_kb_import.py @@ -11,10 +11,15 @@ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle, LifecycleState from astrbot.core.db.sqlite import SQLiteDatabase from astrbot.core.exceptions import KnowledgeBaseUploadError from astrbot.core.knowledge_base.models import KBDocument -from astrbot.core.utils.auth_password import hash_dashboard_password +from astrbot.core.utils.auth_password import ( + hash_dashboard_password, + hash_legacy_dashboard_password, +) from astrbot.dashboard.routes.knowledge_base import KnowledgeBaseRoute from astrbot.dashboard.server import AstrBotDashboard +_TEST_DASHBOARD_PASSWORD = "AstrbotTest123" + @pytest_asyncio.fixture(scope="module") async def core_lifecycle_td(tmp_path_factory): @@ -60,7 +65,25 @@ async def core_lifecycle_td(tmp_path_factory): kb_helper.upload_document.return_value = mock_doc # kb_manager.get_kb.return_value = kb_helper # Removed this line as it's handled above - setattr(core_lifecycle, 'kb_manager', kb_manager) + core_lifecycle.kb_manager = kb_manager + generated_password = getattr( + core_lifecycle.astrbot_config, + "_generated_dashboard_password", + None, + ) + dashboard_password = generated_password or _TEST_DASHBOARD_PASSWORD + if not generated_password: + core_lifecycle.astrbot_config["dashboard"]["pbkdf2_password"] = ( + hash_dashboard_password(dashboard_password) + ) + core_lifecycle.astrbot_config["dashboard"]["password"] = ( + hash_legacy_dashboard_password(dashboard_password) + ) + object.__setattr__( + core_lifecycle, + "_dashboard_plain_password", + dashboard_password, + ) try: yield core_lifecycle @@ -82,12 +105,13 @@ def app(core_lifecycle_td: AstrBotCoreLifecycle): def _resolve_dashboard_password(core_lifecycle_td: AstrBotCoreLifecycle) -> str: - password = core_lifecycle_td.astrbot_config["dashboard"]["password"] - if isinstance(password, str) and ( - password.startswith("pbkdf2_sha256$") or password.startswith("$argon2") - ): - return "astrbot-test-password" - return str(password) + generated_password = getattr(core_lifecycle_td, "_dashboard_plain_password", None) + if generated_password: + return generated_password + password = core_lifecycle_td.astrbot_config["dashboard"]["pbkdf2_password"] + if isinstance(password, str) and password.startswith("pbkdf2_sha256$"): + return "astrbot" + return password @pytest_asyncio.fixture(scope="module") diff --git a/tests/test_lark_app_registration.py b/tests/test_lark_app_registration.py new file mode 100644 index 000000000..b7e9b0bad --- /dev/null +++ b/tests/test_lark_app_registration.py @@ -0,0 +1,32 @@ +from astrbot.core.platform.sources.lark.app_registration import ( + DEFAULT_FEISHU_OPEN_DOMAIN, + DEFAULT_LARK_OPEN_DOMAIN, + _registration_data, + resolve_app_registration_endpoints, +) + + +def test_resolve_app_registration_endpoints_uses_feishu_accounts_domain(): + endpoints = resolve_app_registration_endpoints(DEFAULT_FEISHU_OPEN_DOMAIN) + + assert endpoints.open_base == DEFAULT_FEISHU_OPEN_DOMAIN + assert endpoints.registration == ( + "https://accounts.feishu.cn/oauth/v1/app/registration" + ) + + +def test_resolve_app_registration_endpoints_uses_lark_accounts_domain(): + endpoints = resolve_app_registration_endpoints(DEFAULT_LARK_OPEN_DOMAIN) + + assert endpoints.open_base == DEFAULT_LARK_OPEN_DOMAIN + assert endpoints.registration == ( + "https://accounts.larksuite.com/oauth/v1/app/registration" + ) + + +def test_registration_data_accepts_wrapped_and_plain_payloads(): + wrapped = {"data": {"device_code": "device"}} + plain = {"device_code": "device"} + + assert _registration_data(wrapped) == {"device_code": "device"} + assert _registration_data(plain) == {"device_code": "device"} diff --git a/tests/test_main.py b/tests/test_main.py index 8ed21f7b1..f7fa205dc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,6 @@ import os import sys +from pathlib import Path # 将项目根目录添加到 sys.path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -8,6 +9,7 @@ from unittest import mock import pytest +from astrbot.core.utils.io import should_use_bundled_dashboard_dist from main import check_dashboard_files, check_env @@ -175,8 +177,9 @@ async def test_check_dashboard_files_exists_but_version_mismatch(monkeypatch): """Tests that a warning is logged when dashboard version mismatches.""" monkeypatch.setattr(os.path, "exists", lambda x: True) - with mock.patch("main.get_dashboard_version") as mock_get_version: - mock_get_version.return_value = "v0.0.1" # A different version + with mock.patch( + "main.get_dashboard_version", mock.AsyncMock(return_value="v0.0.1") + ): with mock.patch("main.logger.warning") as mock_logger_warning: await check_dashboard_files() @@ -185,6 +188,64 @@ async def test_check_dashboard_files_exists_but_version_mismatch(monkeypatch): assert "WebUI version mismatch" in call_args[0] +def test_should_use_bundled_dashboard_dist_when_data_dist_is_stale(tmp_path): + user_dist = tmp_path / "user-dist" + bundled_dist = tmp_path / "bundled-dist" + (user_dist / "assets").mkdir(parents=True) + (bundled_dist / "assets").mkdir(parents=True) + (user_dist / "assets" / "version").write_text("v4.24.2", encoding="utf-8") + (bundled_dist / "assets" / "version").write_text("v4.24.4", encoding="utf-8") + + with mock.patch( + "astrbot.core.utils.io.get_bundled_dashboard_dist_path", + return_value=bundled_dist, + ): + assert should_use_bundled_dashboard_dist(user_dist, "v4.24.4") is True + + +def test_should_keep_data_dist_when_version_file_is_malformed(tmp_path): + user_dist = tmp_path / "user-dist" + bundled_dist = tmp_path / "bundled-dist" + (user_dist / "assets").mkdir(parents=True) + (bundled_dist / "assets").mkdir(parents=True) + (user_dist / "assets" / "version").write_text("not-a-version", encoding="utf-8") + + with mock.patch( + "astrbot.core.utils.io.get_bundled_dashboard_dist_path", + return_value=bundled_dist, + ): + assert should_use_bundled_dashboard_dist(user_dist, "4.24.4") is False + + +@pytest.mark.asyncio +async def test_check_dashboard_files_uses_bundled_dist_when_data_dist_is_stale( + tmp_path, +): + """Tests that a stale data/dist does not override bundled dashboard assets.""" + data_dir = tmp_path / "data" + data_dist = data_dir / "dist" + bundled_dist = tmp_path / "bundled-dist" + data_dist.mkdir(parents=True) + bundled_dist.mkdir() + + with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)): + with mock.patch( + "main.get_dashboard_version", mock.AsyncMock(return_value="v0.0.1") + ): + with mock.patch( + "main.should_use_bundled_dashboard_dist", return_value=True + ): + with mock.patch( + "main.get_bundled_dashboard_dist_path", + return_value=Path(bundled_dist), + ): + with mock.patch("main.download_dashboard") as mock_download: + result = await check_dashboard_files() + + assert result == str(bundled_dist) + mock_download.assert_not_called() + + @pytest.mark.asyncio async def test_check_dashboard_files_with_webui_dir_arg(monkeypatch): """Tests that providing a valid webui_dir skips all checks.""" diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py index 6357dacc8..627b754a3 100644 --- a/tests/test_plugin_manager.py +++ b/tests/test_plugin_manager.py @@ -28,13 +28,15 @@ class MockStar: self.info = {"repo": TEST_PLUGIN_REPO, "readme": ""} -def _write_local_test_plugin(plugin_path: Path, repo_url: str): +def _write_local_test_plugin( + plugin_path: Path, repo_url: str, version: str = "1.0.0" +): """Creates a minimal valid plugin structure.""" plugin_path.mkdir(parents=True, exist_ok=True) metadata = { "name": TEST_PLUGIN_NAME, "repo": repo_url, - "version": "1.0.0", + "version": version, "author": "AstrBot Team", "desc": "Local test plugin", "short_desc": "Local test short description", @@ -386,6 +388,41 @@ async def test_install_plugin_from_file_dependency_install_flow( assert ("load", TEST_PLUGIN_DIR) in events +@pytest.mark.asyncio +async def test_install_plugin_from_file_conflict_keeps_failed_plugins_clean( + plugin_manager_pm: PluginManager, + local_updator: Path, + monkeypatch, + tmp_path: Path, +): + zip_file_path = tmp_path / "plugin_upload_helloworld_v2.zip" + zip_file_path.write_text("placeholder", encoding="utf-8") + plugin_store_path = Path(plugin_manager_pm.plugin_store_path) + existing_upload_dirs = set(plugin_store_path.glob("plugin_upload_*")) + + def mock_unzip_file(zip_path: str, target_dir: str) -> None: + assert zip_path == str(zip_file_path) + _write_local_test_plugin( + Path(target_dir), + TEST_PLUGIN_REPO, + version="2.0.0", + ) + + assert local_updator.is_dir() + monkeypatch.setattr(plugin_manager_pm.updator, "unzip_file", mock_unzip_file) + + with pytest.raises(Exception, match=f"安装失败:目录 {TEST_PLUGIN_DIR} 已存在。"): + await plugin_manager_pm.install_plugin_from_file(str(zip_file_path)) + + new_upload_dirs = [ + upload_dir + for upload_dir in plugin_store_path.glob("plugin_upload_*") + if upload_dir not in existing_upload_dirs + ] + assert plugin_manager_pm.failed_plugin_dict == {} + assert new_upload_dirs == [] + + @pytest.mark.asyncio @pytest.mark.parametrize("dependency_install_fails", [False, True]) async def test_reload_failed_plugin_dependency_install_flow( diff --git a/tests/test_profile_aware_tools.py b/tests/test_profile_aware_tools.py index f569c51f5..19e121ee9 100644 --- a/tests/test_profile_aware_tools.py +++ b/tests/test_profile_aware_tools.py @@ -174,7 +174,7 @@ class TestApplySandboxToolsConditional: class TestResolveProfile: """Test smart profile selection logic.""" - def _make_booter(self, profile: str = "python-default"): + def _make_booter(self, profile: str = ""): from astrbot.core.computer.booters.shipyard_neo import ShipyardNeoBooter return ShipyardNeoBooter( @@ -191,9 +191,17 @@ class TestResolveProfile: result = await booter._resolve_profile(client) assert result == "browser-python" + @pytest.mark.asyncio + async def test_user_specified_default_profile_honoured(self): + """User explicitly sets python-default → use it directly.""" + booter = self._make_booter(profile="python-default") + client = SimpleNamespace() # list_profiles should NOT be called + result = await booter._resolve_profile(client) + assert result == "python-default" + @pytest.mark.asyncio async def test_selects_browser_profile(self): - """When multiple profiles available, prefer one with browser.""" + """When profile is empty, prefer an available profile with browser.""" async def _mock_list_profiles(): return SimpleNamespace( diff --git a/tests/test_weixin_oc_login_registration.py b/tests/test_weixin_oc_login_registration.py new file mode 100644 index 000000000..7baad5b29 --- /dev/null +++ b/tests/test_weixin_oc_login_registration.py @@ -0,0 +1,47 @@ +from astrbot.core.platform.sources.weixin_oc.login_registration import ( + DEFAULT_WEIXIN_OC_BASE_URL, + normalize_weixin_oc_base_url, + weixin_oc_login_result, +) + + +def test_normalize_weixin_oc_base_url_uses_default_and_strips_slash(): + assert normalize_weixin_oc_base_url("") == DEFAULT_WEIXIN_OC_BASE_URL + assert ( + normalize_weixin_oc_base_url("https://ilinkai.weixin.qq.com/") + == DEFAULT_WEIXIN_OC_BASE_URL + ) + + +def test_weixin_oc_login_result_maps_confirmed_payload(): + result = weixin_oc_login_result( + { + "status": "confirmed", + "bot_token": "token", + "ilink_bot_id": "bot-id", + "baseurl": "https://example.com/", + "ilink_user_id": "user-id", + }, + default_base_url=DEFAULT_WEIXIN_OC_BASE_URL, + ) + + assert result == { + "status": "created", + "qr_status": "confirmed", + "weixin_oc_token": "token", + "weixin_oc_account_id": "bot-id", + "weixin_oc_base_url": "https://example.com", + "weixin_oc_user_id": "user-id", + } + + +def test_weixin_oc_login_result_maps_wait_and_expired_payloads(): + assert weixin_oc_login_result( + {"status": "wait"}, + default_base_url=DEFAULT_WEIXIN_OC_BASE_URL, + ) == {"status": "pending", "qr_status": "wait"} + + assert weixin_oc_login_result( + {"status": "expired"}, + default_base_url=DEFAULT_WEIXIN_OC_BASE_URL, + ) == {"status": "expired", "qr_status": "expired", "message": "二维码已过期"} diff --git a/tests/unit/test_astr_main_agent.py b/tests/unit/test_astr_main_agent.py index f60d18da4..e20bdd612 100644 --- a/tests/unit/test_astr_main_agent.py +++ b/tests/unit/test_astr_main_agent.py @@ -339,6 +339,23 @@ class TestApplyKb: assert req.system_prompt == "System" + @pytest.mark.asyncio + @pytest.mark.parametrize("prompt", ["", " \n\t"]) + async def test_apply_kb_blank_prompt(self, prompt, mock_event, mock_context): + """Test applying knowledge base when prompt is blank.""" + module = ama + req = ProviderRequest(prompt=prompt, system_prompt="System") + config = module.MainAgentBuildConfig( + tool_call_timeout=60, kb_agentic_mode=False + ) + retrieve = AsyncMock(return_value="KB result") + + with patch("astrbot.core.astr_main_agent.retrieve_knowledge_base", retrieve): + await module._apply_kb(mock_event, req, mock_context, config) + + retrieve.assert_not_awaited() + assert req.system_prompt == "System" + @pytest.mark.asyncio async def test_apply_kb_no_result(self, mock_event, mock_context): """Test applying knowledge base when no result is returned.""" diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index c8f383a29..7afe82ebe 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -8,6 +8,11 @@ import pytest from astrbot.core.config.astrbot_config import AstrBotConfig, RateLimitStrategy from astrbot.core.config.default import DEFAULT_VALUE_MAP from astrbot.core.config.i18n_utils import ConfigMetadataI18n +from astrbot.core.utils.auth_password import ( + DEFAULT_DASHBOARD_PASSWORD, + validate_dashboard_password, + verify_dashboard_password, +) @pytest.fixture @@ -185,6 +190,177 @@ class TestAstrBotConfigLoad: assert config2.check_exist() is True assert os.path.exists(non_existent_path) + def test_empty_dashboard_password_generates_random_password(self, temp_config_path): + """Test that an empty dashboard password is replaced with a random password.""" + default_config = { + "dashboard": { + "username": "astrbot", + "password": "", + }, + } + + config = AstrBotConfig( + config_path=temp_config_path, + default_config=default_config, + ) + + generated_password = getattr(config, "_generated_dashboard_password", None) + assert isinstance(generated_password, str) + validate_dashboard_password(generated_password) + assert verify_dashboard_password( + config["dashboard"]["pbkdf2_password"], + generated_password, + ) + assert config["dashboard"]["pbkdf2_password"].startswith( + "pbkdf2_sha256$600000$" + ) + assert config["dashboard"]["password_change_required"] is True + assert config["dashboard"]["password_storage_upgraded"] is True + assert ( + getattr(config, "_generated_dashboard_password_change_required", False) + is True + ) + assert not verify_dashboard_password( + config["dashboard"]["pbkdf2_password"], + DEFAULT_DASHBOARD_PASSWORD, + ) + assert verify_dashboard_password( + config["dashboard"]["password"], + generated_password, + ) + + def test_empty_dashboard_password_uses_initial_password_env( + self, temp_config_path, monkeypatch + ): + """Test that the generated dashboard password can be provided by env.""" + env_password = "CustomInitial123" + monkeypatch.setenv("ASTRBOT_DASHBOARD_INITIAL_PASSWORD", env_password) + default_config = { + "dashboard": { + "username": "astrbot", + "password": "", + }, + } + + config = AstrBotConfig( + config_path=temp_config_path, + default_config=default_config, + ) + + assert getattr(config, "_generated_dashboard_password", None) == env_password + assert verify_dashboard_password( + config["dashboard"]["pbkdf2_password"], + env_password, + ) + assert verify_dashboard_password( + config["dashboard"]["password"], + env_password, + ) + assert config["dashboard"]["password_change_required"] is True + + def test_initial_dashboard_password_env_must_be_valid( + self, temp_config_path, monkeypatch + ): + """Test that weak env-provided initial passwords fail fast.""" + monkeypatch.setenv("ASTRBOT_DASHBOARD_INITIAL_PASSWORD", "weak") + default_config = { + "dashboard": { + "username": "astrbot", + "password": "", + }, + } + + with pytest.raises(ValueError, match="Password must be at least"): + AstrBotConfig( + config_path=temp_config_path, + default_config=default_config, + ) + + def test_legacy_password_change_required_rotates_and_keeps_config_flag( + self, temp_config_path + ): + """Test that the setup flag stays in dashboard config.""" + default_config = { + "dashboard": { + "username": "astrbot", + "password": "", + "pbkdf2_password": "", + }, + } + with open(temp_config_path, "w", encoding="utf-8") as f: + json.dump( + { + "dashboard": { + "username": "astrbot", + "password": "", + "pbkdf2_password": "pbkdf2_sha256$600000$00$00", + "password_change_required": True, + } + }, + f, + ) + + config = AstrBotConfig( + config_path=temp_config_path, + default_config=default_config, + ) + generated_password = getattr(config, "_generated_dashboard_password", None) + + assert isinstance(generated_password, str) + assert config["dashboard"]["password_change_required"] is True + assert config["dashboard"]["password_storage_upgraded"] is True + assert ( + getattr(config, "_dashboard_password_change_required_from_config", False) + is True + ) + assert verify_dashboard_password( + config["dashboard"]["pbkdf2_password"], generated_password + ) + assert verify_dashboard_password( + config["dashboard"]["password"], generated_password + ) + + def test_legacy_astrbot_user_without_change_flag_keeps_legacy_password( + self, temp_config_path + ): + """Test old MD5 configs keep legacy auth until the manual upgrade.""" + default_config = { + "dashboard": { + "username": "astrbot", + "password": "", + "pbkdf2_password": "", + }, + } + with open(temp_config_path, "w", encoding="utf-8") as f: + json.dump( + { + "dashboard": { + "username": "astrbot", + "password": "77b90590a8945a7d36c963981a307dc9", + } + }, + f, + ) + + config = AstrBotConfig( + config_path=temp_config_path, + default_config=default_config, + ) + generated_password = getattr(config, "_generated_dashboard_password", None) + + assert generated_password is None + assert config["dashboard"]["pbkdf2_password"] == "" + assert verify_dashboard_password( + config["dashboard"]["password"], DEFAULT_DASHBOARD_PASSWORD + ) + + def test_legacy_md5_password_requires_plain_password(self): + """Test that a leaked legacy MD5 hash cannot be used as the login password.""" + legacy_hash = "77b90590a8945a7d36c963981a307dc9" + + assert verify_dashboard_password(legacy_hash, DEFAULT_DASHBOARD_PASSWORD) + assert not verify_dashboard_password(legacy_hash, legacy_hash) + class TestConfigValidation: """Tests for config validation and integrity checking.""" diff --git a/tests/unit/test_cua_computer_use.py b/tests/unit/test_cua_computer_use.py index 7c533083c..6d9f295ea 100644 --- a/tests/unit/test_cua_computer_use.py +++ b/tests/unit/test_cua_computer_use.py @@ -20,6 +20,13 @@ class FakeContext: return self._config +def _clear_cua_session_state(computer_client, session_id: str) -> None: + computer_client.session_booter.pop(session_id, None) + state = getattr(computer_client, "cua_idle_state", {}).pop(session_id, None) + if state is not None and not state.task.done(): + state.task.cancel() + + class FakeShell: def __init__(self): self.commands = [] @@ -266,7 +273,8 @@ def test_cua_default_config_matches_booter_defaults(): assert booter.api_key == CUA_DEFAULT_CONFIG["api_key"] assert sandbox_defaults["cua_image"] == CUA_DEFAULT_CONFIG["image"] assert sandbox_defaults["cua_os_type"] == CUA_DEFAULT_CONFIG["os_type"] - assert sandbox_defaults["cua_ttl"] == CUA_DEFAULT_CONFIG["ttl"] + assert "cua_ttl" not in sandbox_defaults + assert sandbox_defaults["cua_idle_timeout"] == CUA_DEFAULT_CONFIG["idle_timeout"] assert ( sandbox_defaults["cua_telemetry_enabled"] == CUA_DEFAULT_CONFIG["telemetry_enabled"] @@ -367,6 +375,189 @@ async def test_get_booter_shuts_down_client_when_skill_sync_fails(monkeypatch): assert "cua-sync-fail" not in computer_client.session_booter +@pytest.mark.asyncio +async def test_cua_idle_timeout_shuts_down_session_proactively(monkeypatch): + from astrbot.core.computer import computer_client + + shutdowns = [] + + class FakeCuaBooter: + def __init__(self, **kwargs): + self.kwargs = kwargs + + async def boot(self, session_id: str): + self.session_id = session_id + + async def available(self): + return True + + async def shutdown(self): + shutdowns.append(self.session_id) + + monkeypatch.setattr( + computer_client, "_sync_skills_to_sandbox", lambda booter: asyncio.sleep(0) + ) + monkeypatch.setattr( + "astrbot.core.computer.booters.cua.CuaBooter", + FakeCuaBooter, + raising=False, + ) + _clear_cua_session_state(computer_client, "cua-idle-expire") + + ctx = FakeContext( + { + "provider_settings": { + "computer_use_runtime": "sandbox", + "sandbox": { + "booter": "cua", + "cua_idle_timeout": 0.1, + }, + } + } + ) + + booter = await computer_client.get_booter(ctx, "cua-idle-expire") + await asyncio.sleep(0.2) + + assert shutdowns == [booter.session_id] + assert "cua-idle-expire" not in computer_client.session_booter + + +@pytest.mark.asyncio +async def test_cua_idle_timeout_refreshes_on_reuse(monkeypatch): + from astrbot.core.computer import computer_client + + shutdowns = [] + + class FakeCuaBooter: + def __init__(self, **kwargs): + self.kwargs = kwargs + + async def boot(self, session_id: str): + self.session_id = session_id + + async def available(self): + return True + + async def shutdown(self): + shutdowns.append(self.session_id) + + monkeypatch.setattr( + computer_client, "_sync_skills_to_sandbox", lambda booter: asyncio.sleep(0) + ) + monkeypatch.setattr( + "astrbot.core.computer.booters.cua.CuaBooter", + FakeCuaBooter, + raising=False, + ) + _clear_cua_session_state(computer_client, "cua-idle-refresh") + + ctx = FakeContext( + { + "provider_settings": { + "computer_use_runtime": "sandbox", + "sandbox": { + "booter": "cua", + "cua_idle_timeout": 0.2, + }, + } + } + ) + + booter1 = await computer_client.get_booter(ctx, "cua-idle-refresh") + await asyncio.sleep(0.05) + booter2 = await computer_client.get_booter(ctx, "cua-idle-refresh") + await asyncio.sleep(0.05) + + assert booter2 is booter1 + assert shutdowns == [] + + await asyncio.sleep(0.25) + + assert shutdowns == [booter1.session_id] + assert "cua-idle-refresh" not in computer_client.session_booter + + +@pytest.mark.asyncio +async def test_cua_idle_timeout_zero_disables_proactive_shutdown(monkeypatch): + from astrbot.core.computer import computer_client + + shutdowns = [] + + class FakeCuaBooter: + def __init__(self, **kwargs): + self.kwargs = kwargs + + async def boot(self, session_id: str): + self.session_id = session_id + + async def available(self): + return True + + async def shutdown(self): + shutdowns.append(self.session_id) + + monkeypatch.setattr( + computer_client, "_sync_skills_to_sandbox", lambda booter: asyncio.sleep(0) + ) + monkeypatch.setattr( + "astrbot.core.computer.booters.cua.CuaBooter", + FakeCuaBooter, + raising=False, + ) + _clear_cua_session_state(computer_client, "cua-idle-disabled") + + ctx = FakeContext( + { + "provider_settings": { + "computer_use_runtime": "sandbox", + "sandbox": { + "booter": "cua", + "cua_idle_timeout": 0, + }, + } + } + ) + + await computer_client.get_booter(ctx, "cua-idle-disabled") + await asyncio.sleep(0.05) + + assert shutdowns == [] + assert "cua-idle-disabled" in computer_client.session_booter + assert "cua-idle-disabled" not in computer_client.cua_idle_state + + +@pytest.mark.asyncio +async def test_non_cua_booter_does_not_schedule_idle_cleanup(monkeypatch): + from astrbot.core.computer import computer_client + + class FakeShipyardBooter: + async def available(self): + return True + + _clear_cua_session_state(computer_client, "shipyard-session") + computer_client.session_booter["shipyard-session"] = FakeShipyardBooter() + + ctx = FakeContext( + { + "provider_settings": { + "computer_use_runtime": "sandbox", + "sandbox": { + "booter": "shipyard", + "shipyard_endpoint": "http://localhost:8080", + "shipyard_access_token": "token", + "cua_idle_timeout": 0.01, + }, + } + } + ) + + booter = await computer_client.get_booter(ctx, "shipyard-session") + + assert isinstance(booter, FakeShipyardBooter) + assert "shipyard-session" not in computer_client.cua_idle_state + + @pytest.mark.asyncio async def test_cua_components_map_sdk_results(tmp_path): from astrbot.core.computer.booters.cua import ( @@ -1194,7 +1385,8 @@ def test_cua_is_exposed_in_sandbox_config_metadata(): assert "CUA" in booter["labels"] assert "provider_settings.sandbox.cua_image" in items assert "provider_settings.sandbox.cua_os_type" in items - assert "provider_settings.sandbox.cua_ttl" in items + assert "provider_settings.sandbox.cua_ttl" not in items + assert "provider_settings.sandbox.cua_idle_timeout" in items assert "provider_settings.sandbox.cua_telemetry_enabled" in items assert "provider_settings.sandbox.cua_local" in items assert "provider_settings.sandbox.cua_api_key" in items diff --git a/tests/unit/test_message_tools.py b/tests/unit/test_message_tools.py index eedee80ab..28896b6d2 100644 --- a/tests/unit/test_message_tools.py +++ b/tests/unit/test_message_tools.py @@ -133,3 +133,31 @@ async def test_send_message_empty_messages_returns_error(): result = await tool.call(ctx, messages=[], session="oc_xxx") assert "error:" in result assert "messages" in result.lower() + + +@pytest.mark.asyncio +async def test_send_message_missing_image_path_stops_before_send(tmp_path, monkeypatch): + """Missing image paths fail before sending any message components.""" + tool = SendMessageToUserTool() + ctx = _make_context() + missing_image_path = tmp_path / "missing.png" + + async def mock_get_booter(*args, **kwargs): + del args, kwargs + raise RuntimeError("sandbox unavailable") + + monkeypatch.setattr( + "astrbot.core.tools.message_tools.get_booter", + mock_get_booter, + ) + + result = await tool.call( + ctx, + messages=[ + {"type": "plain", "text": "before image"}, + {"type": "image", "path": str(missing_image_path)}, + ], + ) + + assert "error: failed to build messages[1] component: sandbox unavailable" in result + ctx.context.context.send_message.assert_not_called()