Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b9abc093a | |||
| eeec6bcc48 | |||
| bd1c1c7e4f | |||
| 2387abe570 | |||
| 7e43cca134 | |||
| 89fdb18936 | |||
| e710454d18 | |||
| 20024cfec9 | |||
| 042c507127 | |||
| 5e83a19ac5 | |||
| 1f87984133 | |||
| c6739105c4 | |||
| 6fd86eda13 | |||
| 693f2988be | |||
| adcffcc466 | |||
| 238aa30331 | |||
| 26a27776ab | |||
| eb2c88f802 | |||
| 81a0e0f28e | |||
| aa61815fcd | |||
| f34902574f | |||
| b1b031077c | |||
| 7f0e011126 | |||
| 7b7d9f1b8c | |||
| fe040da7a4 | |||
| b98cd1bd72 | |||
| 7fa71c538e | |||
| 97c0be85e4 | |||
| b1273ff997 | |||
| e560f396c5 | |||
| 9c842ecd03 | |||
| 281ac6dcfe | |||
| 7aa44ba3d8 | |||
| 8144b61ae0 | |||
| 3da0c77e87 | |||
| 5e7a0591d9 | |||
| 09d6b715f0 | |||
| f0770c5c4d | |||
| 0858ec4cba | |||
| ae07835da7 | |||
| 6ba1c51cd2 | |||
| 2dc28eff89 | |||
| 68c1e4ecf9 |
@@ -243,10 +243,4 @@ pre-commit install
|
||||
|
||||
</details>
|
||||
|
||||
<div align="center">
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
</div
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.9.2"
|
||||
__version__ = "4.9.0"
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.9.2"
|
||||
VERSION = "4.9.0"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
@@ -108,7 +108,6 @@ DEFAULT_CONFIG = {
|
||||
"provider_id": "",
|
||||
"dual_output": False,
|
||||
"use_file_service": False,
|
||||
"trigger_probability": 1.0,
|
||||
},
|
||||
"provider_ltm_settings": {
|
||||
"group_icl_enable": False,
|
||||
@@ -2210,9 +2209,6 @@ CONFIG_METADATA_2 = {
|
||||
"use_file_service": {
|
||||
"type": "bool",
|
||||
},
|
||||
"trigger_probability": {
|
||||
"type": "float",
|
||||
},
|
||||
},
|
||||
},
|
||||
"provider_ltm_settings": {
|
||||
@@ -2423,14 +2419,6 @@ CONFIG_METADATA_3 = {
|
||||
"provider_tts_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_tts_settings.trigger_probability": {
|
||||
"description": "TTS 触发概率",
|
||||
"type": "float",
|
||||
"slider": {"min": 0, "max": 1, "step": 0.05},
|
||||
"condition": {
|
||||
"provider_tts_settings.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.image_caption_prompt": {
|
||||
"description": "图片转述提示词",
|
||||
"type": "text",
|
||||
@@ -2998,7 +2986,6 @@ CONFIG_METADATA_3 = {
|
||||
"description": "回复概率",
|
||||
"type": "float",
|
||||
"hint": "0.0-1.0 之间的数值",
|
||||
"slider": {"min": 0, "max": 1, "step": 0.05},
|
||||
"condition": {
|
||||
"provider_ltm_settings.active_reply.enable": True,
|
||||
},
|
||||
|
||||
@@ -79,7 +79,6 @@ class ConfigMetadataI18n:
|
||||
"_special",
|
||||
"invisible",
|
||||
"options",
|
||||
"slider",
|
||||
]:
|
||||
if attr in field_data:
|
||||
field_result[attr] = field_data[attr]
|
||||
|
||||
@@ -9,6 +9,8 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn
|
||||
|
||||
from astrbot.core.db.po import (
|
||||
Attachment,
|
||||
CommandConfig,
|
||||
CommandConflict,
|
||||
ConversationV2,
|
||||
Persona,
|
||||
PlatformMessageHistory,
|
||||
@@ -314,6 +316,76 @@ class BaseDatabase(abc.ABC):
|
||||
"""Clear all preferences for a specific scope ID."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_command_configs(self) -> list[CommandConfig]:
|
||||
"""Get all stored command configurations."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_command_config(self, handler_full_name: str) -> CommandConfig | None:
|
||||
"""Fetch a single command configuration by handler."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def upsert_command_config(
|
||||
self,
|
||||
handler_full_name: str,
|
||||
plugin_name: str,
|
||||
module_path: str,
|
||||
original_command: str,
|
||||
*,
|
||||
resolved_command: str | None = None,
|
||||
enabled: bool | None = None,
|
||||
keep_original_alias: bool | None = None,
|
||||
conflict_key: str | None = None,
|
||||
resolution_strategy: str | None = None,
|
||||
note: str | None = None,
|
||||
extra_data: dict | None = None,
|
||||
auto_managed: bool | None = None,
|
||||
) -> CommandConfig:
|
||||
"""Create or update a command configuration."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def delete_command_config(self, handler_full_name: str) -> None:
|
||||
"""Delete a single command configuration."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def delete_command_configs(self, handler_full_names: list[str]) -> None:
|
||||
"""Bulk delete command configurations."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def list_command_conflicts(
|
||||
self,
|
||||
status: str | None = None,
|
||||
) -> list[CommandConflict]:
|
||||
"""List recorded command conflict entries."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def upsert_command_conflict(
|
||||
self,
|
||||
conflict_key: str,
|
||||
handler_full_name: str,
|
||||
plugin_name: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
resolution: str | None = None,
|
||||
resolved_command: str | None = None,
|
||||
note: str | None = None,
|
||||
extra_data: dict | None = None,
|
||||
auto_generated: bool | None = None,
|
||||
) -> CommandConflict:
|
||||
"""Create or update a conflict record."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def delete_command_conflicts(self, ids: list[int]) -> None:
|
||||
"""Delete conflict records."""
|
||||
...
|
||||
|
||||
# @abc.abstractmethod
|
||||
# async def insert_llm_message(
|
||||
# self,
|
||||
|
||||
@@ -234,6 +234,65 @@ class Attachment(SQLModel, table=True):
|
||||
)
|
||||
|
||||
|
||||
class CommandConfig(SQLModel, table=True):
|
||||
"""Per-command configuration overrides for dashboard management."""
|
||||
|
||||
__tablename__ = "command_configs" # type: ignore
|
||||
|
||||
handler_full_name: str = Field(
|
||||
primary_key=True,
|
||||
max_length=512,
|
||||
)
|
||||
plugin_name: str = Field(nullable=False, max_length=255)
|
||||
module_path: str = Field(nullable=False, max_length=255)
|
||||
original_command: str = Field(nullable=False, max_length=255)
|
||||
resolved_command: str | None = Field(default=None, max_length=255)
|
||||
enabled: bool = Field(default=True, nullable=False)
|
||||
keep_original_alias: bool = Field(default=False, nullable=False)
|
||||
conflict_key: str | None = Field(default=None, max_length=255)
|
||||
resolution_strategy: str | None = Field(default=None, max_length=64)
|
||||
note: str | None = Field(default=None, sa_type=Text)
|
||||
extra_data: dict | None = Field(default=None, sa_type=JSON)
|
||||
auto_managed: bool = Field(default=False, nullable=False)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc),
|
||||
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
|
||||
class CommandConflict(SQLModel, table=True):
|
||||
"""Conflict tracking for duplicated command names."""
|
||||
|
||||
__tablename__ = "command_conflicts" # type: ignore
|
||||
|
||||
id: int | None = Field(
|
||||
default=None, primary_key=True, sa_column_kwargs={"autoincrement": True}
|
||||
)
|
||||
conflict_key: str = Field(nullable=False, max_length=255)
|
||||
handler_full_name: str = Field(nullable=False, max_length=512)
|
||||
plugin_name: str = Field(nullable=False, max_length=255)
|
||||
status: str = Field(default="pending", max_length=32)
|
||||
resolution: str | None = Field(default=None, max_length=64)
|
||||
resolved_command: str | None = Field(default=None, max_length=255)
|
||||
note: str | None = Field(default=None, sa_type=Text)
|
||||
extra_data: dict | None = Field(default=None, sa_type=JSON)
|
||||
auto_generated: bool = Field(default=False, nullable=False)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc),
|
||||
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"conflict_key",
|
||||
"handler_full_name",
|
||||
name="uix_conflict_handler",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Conversation:
|
||||
"""LLM 对话类
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import threading
|
||||
import typing as T
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import CursorResult
|
||||
@@ -10,6 +11,8 @@ from sqlmodel import col, delete, desc, func, or_, select, text, update
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.po import (
|
||||
Attachment,
|
||||
CommandConfig,
|
||||
CommandConflict,
|
||||
ConversationV2,
|
||||
Persona,
|
||||
PlatformMessageHistory,
|
||||
@@ -26,6 +29,7 @@ from astrbot.core.db.po import (
|
||||
)
|
||||
|
||||
NOT_GIVEN = T.TypeVar("NOT_GIVEN")
|
||||
TxResult = T.TypeVar("TxResult")
|
||||
|
||||
|
||||
class SQLiteDatabase(BaseDatabase):
|
||||
@@ -670,6 +674,242 @@ class SQLiteDatabase(BaseDatabase):
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
# ====
|
||||
# Command Configuration & Conflict Tracking
|
||||
# ====
|
||||
|
||||
async def _run_in_tx(
|
||||
self,
|
||||
fn: Callable[[AsyncSession], Awaitable[TxResult]],
|
||||
) -> TxResult:
|
||||
async with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
async with session.begin():
|
||||
return await fn(session)
|
||||
|
||||
@staticmethod
|
||||
def _apply_updates(model, **updates) -> None:
|
||||
for field, value in updates.items():
|
||||
if value is not None:
|
||||
setattr(model, field, value)
|
||||
|
||||
@staticmethod
|
||||
def _new_command_config(
|
||||
handler_full_name: str,
|
||||
plugin_name: str,
|
||||
module_path: str,
|
||||
original_command: str,
|
||||
*,
|
||||
resolved_command: str | None = None,
|
||||
enabled: bool | None = None,
|
||||
keep_original_alias: bool | None = None,
|
||||
conflict_key: str | None = None,
|
||||
resolution_strategy: str | None = None,
|
||||
note: str | None = None,
|
||||
extra_data: dict | None = None,
|
||||
auto_managed: bool | None = None,
|
||||
) -> CommandConfig:
|
||||
return CommandConfig(
|
||||
handler_full_name=handler_full_name,
|
||||
plugin_name=plugin_name,
|
||||
module_path=module_path,
|
||||
original_command=original_command,
|
||||
resolved_command=resolved_command,
|
||||
enabled=True if enabled is None else enabled,
|
||||
keep_original_alias=False
|
||||
if keep_original_alias is None
|
||||
else keep_original_alias,
|
||||
conflict_key=conflict_key or original_command,
|
||||
resolution_strategy=resolution_strategy,
|
||||
note=note,
|
||||
extra_data=extra_data,
|
||||
auto_managed=bool(auto_managed),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _new_command_conflict(
|
||||
conflict_key: str,
|
||||
handler_full_name: str,
|
||||
plugin_name: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
resolution: str | None = None,
|
||||
resolved_command: str | None = None,
|
||||
note: str | None = None,
|
||||
extra_data: dict | None = None,
|
||||
auto_generated: bool | None = None,
|
||||
) -> CommandConflict:
|
||||
return CommandConflict(
|
||||
conflict_key=conflict_key,
|
||||
handler_full_name=handler_full_name,
|
||||
plugin_name=plugin_name,
|
||||
status=status or "pending",
|
||||
resolution=resolution,
|
||||
resolved_command=resolved_command,
|
||||
note=note,
|
||||
extra_data=extra_data,
|
||||
auto_generated=bool(auto_generated),
|
||||
)
|
||||
|
||||
async def get_command_configs(self) -> list[CommandConfig]:
|
||||
async with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
result = await session.execute(select(CommandConfig))
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_command_config(
|
||||
self,
|
||||
handler_full_name: str,
|
||||
) -> CommandConfig | None:
|
||||
async with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
return await session.get(CommandConfig, handler_full_name)
|
||||
|
||||
async def upsert_command_config(
|
||||
self,
|
||||
handler_full_name: str,
|
||||
plugin_name: str,
|
||||
module_path: str,
|
||||
original_command: str,
|
||||
*,
|
||||
resolved_command: str | None = None,
|
||||
enabled: bool | None = None,
|
||||
keep_original_alias: bool | None = None,
|
||||
conflict_key: str | None = None,
|
||||
resolution_strategy: str | None = None,
|
||||
note: str | None = None,
|
||||
extra_data: dict | None = None,
|
||||
auto_managed: bool | None = None,
|
||||
) -> CommandConfig:
|
||||
async def _op(session: AsyncSession) -> CommandConfig:
|
||||
config = await session.get(CommandConfig, handler_full_name)
|
||||
if not config:
|
||||
config = self._new_command_config(
|
||||
handler_full_name,
|
||||
plugin_name,
|
||||
module_path,
|
||||
original_command,
|
||||
resolved_command=resolved_command,
|
||||
enabled=enabled,
|
||||
keep_original_alias=keep_original_alias,
|
||||
conflict_key=conflict_key,
|
||||
resolution_strategy=resolution_strategy,
|
||||
note=note,
|
||||
extra_data=extra_data,
|
||||
auto_managed=auto_managed,
|
||||
)
|
||||
session.add(config)
|
||||
else:
|
||||
self._apply_updates(
|
||||
config,
|
||||
plugin_name=plugin_name,
|
||||
module_path=module_path,
|
||||
original_command=original_command,
|
||||
resolved_command=resolved_command,
|
||||
enabled=enabled,
|
||||
keep_original_alias=keep_original_alias,
|
||||
conflict_key=conflict_key,
|
||||
resolution_strategy=resolution_strategy,
|
||||
note=note,
|
||||
extra_data=extra_data,
|
||||
auto_managed=auto_managed,
|
||||
)
|
||||
await session.flush()
|
||||
await session.refresh(config)
|
||||
return config
|
||||
|
||||
return await self._run_in_tx(_op)
|
||||
|
||||
async def delete_command_config(self, handler_full_name: str) -> None:
|
||||
await self.delete_command_configs([handler_full_name])
|
||||
|
||||
async def delete_command_configs(self, handler_full_names: list[str]) -> None:
|
||||
if not handler_full_names:
|
||||
return
|
||||
|
||||
async def _op(session: AsyncSession) -> None:
|
||||
await session.execute(
|
||||
delete(CommandConfig).where(
|
||||
col(CommandConfig.handler_full_name).in_(handler_full_names),
|
||||
),
|
||||
)
|
||||
|
||||
await self._run_in_tx(_op)
|
||||
|
||||
async def list_command_conflicts(
|
||||
self,
|
||||
status: str | None = None,
|
||||
) -> list[CommandConflict]:
|
||||
async with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
query = select(CommandConflict)
|
||||
if status:
|
||||
query = query.where(CommandConflict.status == status)
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def upsert_command_conflict(
|
||||
self,
|
||||
conflict_key: str,
|
||||
handler_full_name: str,
|
||||
plugin_name: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
resolution: str | None = None,
|
||||
resolved_command: str | None = None,
|
||||
note: str | None = None,
|
||||
extra_data: dict | None = None,
|
||||
auto_generated: bool | None = None,
|
||||
) -> CommandConflict:
|
||||
async def _op(session: AsyncSession) -> CommandConflict:
|
||||
result = await session.execute(
|
||||
select(CommandConflict).where(
|
||||
CommandConflict.conflict_key == conflict_key,
|
||||
CommandConflict.handler_full_name == handler_full_name,
|
||||
),
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
if not record:
|
||||
record = self._new_command_conflict(
|
||||
conflict_key,
|
||||
handler_full_name,
|
||||
plugin_name,
|
||||
status=status,
|
||||
resolution=resolution,
|
||||
resolved_command=resolved_command,
|
||||
note=note,
|
||||
extra_data=extra_data,
|
||||
auto_generated=auto_generated,
|
||||
)
|
||||
session.add(record)
|
||||
else:
|
||||
self._apply_updates(
|
||||
record,
|
||||
plugin_name=plugin_name,
|
||||
status=status,
|
||||
resolution=resolution,
|
||||
resolved_command=resolved_command,
|
||||
note=note,
|
||||
extra_data=extra_data,
|
||||
auto_generated=auto_generated,
|
||||
)
|
||||
await session.flush()
|
||||
await session.refresh(record)
|
||||
return record
|
||||
|
||||
return await self._run_in_tx(_op)
|
||||
|
||||
async def delete_command_conflicts(self, ids: list[int]) -> None:
|
||||
if not ids:
|
||||
return
|
||||
|
||||
async def _op(session: AsyncSession) -> None:
|
||||
await session.execute(
|
||||
delete(CommandConflict).where(col(CommandConflict.id).in_(ids)),
|
||||
)
|
||||
|
||||
await self._run_in_tx(_op)
|
||||
|
||||
# ====
|
||||
# Deprecated Methods
|
||||
# ====
|
||||
|
||||
@@ -158,11 +158,7 @@ class RespondStage(Stage):
|
||||
result = event.get_result()
|
||||
if result is None:
|
||||
return
|
||||
if event.get_extra("_streaming_finished", False):
|
||||
# prevent some plugin make result content type to LLM_RESULT after streaming finished, lead to send again
|
||||
return
|
||||
if result.result_content_type == ResultContentType.STREAMING_FINISH:
|
||||
event.set_extra("_streaming_finished", True)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
@@ -43,18 +42,6 @@ class ResultDecorateStage(Stage):
|
||||
"forward_threshold"
|
||||
]
|
||||
|
||||
trigger_probability = ctx.astrbot_config["provider_tts_settings"].get(
|
||||
"trigger_probability",
|
||||
1,
|
||||
)
|
||||
try:
|
||||
self.tts_trigger_probability = max(
|
||||
0.0,
|
||||
min(float(trigger_probability), 1.0),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self.tts_trigger_probability = 1.0
|
||||
|
||||
# 分段回复
|
||||
self.words_count_threshold = int(
|
||||
ctx.astrbot_config["platform_settings"]["segmented_reply"][
|
||||
@@ -259,14 +246,7 @@ class ResultDecorateStage(Stage):
|
||||
and result.is_llm_result()
|
||||
and SessionServiceManager.should_process_tts_request(event)
|
||||
):
|
||||
should_tts = self.tts_trigger_probability >= 1.0 or (
|
||||
self.tts_trigger_probability > 0.0
|
||||
and random.random() <= self.tts_trigger_probability
|
||||
)
|
||||
|
||||
if not should_tts:
|
||||
logger.debug("跳过 TTS:触发概率未命中。")
|
||||
elif not tts_provider:
|
||||
if not tts_provider:
|
||||
logger.warning(
|
||||
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。",
|
||||
)
|
||||
|
||||
@@ -2,19 +2,15 @@ from astrbot.core import html_renderer
|
||||
from astrbot.core.provider import Provider
|
||||
from astrbot.core.star.star_tools import StarTools
|
||||
from astrbot.core.utils.command_parser import CommandParserMixin
|
||||
from astrbot.core.utils.plugin_kv_store import PluginKVStoreMixin
|
||||
|
||||
from .context import Context
|
||||
from .star import StarMetadata, star_map, star_registry
|
||||
from .star_manager import PluginManager
|
||||
|
||||
|
||||
class Star(CommandParserMixin, PluginKVStoreMixin):
|
||||
class Star(CommandParserMixin):
|
||||
"""所有插件(Star)的父类,所有插件都应该继承于这个类"""
|
||||
|
||||
author: str
|
||||
name: str
|
||||
|
||||
def __init__(self, context: Context, config: dict | None = None):
|
||||
StarTools.initialize(context)
|
||||
self.context = context
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from astrbot.core import db_helper
|
||||
from astrbot.core.db.po import CommandConfig
|
||||
from astrbot.core.star.filter.command import CommandFilter
|
||||
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
||||
from astrbot.core.star.filter.permission import PermissionType, PermissionTypeFilter
|
||||
from astrbot.core.star.star import star_map
|
||||
from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandDescriptor:
|
||||
handler: StarHandlerMetadata = field(repr=False)
|
||||
filter_ref: CommandFilter | CommandGroupFilter | None = field(
|
||||
default=None,
|
||||
repr=False,
|
||||
)
|
||||
handler_full_name: str = ""
|
||||
handler_name: str = ""
|
||||
plugin_name: str = ""
|
||||
plugin_display_name: str | None = None
|
||||
module_path: str = ""
|
||||
description: str = ""
|
||||
command_type: str = "command" # "command" | "group" | "sub_command"
|
||||
raw_command_name: str | None = None
|
||||
current_fragment: str | None = None
|
||||
parent_signature: str = ""
|
||||
parent_group_handler: str = ""
|
||||
original_command: str | None = None
|
||||
effective_command: str | None = None
|
||||
aliases: list[str] = field(default_factory=list)
|
||||
permission: str = "everyone"
|
||||
enabled: bool = True
|
||||
is_group: bool = False
|
||||
is_sub_command: bool = False
|
||||
reserved: bool = False
|
||||
config: CommandConfig | None = None
|
||||
has_conflict: bool = False
|
||||
sub_commands: list[CommandDescriptor] = field(default_factory=list)
|
||||
|
||||
|
||||
async def sync_command_configs() -> None:
|
||||
"""同步指令配置,清理过期配置。"""
|
||||
descriptors = _collect_descriptors(include_sub_commands=False)
|
||||
config_records = await db_helper.get_command_configs()
|
||||
config_map = _bind_configs_to_descriptors(descriptors, config_records)
|
||||
live_handlers = {desc.handler_full_name for desc in descriptors}
|
||||
|
||||
stale_configs = [key for key in config_map if key not in live_handlers]
|
||||
if stale_configs:
|
||||
await db_helper.delete_command_configs(stale_configs)
|
||||
|
||||
|
||||
async def toggle_command(handler_full_name: str, enabled: bool) -> CommandDescriptor:
|
||||
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
||||
if not descriptor:
|
||||
raise ValueError("指定的处理函数不存在或不是指令。")
|
||||
|
||||
existing_cfg = await db_helper.get_command_config(handler_full_name)
|
||||
config = await db_helper.upsert_command_config(
|
||||
handler_full_name=handler_full_name,
|
||||
plugin_name=descriptor.plugin_name or "",
|
||||
module_path=descriptor.module_path,
|
||||
original_command=descriptor.original_command or descriptor.handler_name,
|
||||
resolved_command=(
|
||||
existing_cfg.resolved_command
|
||||
if existing_cfg
|
||||
else descriptor.current_fragment
|
||||
),
|
||||
enabled=enabled,
|
||||
keep_original_alias=False,
|
||||
conflict_key=existing_cfg.conflict_key
|
||||
if existing_cfg and existing_cfg.conflict_key
|
||||
else descriptor.original_command,
|
||||
resolution_strategy=existing_cfg.resolution_strategy if existing_cfg else None,
|
||||
note=existing_cfg.note if existing_cfg else None,
|
||||
extra_data=existing_cfg.extra_data if existing_cfg else None,
|
||||
auto_managed=False,
|
||||
)
|
||||
_bind_descriptor_with_config(descriptor, config)
|
||||
await sync_command_configs()
|
||||
return descriptor
|
||||
|
||||
|
||||
async def rename_command(
|
||||
handler_full_name: str,
|
||||
new_fragment: str,
|
||||
) -> CommandDescriptor:
|
||||
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
||||
if not descriptor:
|
||||
raise ValueError("指定的处理函数不存在或不是指令。")
|
||||
|
||||
new_fragment = new_fragment.strip()
|
||||
if not new_fragment:
|
||||
raise ValueError("指令名不能为空。")
|
||||
|
||||
candidate_full = _compose_command(descriptor.parent_signature, new_fragment)
|
||||
if _is_command_in_use(handler_full_name, candidate_full):
|
||||
raise ValueError("新的指令名已被其他指令占用,请换一个名称。")
|
||||
|
||||
config = await db_helper.upsert_command_config(
|
||||
handler_full_name=handler_full_name,
|
||||
plugin_name=descriptor.plugin_name or "",
|
||||
module_path=descriptor.module_path,
|
||||
original_command=descriptor.original_command or descriptor.handler_name,
|
||||
resolved_command=new_fragment,
|
||||
enabled=True if descriptor.enabled else False,
|
||||
keep_original_alias=False,
|
||||
conflict_key=descriptor.original_command,
|
||||
resolution_strategy="manual_rename",
|
||||
note=None,
|
||||
extra_data=None,
|
||||
auto_managed=False,
|
||||
)
|
||||
_bind_descriptor_with_config(descriptor, config)
|
||||
|
||||
await sync_command_configs()
|
||||
return descriptor
|
||||
|
||||
|
||||
async def list_commands() -> list[dict[str, Any]]:
|
||||
descriptors = _collect_descriptors(include_sub_commands=True)
|
||||
config_records = await db_helper.get_command_configs()
|
||||
_bind_configs_to_descriptors(descriptors, config_records)
|
||||
|
||||
conflict_groups = _group_conflicts(descriptors)
|
||||
conflict_handler_names: set[str] = {
|
||||
d.handler_full_name for group in conflict_groups.values() for d in group
|
||||
}
|
||||
|
||||
# 分类,设置冲突标志,将子指令挂载到父指令组
|
||||
group_map: dict[str, CommandDescriptor] = {}
|
||||
sub_commands: list[CommandDescriptor] = []
|
||||
root_commands: list[CommandDescriptor] = []
|
||||
|
||||
for desc in descriptors:
|
||||
desc.has_conflict = desc.handler_full_name in conflict_handler_names
|
||||
if desc.is_group:
|
||||
group_map[desc.handler_full_name] = desc
|
||||
elif desc.is_sub_command:
|
||||
sub_commands.append(desc)
|
||||
else:
|
||||
root_commands.append(desc)
|
||||
|
||||
for sub in sub_commands:
|
||||
if sub.parent_group_handler and sub.parent_group_handler in group_map:
|
||||
group_map[sub.parent_group_handler].sub_commands.append(sub)
|
||||
else:
|
||||
root_commands.append(sub)
|
||||
|
||||
# 指令组 + 普通指令,按 effective_command 字母排序
|
||||
all_commands = list(group_map.values()) + root_commands
|
||||
all_commands.sort(key=lambda d: (d.effective_command or "").lower())
|
||||
|
||||
result = [_descriptor_to_dict(desc) for desc in all_commands]
|
||||
return result
|
||||
|
||||
|
||||
async def list_command_conflicts() -> list[dict[str, Any]]:
|
||||
"""列出所有冲突的指令组。"""
|
||||
descriptors = _collect_descriptors(include_sub_commands=False)
|
||||
config_records = await db_helper.get_command_configs()
|
||||
_bind_configs_to_descriptors(descriptors, config_records)
|
||||
|
||||
conflict_groups = _group_conflicts(descriptors)
|
||||
details = [
|
||||
{
|
||||
"conflict_key": key,
|
||||
"handlers": [
|
||||
{
|
||||
"handler_full_name": item.handler_full_name,
|
||||
"plugin": item.plugin_name,
|
||||
"current_name": item.effective_command,
|
||||
}
|
||||
for item in group
|
||||
],
|
||||
}
|
||||
for key, group in conflict_groups.items()
|
||||
]
|
||||
return details
|
||||
|
||||
|
||||
# Internal helpers ----------------------------------------------------------
|
||||
|
||||
|
||||
def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]:
|
||||
"""收集指令,按需包含子指令。"""
|
||||
descriptors: list[CommandDescriptor] = []
|
||||
for handler in star_handlers_registry:
|
||||
desc = _build_descriptor(handler)
|
||||
if not desc:
|
||||
continue
|
||||
if not include_sub_commands and desc.is_sub_command:
|
||||
continue
|
||||
descriptors.append(desc)
|
||||
return descriptors
|
||||
|
||||
|
||||
def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None:
|
||||
filter_ref = _locate_primary_filter(handler)
|
||||
if filter_ref is None:
|
||||
return None
|
||||
|
||||
plugin_meta = star_map.get(handler.handler_module_path)
|
||||
plugin_name = (
|
||||
plugin_meta.name if plugin_meta else None
|
||||
) or handler.handler_module_path
|
||||
plugin_display = plugin_meta.display_name if plugin_meta else None
|
||||
|
||||
is_sub_command = bool(handler.extras_configs.get("sub_command"))
|
||||
parent_group_handler = ""
|
||||
|
||||
if isinstance(filter_ref, CommandFilter):
|
||||
raw_fragment = getattr(
|
||||
filter_ref, "_original_command_name", filter_ref.command_name
|
||||
)
|
||||
current_fragment = filter_ref.command_name
|
||||
parent_signature = (filter_ref.parent_command_names or [""])[0].strip()
|
||||
# 如果是子指令,尝试找到父指令组的 handler_full_name
|
||||
if is_sub_command and parent_signature:
|
||||
parent_group_handler = _find_parent_group_handler(
|
||||
handler.handler_module_path, parent_signature
|
||||
)
|
||||
else:
|
||||
raw_fragment = getattr(
|
||||
filter_ref, "_original_group_name", filter_ref.group_name
|
||||
)
|
||||
current_fragment = filter_ref.group_name
|
||||
parent_signature = _resolve_group_parent_signature(filter_ref)
|
||||
|
||||
original_command = _compose_command(parent_signature, raw_fragment)
|
||||
effective_command = _compose_command(parent_signature, current_fragment)
|
||||
|
||||
# 确定 command_type
|
||||
if isinstance(filter_ref, CommandGroupFilter):
|
||||
command_type = "group"
|
||||
elif is_sub_command:
|
||||
command_type = "sub_command"
|
||||
else:
|
||||
command_type = "command"
|
||||
|
||||
descriptor = CommandDescriptor(
|
||||
handler=handler,
|
||||
filter_ref=filter_ref,
|
||||
handler_full_name=handler.handler_full_name,
|
||||
handler_name=handler.handler_name,
|
||||
plugin_name=plugin_name,
|
||||
plugin_display_name=plugin_display,
|
||||
module_path=handler.handler_module_path,
|
||||
description=handler.desc or "",
|
||||
command_type=command_type,
|
||||
raw_command_name=raw_fragment,
|
||||
current_fragment=current_fragment,
|
||||
parent_signature=parent_signature,
|
||||
parent_group_handler=parent_group_handler,
|
||||
original_command=original_command,
|
||||
effective_command=effective_command,
|
||||
aliases=sorted(getattr(filter_ref, "alias", set())),
|
||||
permission=_determine_permission(handler),
|
||||
enabled=handler.enabled,
|
||||
is_group=isinstance(filter_ref, CommandGroupFilter),
|
||||
is_sub_command=is_sub_command,
|
||||
reserved=plugin_meta.reserved if plugin_meta else False,
|
||||
)
|
||||
return descriptor
|
||||
|
||||
|
||||
def _build_descriptor_by_full_name(full_name: str) -> CommandDescriptor | None:
|
||||
handler = star_handlers_registry.get_handler_by_full_name(full_name)
|
||||
if not handler:
|
||||
return None
|
||||
return _build_descriptor(handler)
|
||||
|
||||
|
||||
def _locate_primary_filter(
|
||||
handler: StarHandlerMetadata,
|
||||
) -> CommandFilter | CommandGroupFilter | None:
|
||||
for filter_ref in handler.event_filters:
|
||||
if isinstance(filter_ref, (CommandFilter, CommandGroupFilter)):
|
||||
return filter_ref
|
||||
return None
|
||||
|
||||
|
||||
def _determine_permission(handler: StarHandlerMetadata) -> str:
|
||||
for filter_ref in handler.event_filters:
|
||||
if isinstance(filter_ref, PermissionTypeFilter):
|
||||
return (
|
||||
"admin"
|
||||
if filter_ref.permission_type == PermissionType.ADMIN
|
||||
else "member"
|
||||
)
|
||||
return "everyone"
|
||||
|
||||
|
||||
def _resolve_group_parent_signature(group_filter: CommandGroupFilter) -> str:
|
||||
signatures: list[str] = []
|
||||
parent = group_filter.parent_group
|
||||
while parent:
|
||||
signatures.append(getattr(parent, "_original_group_name", parent.group_name))
|
||||
parent = parent.parent_group
|
||||
return " ".join(reversed(signatures)).strip()
|
||||
|
||||
|
||||
def _find_parent_group_handler(module_path: str, parent_signature: str) -> str:
|
||||
"""根据模块路径和父级签名,找到对应的指令组 handler_full_name。"""
|
||||
parent_sig_normalized = parent_signature.strip()
|
||||
for handler in star_handlers_registry:
|
||||
if handler.handler_module_path != module_path:
|
||||
continue
|
||||
filter_ref = _locate_primary_filter(handler)
|
||||
if not isinstance(filter_ref, CommandGroupFilter):
|
||||
continue
|
||||
# 检查该指令组的完整指令名是否匹配 parent_signature
|
||||
group_names = filter_ref.get_complete_command_names()
|
||||
if parent_sig_normalized in group_names:
|
||||
return handler.handler_full_name
|
||||
return ""
|
||||
|
||||
|
||||
def _compose_command(parent_signature: str, fragment: str | None) -> str:
|
||||
fragment = (fragment or "").strip()
|
||||
parent_signature = parent_signature.strip()
|
||||
if not parent_signature:
|
||||
return fragment
|
||||
if not fragment:
|
||||
return parent_signature
|
||||
return f"{parent_signature} {fragment}"
|
||||
|
||||
|
||||
def _bind_descriptor_with_config(
|
||||
descriptor: CommandDescriptor,
|
||||
config: CommandConfig,
|
||||
) -> None:
|
||||
_apply_config_to_descriptor(descriptor, config)
|
||||
_apply_config_to_runtime(descriptor, config)
|
||||
|
||||
|
||||
def _apply_config_to_descriptor(
|
||||
descriptor: CommandDescriptor,
|
||||
config: CommandConfig,
|
||||
) -> None:
|
||||
descriptor.config = config
|
||||
descriptor.enabled = config.enabled
|
||||
|
||||
if config.original_command:
|
||||
descriptor.original_command = config.original_command
|
||||
|
||||
new_fragment = config.resolved_command or descriptor.current_fragment
|
||||
descriptor.current_fragment = new_fragment
|
||||
descriptor.effective_command = _compose_command(
|
||||
descriptor.parent_signature,
|
||||
new_fragment,
|
||||
)
|
||||
|
||||
|
||||
def _apply_config_to_runtime(
|
||||
descriptor: CommandDescriptor,
|
||||
config: CommandConfig,
|
||||
) -> None:
|
||||
descriptor.handler.enabled = config.enabled
|
||||
if descriptor.filter_ref and descriptor.current_fragment:
|
||||
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
|
||||
|
||||
|
||||
def _bind_configs_to_descriptors(
|
||||
descriptors: list[CommandDescriptor],
|
||||
config_records: list[CommandConfig],
|
||||
) -> dict[str, CommandConfig]:
|
||||
config_map = {cfg.handler_full_name: cfg for cfg in config_records}
|
||||
for desc in descriptors:
|
||||
if cfg := config_map.get(desc.handler_full_name):
|
||||
_bind_descriptor_with_config(desc, cfg)
|
||||
return config_map
|
||||
|
||||
|
||||
def _group_conflicts(
|
||||
descriptors: list[CommandDescriptor],
|
||||
) -> dict[str, list[CommandDescriptor]]:
|
||||
conflicts: dict[str, list[CommandDescriptor]] = defaultdict(list)
|
||||
for desc in descriptors:
|
||||
if desc.effective_command and desc.enabled:
|
||||
conflicts[desc.effective_command].append(desc)
|
||||
return {k: v for k, v in conflicts.items() if len(v) > 1}
|
||||
|
||||
|
||||
def _set_filter_fragment(
|
||||
filter_ref: CommandFilter | CommandGroupFilter,
|
||||
fragment: str,
|
||||
) -> None:
|
||||
attr = (
|
||||
"group_name" if isinstance(filter_ref, CommandGroupFilter) else "command_name"
|
||||
)
|
||||
current_value = getattr(filter_ref, attr)
|
||||
if fragment == current_value:
|
||||
return
|
||||
setattr(filter_ref, attr, fragment)
|
||||
if hasattr(filter_ref, "_cmpl_cmd_names"):
|
||||
filter_ref._cmpl_cmd_names = None
|
||||
|
||||
|
||||
def _is_command_in_use(
|
||||
target_handler_full_name: str,
|
||||
candidate_full_command: str,
|
||||
) -> bool:
|
||||
candidate = candidate_full_command.strip()
|
||||
for handler in star_handlers_registry:
|
||||
if handler.handler_full_name == target_handler_full_name:
|
||||
continue
|
||||
filter_ref = _locate_primary_filter(handler)
|
||||
if not filter_ref:
|
||||
continue
|
||||
names = {name.strip() for name in filter_ref.get_complete_command_names()}
|
||||
if candidate in names:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]:
|
||||
result = {
|
||||
"handler_full_name": desc.handler_full_name,
|
||||
"handler_name": desc.handler_name,
|
||||
"plugin": desc.plugin_name,
|
||||
"plugin_display_name": desc.plugin_display_name,
|
||||
"module_path": desc.module_path,
|
||||
"description": desc.description,
|
||||
"type": desc.command_type,
|
||||
"parent_signature": desc.parent_signature,
|
||||
"parent_group_handler": desc.parent_group_handler,
|
||||
"original_command": desc.original_command,
|
||||
"current_fragment": desc.current_fragment,
|
||||
"effective_command": desc.effective_command,
|
||||
"aliases": desc.aliases,
|
||||
"permission": desc.permission,
|
||||
"enabled": desc.enabled,
|
||||
"is_group": desc.is_group,
|
||||
"has_conflict": desc.has_conflict,
|
||||
"reserved": desc.reserved,
|
||||
}
|
||||
# 如果是指令组,包含子指令列表
|
||||
if desc.is_group and desc.sub_commands:
|
||||
result["sub_commands"] = [_descriptor_to_dict(sub) for sub in desc.sub_commands]
|
||||
else:
|
||||
result["sub_commands"] = []
|
||||
return result
|
||||
@@ -40,6 +40,7 @@ class CommandFilter(HandlerFilter):
|
||||
):
|
||||
self.command_name = command_name
|
||||
self.alias = alias if alias else set()
|
||||
self._original_command_name = command_name
|
||||
self.parent_command_names = (
|
||||
parent_command_names if parent_command_names is not None else [""]
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ class CommandGroupFilter(HandlerFilter):
|
||||
):
|
||||
self.group_name = group_name
|
||||
self.alias = alias if alias else set()
|
||||
self._original_group_name = group_name
|
||||
self.sub_command_filters: list[CommandFilter | CommandGroupFilter] = []
|
||||
self.custom_filter_list: list[CustomFilter] = []
|
||||
self.parent_group = parent_group
|
||||
|
||||
@@ -118,6 +118,8 @@ class StarHandlerRegistry(Generic[T]):
|
||||
# 过滤事件类型
|
||||
if handler.event_type != event_type:
|
||||
continue
|
||||
if not handler.enabled:
|
||||
continue
|
||||
# 过滤启用状态
|
||||
if only_activated:
|
||||
plugin = star_map.get(handler.handler_module_path)
|
||||
@@ -220,6 +222,8 @@ class StarHandlerMetadata(Generic[H]):
|
||||
extras_configs: dict = field(default_factory=dict)
|
||||
"""插件注册的一些其他的信息, 如 priority 等"""
|
||||
|
||||
enabled: bool = True
|
||||
|
||||
def __lt__(self, other: StarHandlerMetadata):
|
||||
"""定义小于运算符以支持优先队列"""
|
||||
return self.extras_configs.get("priority", 0) < other.extras_configs.get(
|
||||
|
||||
@@ -23,6 +23,7 @@ from astrbot.core.utils.astrbot_path import (
|
||||
from astrbot.core.utils.io import remove_dir
|
||||
|
||||
from . import StarMetadata
|
||||
from .command_management import sync_command_configs
|
||||
from .context import Context
|
||||
from .filter.permission import PermissionType, PermissionTypeFilter
|
||||
from .star import star_map, star_registry
|
||||
@@ -467,18 +468,6 @@ class PluginManager:
|
||||
metadata.star_cls = metadata.star_cls_type(
|
||||
context=self.context,
|
||||
)
|
||||
|
||||
p_name = (metadata.name or "unknown").lower().replace("/", "_")
|
||||
p_author = (
|
||||
(metadata.author or "unknown").lower().replace("/", "_")
|
||||
)
|
||||
setattr(metadata.star_cls, "name", p_name)
|
||||
setattr(metadata.star_cls, "author", p_author)
|
||||
setattr(
|
||||
metadata.star_cls,
|
||||
"plugin_id",
|
||||
f"{p_author}/{p_name}",
|
||||
)
|
||||
else:
|
||||
logger.info(f"插件 {metadata.name} 已被禁用。")
|
||||
|
||||
@@ -630,6 +619,7 @@ class PluginManager:
|
||||
# 清除 pip.main 导致的多余的 logging handlers
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
await sync_command_configs()
|
||||
|
||||
if not fail_rec:
|
||||
return True, None
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
from typing import TypeVar
|
||||
|
||||
from astrbot.core import sp
|
||||
|
||||
SUPPORTED_VALUE_TYPES = int | float | str | bytes | bool | dict | list | None
|
||||
_VT = TypeVar("_VT")
|
||||
|
||||
|
||||
class PluginKVStoreMixin:
|
||||
"""为插件提供键值存储功能的 Mixin 类"""
|
||||
|
||||
plugin_id: str
|
||||
|
||||
async def put_kv_data(
|
||||
self,
|
||||
key: str,
|
||||
value: SUPPORTED_VALUE_TYPES,
|
||||
) -> None:
|
||||
"""为指定插件存储一个键值对"""
|
||||
await sp.put_async("plugin", self.plugin_id, key, value)
|
||||
|
||||
async def get_kv_data(self, key: str, default: _VT) -> _VT | None:
|
||||
"""获取指定插件存储的键值对"""
|
||||
return await sp.get_async("plugin", self.plugin_id, key, default)
|
||||
|
||||
async def delete_kv_data(self, key: str) -> None:
|
||||
"""删除指定插件存储的键值对"""
|
||||
await sp.remove_async("plugin", self.plugin_id, key)
|
||||
@@ -1,5 +1,6 @@
|
||||
from .auth import AuthRoute
|
||||
from .chat import ChatRoute
|
||||
from .command import CommandRoute
|
||||
from .config import ConfigRoute
|
||||
from .conversation import ConversationRoute
|
||||
from .file import FileRoute
|
||||
@@ -17,6 +18,7 @@ from .update import UpdateRoute
|
||||
__all__ = [
|
||||
"AuthRoute",
|
||||
"ChatRoute",
|
||||
"CommandRoute",
|
||||
"ConfigRoute",
|
||||
"ConversationRoute",
|
||||
"FileRoute",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from quart import request
|
||||
|
||||
from astrbot.core.star.command_management import (
|
||||
list_command_conflicts,
|
||||
list_commands,
|
||||
)
|
||||
from astrbot.core.star.command_management import (
|
||||
rename_command as rename_command_service,
|
||||
)
|
||||
from astrbot.core.star.command_management import (
|
||||
toggle_command as toggle_command_service,
|
||||
)
|
||||
|
||||
from .route import Response, Route, RouteContext
|
||||
|
||||
|
||||
class CommandRoute(Route):
|
||||
def __init__(self, context: RouteContext) -> None:
|
||||
super().__init__(context)
|
||||
self.routes = {
|
||||
"/commands": ("GET", self.get_commands),
|
||||
"/commands/conflicts": ("GET", self.get_conflicts),
|
||||
"/commands/toggle": ("POST", self.toggle_command),
|
||||
"/commands/rename": ("POST", self.rename_command),
|
||||
}
|
||||
self.register_routes()
|
||||
|
||||
async def get_commands(self):
|
||||
commands = await list_commands()
|
||||
summary = {
|
||||
"total": len(commands),
|
||||
"disabled": len([cmd for cmd in commands if not cmd["enabled"]]),
|
||||
"conflicts": len([cmd for cmd in commands if cmd.get("has_conflict")]),
|
||||
}
|
||||
return Response().ok({"items": commands, "summary": summary}).__dict__
|
||||
|
||||
async def get_conflicts(self):
|
||||
conflicts = await list_command_conflicts()
|
||||
return Response().ok(conflicts).__dict__
|
||||
|
||||
async def toggle_command(self):
|
||||
data = await request.get_json()
|
||||
handler_full_name = data.get("handler_full_name")
|
||||
enabled = data.get("enabled")
|
||||
|
||||
if handler_full_name is None or enabled is None:
|
||||
return Response().error("handler_full_name 与 enabled 均为必填。").__dict__
|
||||
|
||||
if isinstance(enabled, str):
|
||||
enabled = enabled.lower() in ("1", "true", "yes", "on")
|
||||
|
||||
try:
|
||||
await toggle_command_service(handler_full_name, bool(enabled))
|
||||
except ValueError as exc:
|
||||
return Response().error(str(exc)).__dict__
|
||||
|
||||
payload = await _get_command_payload(handler_full_name)
|
||||
return Response().ok(payload).__dict__
|
||||
|
||||
async def rename_command(self):
|
||||
data = await request.get_json()
|
||||
handler_full_name = data.get("handler_full_name")
|
||||
new_name = data.get("new_name")
|
||||
|
||||
if not handler_full_name or not new_name:
|
||||
return Response().error("handler_full_name 与 new_name 均为必填。").__dict__
|
||||
|
||||
try:
|
||||
await rename_command_service(handler_full_name, new_name)
|
||||
except ValueError as exc:
|
||||
return Response().error(str(exc)).__dict__
|
||||
|
||||
payload = await _get_command_payload(handler_full_name)
|
||||
return Response().ok(payload).__dict__
|
||||
|
||||
|
||||
async def _get_command_payload(handler_full_name: str):
|
||||
commands = await list_commands()
|
||||
for cmd in commands:
|
||||
if cmd["handler_full_name"] == handler_full_name:
|
||||
return cmd
|
||||
return {}
|
||||
@@ -3,6 +3,7 @@ import traceback
|
||||
from quart import request
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.agent.mcp_client import MCPTool
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.star import star_map
|
||||
|
||||
@@ -296,15 +297,30 @@ class ToolsRoute(Route):
|
||||
"""获取所有注册的工具列表"""
|
||||
try:
|
||||
tools = self.tool_mgr.func_list
|
||||
tools_dict = [
|
||||
{
|
||||
tools_dict = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, MCPTool):
|
||||
origin = "mcp"
|
||||
origin_name = tool.mcp_server_name
|
||||
elif tool.handler_module_path and star_map.get(
|
||||
tool.handler_module_path
|
||||
):
|
||||
star = star_map[tool.handler_module_path]
|
||||
origin = "plugin"
|
||||
origin_name = star.name
|
||||
else:
|
||||
origin = "unknown"
|
||||
origin_name = "unknown"
|
||||
|
||||
tool_info = {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.parameters,
|
||||
"active": tool.active,
|
||||
"origin": origin,
|
||||
"origin_name": origin_name,
|
||||
}
|
||||
for tool in tools
|
||||
]
|
||||
tools_dict.append(tool_info)
|
||||
return Response().ok(data=tools_dict).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
@@ -67,6 +67,7 @@ class AstrBotDashboard:
|
||||
core_lifecycle,
|
||||
core_lifecycle.plugin_manager,
|
||||
)
|
||||
self.command_route = CommandRoute(self.context)
|
||||
self.cr = ConfigRoute(self.context, core_lifecycle)
|
||||
self.lr = LogRoute(self.context, core_lifecycle.log_broker)
|
||||
self.sfr = StaticFileRoute(self.context)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
-
|
||||
@@ -1,17 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
### 修复
|
||||
|
||||
- 企业自部署飞书(自定义 domain)可以接收消息但无法发送消息的问题。
|
||||
- 安装插件 Dialog 的深色样式问题。
|
||||
|
||||
### 优化
|
||||
|
||||
- 避免某些插件在流式响应结束后重d复发送消息的问题。
|
||||
|
||||
### 新增
|
||||
|
||||
- 支持在对话管理批量导出对话轨迹数据为 `jsonl` 格式文件。入口:WebUI -> 对话管理 -> 批量选中 -> 导出。
|
||||
- 支持对 TTS(文本转语音)设置概率触发。
|
||||
- (插件开发)支持在 schema 中对 float 和 int 类型设置 `slider` 滑块控件。例如 `slider: {min: 0, max: 1, step: 0.1}`。
|
||||
- (插件开发)支持 key-value 存储功能。例如使用 `await self.put_kv_data("key", value)`, `await self.get_kv_data("key", default_value)` 和 `await self.delete_kv_data("key")`。
|
||||
+38
-328
@@ -4,42 +4,18 @@
|
||||
<!-- 页面标题 -->
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<div>
|
||||
<h1 class="text-h1 font-weight-bold mb-2">
|
||||
<v-icon color="black" class="me-2">mdi-function-variant</v-icon>{{ tm('title') }}
|
||||
</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis mb-4 d-flex align-center">
|
||||
{{ tm('subtitle') }}
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" size="small" color="primary" class="ms-1 cursor-pointer"
|
||||
@click="openurl('https://astrbot.app/use/function-calling.html')">
|
||||
mdi-information
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tm('tooltip.info') }}</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<v-btn color="primary" prepend-icon="mdi-tools" class="me-2" variant="tonal" @click="showToolsDialog = true"
|
||||
rounded="xl" size="x-large">
|
||||
{{ tm('functionTools.buttons.view') }}({{ tools.length }})
|
||||
</v-btn>
|
||||
<v-btn color="success" prepend-icon="mdi-plus" class="me-2" variant="tonal"
|
||||
@click="showMcpServerDialog = true" rounded="xl" size="x-large">
|
||||
@click="showMcpServerDialog = true" >
|
||||
{{ tm('mcpServers.buttons.add') }}
|
||||
</v-btn>
|
||||
<v-btn color="success" prepend-icon="mdi-refresh" variant="tonal" @click="showSyncMcpServerDialog = true"
|
||||
rounded="xl" size="x-large">
|
||||
>
|
||||
{{ tm('mcpServers.buttons.sync') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<!-- 本地服务器列表 -->
|
||||
|
||||
<!-- MCP 服务器部分 -->
|
||||
|
||||
<div v-if="mcpServers.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-server-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('mcpServers.empty') }}</p>
|
||||
@@ -57,7 +33,6 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex" style="gap: 8px;">
|
||||
<div>
|
||||
<div v-if="item.tools && item.tools.length > 0">
|
||||
@@ -67,8 +42,7 @@
|
||||
<template v-slot:activator="{ props: listToolsProps }">
|
||||
<span class="text-caption text-medium-emphasis cursor-pointer" v-bind="listToolsProps"
|
||||
style="text-decoration: underline;">
|
||||
{{ tm('mcpServers.status.availableTools', { count: item.tools.length }) }} ({{
|
||||
item.tools.length }})
|
||||
{{ tm('mcpServers.status.availableTools', { count: item.tools.length }) }} ({{ item.tools.length }})
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
@@ -78,10 +52,7 @@
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<ul>
|
||||
<li v-for="(tool, idx) in item.tools" :key="idx" style="margin: 8px 0px;">{{
|
||||
tool
|
||||
}}
|
||||
</li>
|
||||
<li v-for="(tool, idx) in item.tools" :key="idx" style="margin: 8px 0px;">{{ tool }}</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
@@ -91,8 +62,6 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
</v-dialog>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,8 +74,6 @@
|
||||
<v-progress-circular indeterminate color="primary" size="16"></v-progress-circular>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
@@ -183,8 +150,7 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
|
||||
<!-- 添加/编辑 MCP 服务器对话框 -->
|
||||
<!-- 同步 MCP 服务器对话框 -->
|
||||
<v-dialog v-model="showSyncMcpServerDialog" max-width="500px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="bg-primary text-white py-3">
|
||||
@@ -240,115 +206,8 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 函数工具对话框 -->
|
||||
<v-dialog v-model="showToolsDialog" max-width="800px">
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
{{ tm('functionTools.title') }}
|
||||
<v-chip color="info" size="small" class="ml-2">{{ tools.length }}</v-chip>
|
||||
</v-card-title>
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-0" v-if="showTools">
|
||||
<div class="pa-4">
|
||||
<div v-if="tools.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('functionTools.empty') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<v-text-field v-model="toolSearch" prepend-inner-icon="mdi-magnify" :label="tm('functionTools.search')"
|
||||
variant="outlined" density="compact" class="mb-4" hide-details clearable></v-text-field>
|
||||
|
||||
<small>复选框代表该工具是否被启用。</small>
|
||||
|
||||
<v-expansion-panels v-model="openedPanel" multiple style="max-height: 500px; overflow-y: auto;">
|
||||
<v-expansion-panel v-for="(tool, index) in filteredTools" :key="index" :value="index"
|
||||
class="mb-2 tool-panel" rounded="lg">
|
||||
<v-expansion-panel-title>
|
||||
<v-row no-gutters align="center">
|
||||
<v-col cols="1">
|
||||
<v-checkbox v-model="tool.active" color="primary" hide-details density="compact" @click.stop
|
||||
@change="toggleToolStatus(tool)"></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon color="primary" class="me-2" size="small">
|
||||
{{ tool.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant' }}
|
||||
</v-icon>
|
||||
<span class="text-body-1 text-high-emphasis font-weight-medium text-truncate"
|
||||
:title="tool.name">
|
||||
{{ formatToolName(tool.name) }}
|
||||
</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="8" class="text-grey">
|
||||
{{ tool.description }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<p class="text-body-1 font-weight-medium mb-3">
|
||||
<v-icon color="primary" size="small" class="me-1">mdi-information</v-icon>
|
||||
{{ tm('functionTools.description') }}
|
||||
</p>
|
||||
<p class="text-body-2 ml-6 mb-4">{{ tool.description }}</p>
|
||||
|
||||
<template v-if="tool.parameters && tool.parameters.properties">
|
||||
<p class="text-body-1 font-weight-medium mb-3">
|
||||
<v-icon color="primary" size="small" class="me-1">mdi-code-json</v-icon>
|
||||
{{ tm('functionTools.parameters') }}
|
||||
</p>
|
||||
|
||||
<v-table density="compact" class="params-table mt-1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tm('functionTools.table.paramName') }}</th>
|
||||
<th>{{ tm('functionTools.table.type') }}</th>
|
||||
<th>{{ tm('functionTools.table.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(param, paramName) in tool.parameters.properties" :key="paramName">
|
||||
<td class="font-weight-medium">{{ paramName }}</td>
|
||||
<td>
|
||||
<v-chip size="x-small" color="primary" text class="text-caption">
|
||||
{{ param.type }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>{{ param.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</template>
|
||||
<div v-else class="text-center pa-4 text-medium-emphasis">
|
||||
<v-icon size="large" color="grey-lighten-1">mdi-code-brackets</v-icon>
|
||||
<p>{{ tm('functionTools.noParameters') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showToolsDialog = false">
|
||||
{{ tm('dialogs.serverDetail.buttons.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
|
||||
location="top">
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack" location="top">
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
@@ -356,15 +215,13 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'ToolUsePage',
|
||||
name: 'McpServersSection',
|
||||
components: {
|
||||
AstrBotConfig,
|
||||
VueMonacoEditor,
|
||||
ItemCard
|
||||
},
|
||||
@@ -377,20 +234,15 @@ export default {
|
||||
return {
|
||||
refreshInterval: null,
|
||||
mcpServers: [],
|
||||
tools: [],
|
||||
showMcpServerDialog: false,
|
||||
|
||||
selectedMcpServerProvider: "modelscope",
|
||||
mcpServerProviderList: ["modelscope"],
|
||||
selectedMcpServerProvider: 'modelscope',
|
||||
mcpServerProviderList: ['modelscope'],
|
||||
mcpProviderToken: '',
|
||||
|
||||
showSyncMcpServerDialog: false,
|
||||
addServerDialogMessage: "",
|
||||
showToolsDialog: false,
|
||||
showTools: true,
|
||||
addServerDialogMessage: '',
|
||||
loading: false,
|
||||
loadingGettingServers: false,
|
||||
mcpServerUpdateLoaders: {}, // record loading state for each server update
|
||||
mcpServerUpdateLoaders: {},
|
||||
isEditMode: false,
|
||||
serverConfigJson: '',
|
||||
jsonError: null,
|
||||
@@ -400,87 +252,50 @@ export default {
|
||||
tools: []
|
||||
},
|
||||
save_message_snack: false,
|
||||
save_message: "",
|
||||
save_message_success: "success",
|
||||
toolSearch: '',
|
||||
openedPanel: [], // 存储打开的面板索引
|
||||
}
|
||||
save_message: '',
|
||||
save_message_success: 'success'
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredTools() {
|
||||
if (!this.toolSearch) return this.tools;
|
||||
|
||||
const searchTerm = this.toolSearch.toLowerCase();
|
||||
return this.tools.filter(tool =>
|
||||
tool.name.toLowerCase().includes(searchTerm) ||
|
||||
tool.description.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
},
|
||||
|
||||
isServerFormValid() {
|
||||
return !!this.currentServer.name && !this.jsonError;
|
||||
},
|
||||
|
||||
// 显示服务器配置的文本摘要
|
||||
getServerConfigSummary() {
|
||||
return (server) => {
|
||||
if (server.command) {
|
||||
return `${server.command} ${(server.args || []).join(' ')}`;
|
||||
}
|
||||
|
||||
// 如果没有command字段,尝试显示其他有意义的配置信息
|
||||
const configKeys = Object.keys(server).filter(key =>
|
||||
!['name', 'active', 'tools'].includes(key)
|
||||
);
|
||||
|
||||
if (configKeys.length > 0) {
|
||||
return this.tm('mcpServers.status.configSummary', { keys: configKeys.join(', ') });
|
||||
}
|
||||
|
||||
return this.tm('mcpServers.status.noConfig');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
// 清除定时器 if it exists
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openurl(url) {
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
formatToolName(name) {
|
||||
if (name.includes(':')) {
|
||||
// MCP 工具通常命名为 mcp:server:tool
|
||||
const parts = name.split(':');
|
||||
return parts[parts.length - 1]; // 返回最后一部分
|
||||
}
|
||||
return name;
|
||||
},
|
||||
|
||||
getServers() {
|
||||
this.loadingGettingServers = true;
|
||||
axios.get('/api/tools/mcp/servers')
|
||||
.then(response => {
|
||||
this.mcpServers = response.data.data || [];
|
||||
this.mcpServers.forEach(server => {
|
||||
// Ensure each server has a loader state
|
||||
if (!this.mcpServerUpdateLoaders[server.name]) {
|
||||
this.mcpServerUpdateLoaders[server.name] = false;
|
||||
}
|
||||
@@ -492,24 +307,12 @@ export default {
|
||||
this.loadingGettingServers = false;
|
||||
});
|
||||
},
|
||||
|
||||
getTools() {
|
||||
axios.get('/api/tools/list')
|
||||
.then(response => {
|
||||
this.tools = response.data.data || [];
|
||||
})
|
||||
.catch(error => {
|
||||
this.showError(this.tm('messages.getToolsError', { error: error.message }));
|
||||
});
|
||||
},
|
||||
|
||||
validateJson() {
|
||||
try {
|
||||
if (!this.serverConfigJson.trim()) {
|
||||
this.jsonError = this.tm('dialogs.addServer.errors.configEmpty');
|
||||
return false;
|
||||
}
|
||||
|
||||
JSON.parse(this.serverConfigJson);
|
||||
this.jsonError = null;
|
||||
return true;
|
||||
@@ -518,61 +321,51 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
setConfigTemplate(type = 'stdio') {
|
||||
let template = {};
|
||||
if (type === 'streamable_http') {
|
||||
template = {
|
||||
transport: "streamable_http",
|
||||
url: "your mcp server url",
|
||||
transport: 'streamable_http',
|
||||
url: 'your mcp server url',
|
||||
headers: {},
|
||||
timeout: 5,
|
||||
sse_read_timeout: 300,
|
||||
sse_read_timeout: 300
|
||||
};
|
||||
} else if (type === 'sse') {
|
||||
template = {
|
||||
transport: "sse",
|
||||
url: "your mcp server url",
|
||||
transport: 'sse',
|
||||
url: 'your mcp server url',
|
||||
headers: {},
|
||||
timeout: 5,
|
||||
sse_read_timeout: 300,
|
||||
sse_read_timeout: 300
|
||||
};
|
||||
} else {
|
||||
template = {
|
||||
command: "python",
|
||||
args: ["-m", "your_module"],
|
||||
command: 'python',
|
||||
args: ['-m', 'your_module']
|
||||
};
|
||||
}
|
||||
this.serverConfigJson = JSON.stringify(template, null, 2);
|
||||
},
|
||||
|
||||
saveServer() {
|
||||
if (!this.validateJson()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
// 解析JSON配置并与基本信息合并
|
||||
try {
|
||||
const configObj = JSON.parse(this.serverConfigJson);
|
||||
|
||||
// 创建要发送的完整配置对象
|
||||
const serverData = {
|
||||
name: this.currentServer.name,
|
||||
active: this.currentServer.active,
|
||||
...configObj
|
||||
};
|
||||
|
||||
const endpoint = this.isEditMode ? '/api/tools/mcp/update' : '/api/tools/mcp/add';
|
||||
|
||||
axios.post(endpoint, serverData)
|
||||
.then(response => {
|
||||
this.loading = false;
|
||||
this.showMcpServerDialog = false;
|
||||
this.addServerDialogMessage = "";
|
||||
this.addServerDialogMessage = '';
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
this.showSuccess(response.data.message || this.tm('messages.saveSuccess'));
|
||||
this.resetForm();
|
||||
})
|
||||
@@ -585,14 +378,12 @@ export default {
|
||||
this.showError(this.tm('dialogs.addServer.errors.jsonParse', { error: e.message }));
|
||||
}
|
||||
},
|
||||
|
||||
deleteServer(server) {
|
||||
let serverName = server.name || server;
|
||||
const serverName = server.name || server;
|
||||
if (confirm(this.tm('dialogs.confirmDelete', { name: serverName }))) {
|
||||
axios.post('/api/tools/mcp/delete', { name: serverName })
|
||||
.then(response => {
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
this.showSuccess(response.data.message || this.tm('messages.deleteSuccess'));
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -600,37 +391,22 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
editServer(server) {
|
||||
// 创建一个不包含基本字段的配置对象副本
|
||||
const configCopy = { ...server };
|
||||
|
||||
// 移除基本字段,只保留配置相关字段
|
||||
try {
|
||||
delete configCopy.name;
|
||||
delete configCopy.active;
|
||||
delete configCopy.tools;
|
||||
delete configCopy.errlogs;
|
||||
} catch (e) {
|
||||
console.error("Error removing basic fields: ", e);
|
||||
}
|
||||
|
||||
// 设置当前服务器的基本信息
|
||||
delete configCopy.name;
|
||||
delete configCopy.active;
|
||||
delete configCopy.tools;
|
||||
delete configCopy.errlogs;
|
||||
this.currentServer = {
|
||||
name: server.name,
|
||||
active: server.active,
|
||||
tools: server.tools || []
|
||||
};
|
||||
|
||||
// 将剩余配置转换为JSON字符串
|
||||
this.serverConfigJson = JSON.stringify(configCopy, null, 2);
|
||||
|
||||
this.isEditMode = true;
|
||||
this.showMcpServerDialog = true;
|
||||
},
|
||||
|
||||
updateServerStatus(server) {
|
||||
// 切换服务器状态
|
||||
this.mcpServerUpdateLoaders[server.name] = true;
|
||||
server.active = !server.active;
|
||||
axios.post('/api/tools/mcp/update', server)
|
||||
@@ -646,20 +422,16 @@ export default {
|
||||
this.mcpServerUpdateLoaders[server.name] = false;
|
||||
});
|
||||
},
|
||||
|
||||
closeServerDialog() {
|
||||
this.showMcpServerDialog = false;
|
||||
this.addServerDialogMessage = '';
|
||||
this.resetForm();
|
||||
},
|
||||
|
||||
testServerConnection() {
|
||||
if (!this.validateJson()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
let configObj;
|
||||
try {
|
||||
configObj = JSON.parse(this.serverConfigJson);
|
||||
@@ -668,9 +440,8 @@ export default {
|
||||
this.showError(this.tm('dialogs.addServer.errors.jsonParse', { error: e.message }));
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/tools/mcp/test', {
|
||||
"mcp_server_config": configObj,
|
||||
mcp_server_config: configObj
|
||||
})
|
||||
.then(response => {
|
||||
this.loading = false;
|
||||
@@ -681,7 +452,6 @@ export default {
|
||||
this.showError(this.tm('messages.testError', { error: error.response?.data?.message || error.message }));
|
||||
});
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.currentServer = {
|
||||
name: '',
|
||||
@@ -692,58 +462,26 @@ export default {
|
||||
this.jsonError = null;
|
||||
this.isEditMode = false;
|
||||
},
|
||||
|
||||
showSuccess(message) {
|
||||
this.save_message = message;
|
||||
this.save_message_success = "success";
|
||||
this.save_message_success = 'success';
|
||||
this.save_message_snack = true;
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
this.save_message = message;
|
||||
this.save_message_success = "error";
|
||||
this.save_message_success = 'error';
|
||||
this.save_message_snack = true;
|
||||
},
|
||||
|
||||
// MCP 市场相关方法已移除
|
||||
|
||||
// 切换工具状态
|
||||
async toggleToolStatus(tool) {
|
||||
try {
|
||||
const response = await axios.post('/api/tools/toggle-tool', {
|
||||
name: tool.name,
|
||||
activate: tool.active
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(response.data.message || this.tm('messages.toggleToolSuccess'));
|
||||
} else {
|
||||
// 如果失败,恢复原状态
|
||||
tool.active = !tool.active;
|
||||
this.showError(response.data.message || this.tm('messages.toggleToolError'));
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果失败,恢复原状态
|
||||
tool.active = !tool.active;
|
||||
this.showError(this.tm('messages.toggleToolError', { error: error.response?.data?.message || error.message }));
|
||||
}
|
||||
},
|
||||
|
||||
// 同步 MCP 服务器
|
||||
async syncMcpServers() {
|
||||
if (!this.selectedMcpServerProvider) {
|
||||
this.showError(this.tm('syncProvider.status.selectProvider'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const requestData = {
|
||||
name: this.selectedMcpServerProvider
|
||||
};
|
||||
|
||||
// 根据不同平台添加相应的参数
|
||||
if (this.selectedMcpServerProvider === 'modelscope') {
|
||||
if (!this.mcpProviderToken.trim()) {
|
||||
this.showError(this.tm('syncProvider.status.enterToken'));
|
||||
@@ -752,61 +490,33 @@ export default {
|
||||
}
|
||||
requestData.access_token = this.mcpProviderToken.trim();
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/tools/mcp/sync-provider', requestData);
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(response.data.message || this.tm('syncProvider.messages.syncSuccess'));
|
||||
this.showSyncMcpServerDialog = false;
|
||||
this.mcpProviderToken = '';
|
||||
// 刷新服务器列表
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('syncProvider.messages.syncError', { error: 'Unknown error' }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('同步 MCP 服务器失败:', error);
|
||||
this.showError(this.tm('syncProvider.messages.syncError', {
|
||||
error: error.response?.data?.message || error.message || '网络连接或访问令牌问题'
|
||||
this.showError(this.tm('syncProvider.messages.syncError', {
|
||||
error: error.response?.data?.message || error.message || '网络连接或访问令牌问题'
|
||||
}));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tools-page {
|
||||
padding: 20px;
|
||||
padding: 0px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tool-chips {
|
||||
max-height: 60px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tool-panel {
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-panel:hover {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.params-table {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.params-table th {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.monaco-container {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
@@ -814,4 +524,4 @@ export default {
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
availablePlugins: string[];
|
||||
hasSystemPluginConflict: boolean;
|
||||
effectiveShowSystemPlugins: boolean;
|
||||
pluginFilter: string;
|
||||
typeFilter: string;
|
||||
permissionFilter: string;
|
||||
statusFilter: string;
|
||||
showSystemPlugins: boolean;
|
||||
searchQuery: string;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:pluginFilter', value: string): void;
|
||||
(e: 'update:typeFilter', value: string): void;
|
||||
(e: 'update:permissionFilter', value: string): void;
|
||||
(e: 'update:statusFilter', value: string): void;
|
||||
(e: 'update:showSystemPlugins', value: boolean): void;
|
||||
(e: 'update:searchQuery', value: string): void;
|
||||
}>();
|
||||
|
||||
// Computed items for selects
|
||||
const pluginItems = computed(() => [
|
||||
{ title: tm('filters.all'), value: 'all' },
|
||||
...props.availablePlugins.map(p => ({ title: p, value: p }))
|
||||
]);
|
||||
|
||||
const typeItems = [
|
||||
{ title: tm('filters.all'), value: 'all' },
|
||||
{ title: tm('type.group'), value: 'group' },
|
||||
{ title: tm('type.command'), value: 'command' },
|
||||
{ title: tm('type.subCommand'), value: 'sub_command' }
|
||||
];
|
||||
|
||||
const permissionItems = [
|
||||
{ title: tm('filters.all'), value: 'all' },
|
||||
{ title: tm('permission.everyone'), value: 'everyone' },
|
||||
{ title: tm('permission.admin'), value: 'admin' }
|
||||
];
|
||||
|
||||
const statusItems = [
|
||||
{ title: tm('filters.all'), value: 'all' },
|
||||
{ title: tm('filters.enabled'), value: 'enabled' },
|
||||
{ title: tm('filters.disabled'), value: 'disabled' },
|
||||
{ title: tm('filters.conflict'), value: 'conflict' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 过滤器行 -->
|
||||
<v-row class="mb-4" align="center">
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
:model-value="pluginFilter"
|
||||
@update:model-value="emit('update:pluginFilter', $event)"
|
||||
:items="pluginItems"
|
||||
:label="tm('filters.byPlugin')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-select
|
||||
:model-value="typeFilter"
|
||||
@update:model-value="emit('update:typeFilter', $event)"
|
||||
:items="typeItems"
|
||||
:label="tm('filters.byType')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-select
|
||||
:model-value="permissionFilter"
|
||||
@update:model-value="emit('update:permissionFilter', $event)"
|
||||
:items="permissionItems"
|
||||
:label="tm('filters.byPermission')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-select
|
||||
:model-value="statusFilter"
|
||||
@update:model-value="emit('update:statusFilter', $event)"
|
||||
:items="statusItems"
|
||||
:label="tm('filters.byStatus')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 搜索栏 + 统计信息行 -->
|
||||
<div class="mb-4 d-flex flex-wrap align-center ga-4">
|
||||
<div style="min-width: 200px; max-width: 350px; flex: 1; border: 1px solid #B9B9B9; border-radius: 16px;">
|
||||
<v-text-field
|
||||
:model-value="searchQuery"
|
||||
@update:model-value="emit('update:searchQuery', $event)"
|
||||
density="compact"
|
||||
:label="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-4">
|
||||
<slot name="stats"></slot>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<v-checkbox
|
||||
:model-value="effectiveShowSystemPlugins"
|
||||
@update:model-value="emit('update:showSystemPlugins', !!$event)"
|
||||
:label="tm('filters.showSystemPlugins')"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="hasSystemPluginConflict"
|
||||
class="system-plugin-checkbox"
|
||||
>
|
||||
<template v-slot:label>
|
||||
<span class="text-body-2">{{ tm('filters.showSystemPlugins') }}</span>
|
||||
<v-tooltip v-if="hasSystemPluginConflict" location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps" size="16" color="warning" class="ml-1">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
{{ tm('filters.systemPluginConflictHint') }}
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.system-plugin-checkbox {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.system-plugin-checkbox :deep(.v-selection-control) {
|
||||
min-height: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,257 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import type { CommandItem, TypeInfo, StatusInfo } from '../types';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
items: CommandItem[];
|
||||
expandedGroups: Set<string>;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle-expand', cmd: CommandItem): void;
|
||||
(e: 'toggle-command', cmd: CommandItem): void;
|
||||
(e: 'rename', cmd: CommandItem): void;
|
||||
(e: 'view-details', cmd: CommandItem): void;
|
||||
}>();
|
||||
|
||||
// 表格表头
|
||||
const commandHeaders = computed(() => [
|
||||
{ title: tm('table.headers.command'), key: 'effective_command', minWidth: '100px' },
|
||||
{ title: tm('table.headers.type'), key: 'type', sortable: false, width: '100px' },
|
||||
{ title: tm('table.headers.plugin'), key: 'plugin', width: '140px' },
|
||||
{ title: tm('table.headers.description'), key: 'description', sortable: false },
|
||||
{ title: tm('table.headers.permission'), key: 'permission', sortable: false, width: '100px' },
|
||||
{ title: tm('table.headers.status'), key: 'enabled', sortable: false, width: '100px' },
|
||||
{ title: tm('table.headers.actions'), key: 'actions', sortable: false, width: '140px' }
|
||||
]);
|
||||
|
||||
// 检查组是否展开
|
||||
const isGroupExpanded = (cmd: CommandItem): boolean => {
|
||||
return props.expandedGroups.has(cmd.handler_full_name);
|
||||
};
|
||||
|
||||
// 获取类型信息
|
||||
const getTypeInfo = (type: string): TypeInfo => {
|
||||
switch (type) {
|
||||
case 'group':
|
||||
return { text: tm('type.group'), color: 'info', icon: 'mdi-folder-outline' };
|
||||
case 'sub_command':
|
||||
return { text: tm('type.subCommand'), color: 'secondary', icon: 'mdi-subdirectory-arrow-right' };
|
||||
default:
|
||||
return { text: tm('type.command'), color: 'primary', icon: 'mdi-console-line' };
|
||||
}
|
||||
};
|
||||
|
||||
// 获取权限颜色
|
||||
const getPermissionColor = (permission: string): string => {
|
||||
switch (permission) {
|
||||
case 'admin': return 'error';
|
||||
default: return 'success';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取权限标签
|
||||
const getPermissionLabel = (permission: string): string => {
|
||||
switch (permission) {
|
||||
case 'admin': return tm('permission.admin');
|
||||
default: return tm('permission.everyone');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态信息
|
||||
const getStatusInfo = (cmd: CommandItem): StatusInfo => {
|
||||
if (cmd.has_conflict) {
|
||||
return { text: tm('status.conflict'), color: 'warning', variant: 'flat' };
|
||||
}
|
||||
if (cmd.enabled) {
|
||||
return { text: tm('status.enabled'), color: 'success', variant: 'flat' };
|
||||
}
|
||||
return { text: tm('status.disabled'), color: 'error', variant: 'outlined' };
|
||||
};
|
||||
|
||||
// 获取行属性
|
||||
const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
const classes: string[] = [];
|
||||
if (item.has_conflict) {
|
||||
classes.push('conflict-row');
|
||||
}
|
||||
if (item.type === 'sub_command') {
|
||||
classes.push('sub-command-row');
|
||||
}
|
||||
if (item.is_group) {
|
||||
classes.push('group-row');
|
||||
}
|
||||
return classes.length > 0 ? { class: classes.join(' ') } : {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="rounded-lg overflow-hidden elevation-1">
|
||||
<v-data-table
|
||||
:headers="commandHeaders"
|
||||
:items="items"
|
||||
item-key="handler_full_name"
|
||||
hover
|
||||
:row-props="getRowProps"
|
||||
:loading="props.loading"
|
||||
>
|
||||
<template v-slot:item.effective_command="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<!-- 展开/折叠按钮(针对指令组) -->
|
||||
<v-btn
|
||||
v-if="item.is_group && item.sub_commands?.length > 0"
|
||||
icon
|
||||
variant="text"
|
||||
size="x-small"
|
||||
class="mr-1"
|
||||
@click.stop="emit('toggle-expand', item)"
|
||||
>
|
||||
<v-icon size="18">{{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
|
||||
</v-btn>
|
||||
<!-- 子指令缩进 -->
|
||||
<div v-else-if="item.type === 'sub_command'" class="ml-6"></div>
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
<code :class="{ 'sub-command-code': item.type === 'sub_command' }">{{ item.effective_command }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.type="{ item }">
|
||||
<v-chip
|
||||
:color="getTypeInfo(item.type).color"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="14">{{ getTypeInfo(item.type).icon }}</v-icon>
|
||||
{{ getTypeInfo(item.type).text }}{{ item.is_group && item.sub_commands?.length > 0 ? `(${item.sub_commands.length})` : '' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.plugin="{ item }">
|
||||
<div class="text-body-2">{{ item.plugin_display_name || item.plugin }}</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.description="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ item.description || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.permission="{ item }">
|
||||
<v-chip :color="getPermissionColor(item.permission)" size="small" class="font-weight-medium">
|
||||
{{ getPermissionLabel(item.permission) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.enabled="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusInfo(item).color"
|
||||
size="small"
|
||||
class="font-weight-medium"
|
||||
:variant="getStatusInfo(item).variant"
|
||||
>
|
||||
{{ getStatusInfo(item).text }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn-group density="default" variant="text" color="primary">
|
||||
<v-btn
|
||||
v-if="!item.enabled"
|
||||
icon
|
||||
size="small"
|
||||
color="success"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-play</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.enable') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-pause</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.disable') }}</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" color="warning" @click="emit('rename', item)">
|
||||
<v-icon size="22">mdi-pencil</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.rename') }}</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" @click="emit('view-details', item)">
|
||||
<v-icon size="22">mdi-information</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.viewDetails') }}</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<div class="text-center pa-8">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-console-line</v-icon>
|
||||
<div class="text-h5 mb-2">{{ tm('empty.noCommands') }}</div>
|
||||
<div class="text-body-1 mb-4">{{ tm('empty.noCommandsDesc') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
code.sub-command-code {
|
||||
background-color: rgba(var(--v-theme-secondary), 0.1);
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 冲突行高亮 */
|
||||
.v-data-table .conflict-row {
|
||||
background: linear-gradient(90deg, rgba(var(--v-theme-warning), 0.15) 0%, rgba(var(--v-theme-warning), 0.05) 100%) !important;
|
||||
border-left: 3px solid rgb(var(--v-theme-warning)) !important;
|
||||
}
|
||||
|
||||
.v-data-table .conflict-row:hover {
|
||||
background: linear-gradient(90deg, rgba(var(--v-theme-warning), 0.25) 0%, rgba(var(--v-theme-warning), 0.1) 100%) !important;
|
||||
}
|
||||
|
||||
/* 指令组行样式 */
|
||||
.v-data-table .group-row {
|
||||
background-color: rgba(var(--v-theme-info), 0.05);
|
||||
}
|
||||
|
||||
.v-data-table .group-row:hover {
|
||||
background-color: rgba(var(--v-theme-info), 0.08) !important;
|
||||
}
|
||||
|
||||
/* 子指令行样式 */
|
||||
.v-data-table .sub-command-row {
|
||||
background-color: rgba(var(--v-theme-info), 0.05);
|
||||
}
|
||||
|
||||
.v-data-table .sub-command-row:hover {
|
||||
background-color: rgba(var(--v-theme-info), 0.08) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import type { CommandItem, TypeInfo } from '../types';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// Props
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
command: CommandItem | null;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void;
|
||||
}>();
|
||||
|
||||
// 获取类型信息
|
||||
const getTypeInfo = (type: string): TypeInfo => {
|
||||
switch (type) {
|
||||
case 'group':
|
||||
return { text: tm('type.group'), color: 'info', icon: 'mdi-folder-outline' };
|
||||
case 'sub_command':
|
||||
return { text: tm('type.subCommand'), color: 'secondary', icon: 'mdi-subdirectory-arrow-right' };
|
||||
default:
|
||||
return { text: tm('type.command'), color: 'primary', icon: 'mdi-console-line' };
|
||||
}
|
||||
};
|
||||
|
||||
// 获取权限颜色
|
||||
const getPermissionColor = (permission: string): string => {
|
||||
switch (permission) {
|
||||
case 'admin': return 'error';
|
||||
default: return 'success';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取权限标签
|
||||
const getPermissionLabel = (permission: string): string => {
|
||||
switch (permission) {
|
||||
case 'admin': return tm('permission.admin');
|
||||
default: return tm('permission.everyone');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog :model-value="show" @update:model-value="emit('update:show', $event)" max-width="500">
|
||||
<v-card v-if="command">
|
||||
<v-card-title class="text-h5">{{ tm('dialogs.details.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list density="compact">
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.type') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip
|
||||
:color="getTypeInfo(command.type).color"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="14">{{ getTypeInfo(command.type).icon }}</v-icon>
|
||||
{{ getTypeInfo(command.type).text }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.handler') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.handler_name }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.module') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.module_path }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.originalCommand') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.original_command }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.effectiveCommand') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.effective_command }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.parent_signature">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.parentGroup') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.parent_signature }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.aliases.length > 0">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.aliases') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip v-for="alias in command.aliases" :key="alias" size="small" class="mr-1">
|
||||
{{ alias }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.is_group && command.sub_commands?.length > 0">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.subCommands') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap ga-1 mt-1">
|
||||
<v-chip
|
||||
v-for="sub in command.sub_commands"
|
||||
:key="sub.handler_full_name"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ sub.current_fragment }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.permission') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip :color="getPermissionColor(command.permission)" size="small">
|
||||
{{ getPermissionLabel(command.permission) }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.has_conflict">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.conflictStatus') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip color="warning" size="small">{{ tm('status.conflict') }}</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" variant="text" @click="emit('update:show', false)">
|
||||
{{ t('core.actions.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import type { CommandItem } from '../types';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// Props
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
command: CommandItem | null;
|
||||
newName: string;
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void;
|
||||
(e: 'update:newName', value: string): void;
|
||||
(e: 'confirm'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog :model-value="show" @update:model-value="emit('update:show', $event)" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">{{ tm('dialogs.rename.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
:model-value="newName"
|
||||
@update:model-value="emit('update:newName', $event)"
|
||||
:label="tm('dialogs.rename.newName')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
autofocus
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="emit('update:show', false)">
|
||||
{{ tm('dialogs.rename.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
:loading="loading"
|
||||
@click="emit('confirm')"
|
||||
>
|
||||
{{ tm('dialogs.rename.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import type { ToolItem } from '../types';
|
||||
|
||||
const { tm: tmTool } = useModuleI18n('features/tooluse');
|
||||
const { tm: tmCommand } = useModuleI18n('features/command');
|
||||
|
||||
const props = defineProps<{
|
||||
items: ToolItem[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle-tool', tool: ToolItem): void;
|
||||
}>();
|
||||
|
||||
const toolHeaders = computed(() => [
|
||||
{ title: tmTool('functionTools.title'), key: 'name', minWidth: '160px' },
|
||||
{ title: tmTool('functionTools.description'), key: 'description' },
|
||||
{ title: tmTool('functionTools.table.origin'), key: 'origin', sortable: false, width: '120px' },
|
||||
{ title: tmTool('functionTools.table.originName'), key: 'origin_name', sortable: false, width: '160px' },
|
||||
{ title: tmCommand('status.enabled'), key: 'active', sortable: false, width: '120px' },
|
||||
{ title: tmTool('functionTools.table.actions'), key: 'actions', sortable: false, width: '120px' }
|
||||
]);
|
||||
|
||||
const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.properties || {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="rounded-lg overflow-hidden elevation-1">
|
||||
<v-data-table
|
||||
:headers="toolHeaders"
|
||||
:items="items"
|
||||
item-key="name"
|
||||
hover
|
||||
show-expand
|
||||
class="tool-table"
|
||||
:loading="props.loading"
|
||||
>
|
||||
<template #item.name="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<v-icon color="primary" class="mr-2" size="18">
|
||||
{{ item.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant' }}
|
||||
</v-icon>
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.description="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ item.description || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.origin="{ item }">
|
||||
<v-chip size="small" variant="tonal" color="info" class="text-caption font-weight-medium">
|
||||
{{ item.origin || '-' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.origin_name="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ item.origin_name || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.active="{ item }">
|
||||
<v-chip :color="item.active ? 'success' : 'error'" size="small" class="font-weight-medium" :variant="item.active ? 'flat' : 'outlined'">
|
||||
{{ item.active ? tmCommand('status.enabled') : tmCommand('status.disabled') }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-switch
|
||||
:model-value="item.active"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
inset
|
||||
@update:model-value="emit('toggle-tool', item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center pa-8">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-function-variant</v-icon>
|
||||
<div class="text-h5 mb-2">{{ tmTool('functionTools.empty') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #expanded-row="{ item }">
|
||||
<td :colspan="toolHeaders.length + 1" class="pa-4">
|
||||
<div class="d-flex align-start ga-4">
|
||||
<v-icon size="20" color="primary">mdi-code-json</v-icon>
|
||||
<div class="flex-1">
|
||||
<div class="text-subtitle-2 font-weight-medium mb-2">{{ tmTool('functionTools.parameters') }}</div>
|
||||
<div v-if="parameterEntries(item).length === 0" class="text-caption text-medium-emphasis">
|
||||
{{ tmTool('functionTools.noParameters') }}
|
||||
</div>
|
||||
<v-table
|
||||
v-else
|
||||
density="compact"
|
||||
class="param-table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left text-caption text-medium-emphasis">{{ tmTool('functionTools.table.paramName') }}</th>
|
||||
<th class="text-left text-caption text-medium-emphasis" style="width: 140px;">{{ tmTool('functionTools.table.type') }}</th>
|
||||
<th class="text-left text-caption text-medium-emphasis">{{ tmTool('functionTools.table.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="([paramName, param]) in parameterEntries(item)" :key="paramName">
|
||||
<td class="font-weight-medium text-body-2">{{ paramName }}</td>
|
||||
<td class="text-body-2">
|
||||
<v-chip size="x-small" color="primary" class="text-caption">
|
||||
{{ param?.type || '-' }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td class="text-body-2 text-medium-emphasis">{{ param?.description || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.param-table {
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tool-table :deep(.v-data-table__td) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 指令操作方法 Composable
|
||||
*/
|
||||
import { reactive } from 'vue';
|
||||
import axios from 'axios';
|
||||
import type { CommandItem, RenameDialogState, DetailsDialogState, TypeInfo, StatusInfo } from '../types';
|
||||
|
||||
export function useCommandActions(
|
||||
toast: (message: string, color?: string) => void,
|
||||
fetchCommands: () => Promise<void>
|
||||
) {
|
||||
// 重命名对话框状态
|
||||
const renameDialog = reactive<RenameDialogState>({
|
||||
show: false,
|
||||
command: null,
|
||||
newName: '',
|
||||
loading: false
|
||||
});
|
||||
|
||||
// 详情对话框状态
|
||||
const detailsDialog = reactive<DetailsDialogState>({
|
||||
show: false,
|
||||
command: null
|
||||
});
|
||||
|
||||
/**
|
||||
* 切换指令启用/禁用状态
|
||||
*/
|
||||
const toggleCommand = async (
|
||||
cmd: CommandItem,
|
||||
successMessage: string,
|
||||
errorMessage: string
|
||||
) => {
|
||||
try {
|
||||
const res = await axios.post('/api/commands/toggle', {
|
||||
handler_full_name: cmd.handler_full_name,
|
||||
enabled: !cmd.enabled
|
||||
});
|
||||
if (res.data.status === 'ok') {
|
||||
toast(successMessage, 'success');
|
||||
await fetchCommands();
|
||||
} else {
|
||||
toast(res.data.message || errorMessage, 'error');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast(err?.message || errorMessage, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开重命名对话框
|
||||
*/
|
||||
const openRenameDialog = (cmd: CommandItem) => {
|
||||
renameDialog.command = cmd;
|
||||
renameDialog.newName = cmd.current_fragment || '';
|
||||
renameDialog.show = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 确认重命名
|
||||
*/
|
||||
const confirmRename = async (successMessage: string, errorMessage: string) => {
|
||||
if (!renameDialog.command || !renameDialog.newName.trim()) return;
|
||||
|
||||
renameDialog.loading = true;
|
||||
try {
|
||||
const res = await axios.post('/api/commands/rename', {
|
||||
handler_full_name: renameDialog.command.handler_full_name,
|
||||
new_name: renameDialog.newName.trim()
|
||||
});
|
||||
if (res.data.status === 'ok') {
|
||||
toast(successMessage, 'success');
|
||||
renameDialog.show = false;
|
||||
await fetchCommands();
|
||||
} else {
|
||||
toast(res.data.message || errorMessage, 'error');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast(err?.message || errorMessage, 'error');
|
||||
} finally {
|
||||
renameDialog.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开详情对话框
|
||||
*/
|
||||
const openDetailsDialog = (cmd: CommandItem) => {
|
||||
detailsDialog.command = cmd;
|
||||
detailsDialog.show = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取类型显示信息
|
||||
*/
|
||||
const getTypeInfo = (type: string, translations: { group: string; subCommand: string; command: string }): TypeInfo => {
|
||||
switch (type) {
|
||||
case 'group':
|
||||
return { text: translations.group, color: 'info', icon: 'mdi-folder-outline' };
|
||||
case 'sub_command':
|
||||
return { text: translations.subCommand, color: 'secondary', icon: 'mdi-subdirectory-arrow-right' };
|
||||
default:
|
||||
return { text: translations.command, color: 'primary', icon: 'mdi-console-line' };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取权限颜色
|
||||
*/
|
||||
const getPermissionColor = (permission: string): string => {
|
||||
switch (permission) {
|
||||
case 'admin': return 'error';
|
||||
default: return 'success';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取权限标签
|
||||
*/
|
||||
const getPermissionLabel = (permission: string, translations: { admin: string; everyone: string }): string => {
|
||||
switch (permission) {
|
||||
case 'admin': return translations.admin;
|
||||
default: return translations.everyone;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取状态显示信息
|
||||
*/
|
||||
const getStatusInfo = (
|
||||
cmd: CommandItem,
|
||||
translations: { conflict: string; enabled: string; disabled: string }
|
||||
): StatusInfo => {
|
||||
if (cmd.has_conflict) {
|
||||
return { text: translations.conflict, color: 'warning', variant: 'flat' };
|
||||
}
|
||||
if (cmd.enabled) {
|
||||
return { text: translations.enabled, color: 'success', variant: 'flat' };
|
||||
}
|
||||
return { text: translations.disabled, color: 'error', variant: 'outlined' };
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取表格行属性(用于冲突高亮和子指令样式)
|
||||
*/
|
||||
const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
const classes: string[] = [];
|
||||
if (item.has_conflict) {
|
||||
classes.push('conflict-row');
|
||||
}
|
||||
if (item.type === 'sub_command') {
|
||||
classes.push('sub-command-row');
|
||||
}
|
||||
if (item.is_group) {
|
||||
classes.push('group-row');
|
||||
}
|
||||
return classes.length > 0 ? { class: classes.join(' ') } : {};
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
renameDialog,
|
||||
detailsDialog,
|
||||
|
||||
// 方法
|
||||
toggleCommand,
|
||||
openRenameDialog,
|
||||
confirmRename,
|
||||
openDetailsDialog,
|
||||
getTypeInfo,
|
||||
getPermissionColor,
|
||||
getPermissionLabel,
|
||||
getStatusInfo,
|
||||
getRowProps
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 指令过滤逻辑 Composable
|
||||
*/
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
import type { CommandItem, FilterState } from '../types';
|
||||
|
||||
export function useCommandFilters(commands: Ref<CommandItem[]>) {
|
||||
// 过滤状态
|
||||
const searchQuery = ref('');
|
||||
const pluginFilter = ref('all');
|
||||
const permissionFilter = ref('all');
|
||||
const statusFilter = ref('all');
|
||||
const typeFilter = ref('all');
|
||||
const showSystemPlugins = ref(false);
|
||||
|
||||
// 展开的指令组
|
||||
const expandedGroups = ref<Set<string>>(new Set());
|
||||
|
||||
/**
|
||||
* 检查是否有涉及系统插件的冲突
|
||||
*/
|
||||
const hasSystemPluginConflict = computed(() => {
|
||||
return commands.value.some(cmd => cmd.has_conflict && cmd.reserved);
|
||||
});
|
||||
|
||||
/**
|
||||
* 实际是否显示系统插件(如果有系统插件冲突则强制显示)
|
||||
*/
|
||||
const effectiveShowSystemPlugins = computed(() => {
|
||||
return showSystemPlugins.value || hasSystemPluginConflict.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取可用的插件列表(用于过滤下拉框)
|
||||
*/
|
||||
const availablePlugins = computed(() => {
|
||||
const plugins = new Set(
|
||||
commands.value
|
||||
.filter(cmd => effectiveShowSystemPlugins.value || !cmd.reserved)
|
||||
.map(cmd => cmd.plugin)
|
||||
);
|
||||
return Array.from(plugins).sort();
|
||||
});
|
||||
|
||||
/**
|
||||
* 检查指令是否匹配过滤条件
|
||||
*/
|
||||
const matchesFilters = (cmd: CommandItem, query: string): boolean => {
|
||||
// 系统插件过滤(除非显示系统插件)
|
||||
if (!effectiveShowSystemPlugins.value && cmd.reserved) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 搜索过滤
|
||||
if (query) {
|
||||
const matchesSearch =
|
||||
cmd.effective_command?.toLowerCase().includes(query) ||
|
||||
cmd.description?.toLowerCase().includes(query) ||
|
||||
cmd.plugin?.toLowerCase().includes(query);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// 插件过滤
|
||||
if (pluginFilter.value !== 'all' && cmd.plugin !== pluginFilter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 权限过滤
|
||||
if (permissionFilter.value !== 'all') {
|
||||
if (permissionFilter.value === 'everyone') {
|
||||
if (cmd.permission !== 'everyone' && cmd.permission !== 'member') return false;
|
||||
} else if (cmd.permission !== permissionFilter.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value !== 'all') {
|
||||
if (statusFilter.value === 'enabled' && !cmd.enabled) return false;
|
||||
if (statusFilter.value === 'disabled' && cmd.enabled) return false;
|
||||
if (statusFilter.value === 'conflict' && !cmd.has_conflict) return false;
|
||||
}
|
||||
|
||||
// 类型过滤
|
||||
if (typeFilter.value !== 'all') {
|
||||
if (typeFilter.value === 'group' && cmd.type !== 'group') return false;
|
||||
if (typeFilter.value === 'command' && cmd.type !== 'command') return false;
|
||||
if (typeFilter.value === 'sub_command' && cmd.type !== 'sub_command') return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 过滤后的指令列表(支持层级结构)
|
||||
*/
|
||||
const filteredCommands = computed(() => {
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
const conflictCmds: CommandItem[] = [];
|
||||
const normalCmds: CommandItem[] = [];
|
||||
|
||||
for (const cmd of commands.value) {
|
||||
// 对于指令组,检查组本身或子指令是否匹配
|
||||
if (cmd.is_group) {
|
||||
const groupMatches = matchesFilters(cmd, query);
|
||||
const matchingSubCmds = (cmd.sub_commands || []).filter(sub => matchesFilters(sub, query));
|
||||
|
||||
// 如果组匹配或有匹配的子指令,则包含它
|
||||
if (groupMatches || matchingSubCmds.length > 0) {
|
||||
if (cmd.has_conflict) {
|
||||
conflictCmds.push(cmd);
|
||||
} else {
|
||||
normalCmds.push(cmd);
|
||||
}
|
||||
|
||||
// 如果组已展开,添加匹配的子指令
|
||||
if (expandedGroups.value.has(cmd.handler_full_name)) {
|
||||
const subsToShow = query ? matchingSubCmds : (cmd.sub_commands || []);
|
||||
for (const sub of subsToShow) {
|
||||
if (sub.has_conflict) {
|
||||
conflictCmds.push(sub);
|
||||
} else {
|
||||
normalCmds.push(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cmd.type !== 'sub_command') {
|
||||
// 普通指令(子指令通过组处理)
|
||||
if (matchesFilters(cmd, query)) {
|
||||
if (cmd.has_conflict) {
|
||||
conflictCmds.push(cmd);
|
||||
} else {
|
||||
normalCmds.push(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按 effective_command 排序冲突指令,使其分组在一起
|
||||
conflictCmds.sort((a, b) => (a.effective_command || '').localeCompare(b.effective_command || ''));
|
||||
|
||||
return [...conflictCmds, ...normalCmds];
|
||||
});
|
||||
|
||||
/**
|
||||
* 切换指令组的展开/折叠状态
|
||||
*/
|
||||
const toggleGroupExpand = (cmd: CommandItem) => {
|
||||
if (!cmd.is_group) return;
|
||||
if (expandedGroups.value.has(cmd.handler_full_name)) {
|
||||
expandedGroups.value.delete(cmd.handler_full_name);
|
||||
} else {
|
||||
expandedGroups.value.add(cmd.handler_full_name);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查指令组是否已展开
|
||||
*/
|
||||
const isGroupExpanded = (cmd: CommandItem): boolean => {
|
||||
return expandedGroups.value.has(cmd.handler_full_name);
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
searchQuery,
|
||||
pluginFilter,
|
||||
permissionFilter,
|
||||
statusFilter,
|
||||
typeFilter,
|
||||
showSystemPlugins,
|
||||
expandedGroups,
|
||||
|
||||
// 计算属性
|
||||
hasSystemPluginConflict,
|
||||
effectiveShowSystemPlugins,
|
||||
availablePlugins,
|
||||
filteredCommands,
|
||||
|
||||
// 方法
|
||||
matchesFilters,
|
||||
toggleGroupExpand,
|
||||
isGroupExpanded
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 指令数据管理 Composable
|
||||
*/
|
||||
import { ref, reactive } from 'vue';
|
||||
import axios from 'axios';
|
||||
import type { CommandItem, CommandSummary, SnackbarState, ToolItem } from '../types';
|
||||
|
||||
export function useComponentData() {
|
||||
const loading = ref(false);
|
||||
const commands = ref<CommandItem[]>([]);
|
||||
const tools = ref<ToolItem[]>([]);
|
||||
const toolsLoading = ref(false);
|
||||
const summary = reactive<CommandSummary>({
|
||||
disabled: 0,
|
||||
conflicts: 0
|
||||
});
|
||||
|
||||
const snackbar = reactive<SnackbarState>({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
});
|
||||
|
||||
/**
|
||||
* 显示 Toast 消息
|
||||
*/
|
||||
const toast = (message: string, color: string = 'success') => {
|
||||
snackbar.message = message;
|
||||
snackbar.color = color;
|
||||
snackbar.show = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指令列表
|
||||
*/
|
||||
const fetchCommands = async (errorMessage: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await axios.get('/api/commands');
|
||||
if (res.data.status === 'ok') {
|
||||
commands.value = res.data.data.items || [];
|
||||
const s = res.data.data.summary || {};
|
||||
summary.disabled = s.disabled || 0;
|
||||
summary.conflicts = s.conflicts || 0;
|
||||
} else {
|
||||
toast(res.data.message || errorMessage, 'error');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast(err?.message || errorMessage, 'error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTools = async (errorMessage: string) => {
|
||||
toolsLoading.value = true;
|
||||
try {
|
||||
const res = await axios.get('/api/tools/list');
|
||||
if (res.data.status === 'ok') {
|
||||
tools.value = res.data.data || [];
|
||||
} else {
|
||||
toast(res.data.message || errorMessage, 'error');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast(err?.message || errorMessage, 'error');
|
||||
} finally {
|
||||
toolsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
commands,
|
||||
tools,
|
||||
toolsLoading,
|
||||
summary,
|
||||
snackbar,
|
||||
toast,
|
||||
fetchCommands,
|
||||
fetchTools
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 组件管理页面 - 主入口
|
||||
*
|
||||
* 模块化结构:
|
||||
* - types.ts: 类型定义
|
||||
* - composables/useComponentData.ts: 数据获取和状态管理
|
||||
* - composables/useCommandFilters.ts: 过滤逻辑
|
||||
* - composables/useCommandActions.ts: 操作方法
|
||||
* - components/CommandFilters.vue: 过滤器组件
|
||||
* - components/CommandTable.vue: 表格组件
|
||||
* - components/RenameDialog.vue: 重命名对话框
|
||||
* - components/DetailsDialog.vue: 详情对话框
|
||||
*/
|
||||
import { computed, onActivated, onMounted, ref, watch} from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
// Composables
|
||||
import { useComponentData } from './composables/useComponentData';
|
||||
import { useCommandFilters } from './composables/useCommandFilters';
|
||||
import { useCommandActions } from './composables/useCommandActions';
|
||||
|
||||
// Components
|
||||
import CommandFilters from './components/CommandFilters.vue';
|
||||
import CommandTable from './components/CommandTable.vue';
|
||||
import ToolTable from './components/ToolTable.vue';
|
||||
import RenameDialog from './components/RenameDialog.vue';
|
||||
import DetailsDialog from './components/DetailsDialog.vue';
|
||||
|
||||
// Types
|
||||
import type { CommandItem, ToolItem } from './types';
|
||||
|
||||
defineOptions({ name: 'ComponentPanel' });
|
||||
const props = withDefaults(defineProps<{ active?: boolean }>(), {
|
||||
active: true
|
||||
});
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
const { tm: tmTool } = useModuleI18n('features/tooluse');
|
||||
|
||||
const viewMode = ref<'commands' | 'tools'>('commands');
|
||||
const toolSearch = ref('');
|
||||
|
||||
// 数据管理
|
||||
const {
|
||||
loading,
|
||||
commands,
|
||||
tools,
|
||||
toolsLoading,
|
||||
summary,
|
||||
snackbar,
|
||||
toast,
|
||||
fetchCommands,
|
||||
fetchTools
|
||||
} = useComponentData();
|
||||
|
||||
// 过滤逻辑
|
||||
const {
|
||||
searchQuery,
|
||||
pluginFilter,
|
||||
permissionFilter,
|
||||
statusFilter,
|
||||
typeFilter,
|
||||
showSystemPlugins,
|
||||
expandedGroups,
|
||||
hasSystemPluginConflict,
|
||||
effectiveShowSystemPlugins,
|
||||
availablePlugins,
|
||||
filteredCommands,
|
||||
toggleGroupExpand
|
||||
} = useCommandFilters(commands);
|
||||
|
||||
// 操作方法
|
||||
const {
|
||||
renameDialog,
|
||||
detailsDialog,
|
||||
toggleCommand,
|
||||
openRenameDialog,
|
||||
confirmRename,
|
||||
openDetailsDialog
|
||||
} = useCommandActions(toast, () => fetchCommands(tm('messages.loadFailed')));
|
||||
|
||||
const filteredTools = computed(() => {
|
||||
const query = toolSearch.value.trim().toLowerCase();
|
||||
if (!query) return tools.value;
|
||||
return tools.value.filter(tool =>
|
||||
tool.name?.toLowerCase().includes(query) ||
|
||||
tool.description?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// 处理切换指令状态
|
||||
const handleToggleCommand = async (cmd: CommandItem) => {
|
||||
await toggleCommand(cmd, tm('messages.toggleSuccess'), tm('messages.toggleFailed'));
|
||||
};
|
||||
|
||||
const handleToggleTool = async (tool: ToolItem) => {
|
||||
const previous = tool.active;
|
||||
tool.active = !tool.active;
|
||||
try {
|
||||
const res = await axios.post('/api/tools/toggle-tool', {
|
||||
name: tool.name,
|
||||
activate: tool.active
|
||||
});
|
||||
if (res.data.status === 'ok') {
|
||||
toast(res.data.message || tmTool('messages.toggleToolSuccess'));
|
||||
} else {
|
||||
tool.active = previous;
|
||||
toast(res.data.message || tmTool('messages.toggleToolError', { error: '' }), 'error');
|
||||
}
|
||||
} catch (error: any) {
|
||||
tool.active = previous;
|
||||
toast(error?.response?.data?.message || error?.message || tmTool('messages.toggleToolError', { error: '' }), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理确认重命名
|
||||
const handleConfirmRename = async () => {
|
||||
await confirmRename(tm('messages.renameSuccess'), tm('messages.renameFailed'));
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
fetchCommands(tm('messages.loadFailed')),
|
||||
fetchTools(tmTool('messages.getToolsError', { error: '' }))
|
||||
]);
|
||||
});
|
||||
|
||||
watch(() => props.active, async (isActive) => {
|
||||
if (!isActive) return;
|
||||
if (viewMode.value === 'commands') {
|
||||
await fetchCommands(tm('messages.loadFailed'));
|
||||
} else {
|
||||
await fetchTools(tmTool('messages.getToolsError', { error: '' }));
|
||||
}
|
||||
});
|
||||
|
||||
watch(viewMode, async (mode) => {
|
||||
if (mode === 'commands') {
|
||||
await fetchCommands(tm('messages.loadFailed'));
|
||||
} else {
|
||||
await fetchTools(tmTool('messages.getToolsError', { error: '' }));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card variant="flat" style="background-color: transparent">
|
||||
<v-card-text style="padding: 20px 12px; padding-top: 0px;">
|
||||
<div class="d-flex justify-space-between align-center mb-6 flex-wrap ga-3">
|
||||
<v-btn-toggle v-model="viewMode" color="primary" variant="outlined" density="comfortable" mandatory>
|
||||
<v-btn value="commands">
|
||||
<v-icon size="18" class="mr-1">mdi-console-line</v-icon>
|
||||
{{ tm('type.command') }}
|
||||
</v-btn>
|
||||
<v-btn value="tools">
|
||||
<v-icon size="18" class="mr-1">mdi-function-variant</v-icon>
|
||||
{{ tmTool('functionTools.title') }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-progress-linear
|
||||
v-if="viewMode === 'commands' && loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
style="max-width: 220px; flex: 1;"
|
||||
/>
|
||||
<v-progress-linear
|
||||
v-else-if="viewMode === 'tools' && toolsLoading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
style="max-width: 220px; flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'commands'">
|
||||
<CommandFilters
|
||||
:plugin-filter="pluginFilter"
|
||||
@update:plugin-filter="pluginFilter = $event"
|
||||
:type-filter="typeFilter"
|
||||
@update:type-filter="typeFilter = $event"
|
||||
:permission-filter="permissionFilter"
|
||||
@update:permission-filter="permissionFilter = $event"
|
||||
:status-filter="statusFilter"
|
||||
@update:status-filter="statusFilter = $event"
|
||||
:show-system-plugins="showSystemPlugins"
|
||||
@update:show-system-plugins="showSystemPlugins = $event"
|
||||
:search-query="searchQuery"
|
||||
@update:search-query="searchQuery = $event"
|
||||
:available-plugins="availablePlugins"
|
||||
:has-system-plugin-conflict="hasSystemPluginConflict"
|
||||
:effective-show-system-plugins="effectiveShowSystemPlugins"
|
||||
>
|
||||
<template #stats>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="primary" class="mr-1">mdi-console-line</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-primary">{{ filteredCommands.length }}</span>
|
||||
</div>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="error" class="mr-1">mdi-close-circle-outline</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.disabled') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-error">{{ summary.disabled }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</CommandFilters>
|
||||
|
||||
<v-alert
|
||||
v-if="summary.conflicts > 0"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
prominent
|
||||
border="start"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="28">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
<v-alert-title class="text-subtitle-1 font-weight-bold">
|
||||
{{ tm('conflictAlert.title') }}
|
||||
</v-alert-title>
|
||||
<div class="text-body-2 mt-1">
|
||||
{{ tm('conflictAlert.description', { count: summary.conflicts }) }}
|
||||
</div>
|
||||
<div class="text-body-2 mt-2">
|
||||
<v-icon size="16" class="mr-1">mdi-lightbulb-outline</v-icon>
|
||||
{{ tm('conflictAlert.hint') }}
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<CommandTable
|
||||
:items="filteredCommands"
|
||||
:expanded-groups="expandedGroups"
|
||||
:loading="loading"
|
||||
@toggle-expand="toggleGroupExpand"
|
||||
@toggle-command="handleToggleCommand"
|
||||
@rename="openRenameDialog"
|
||||
@view-details="openDetailsDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="d-flex flex-wrap align-center ga-3 mb-4">
|
||||
<div style="min-width: 240px; max-width: 380px; flex: 1;">
|
||||
<v-text-field
|
||||
v-model="toolSearch"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:label="tmTool('functionTools.search')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-2">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="primary" class="mr-1">mdi-function-variant</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-primary">{{ filteredTools.length }}</span>
|
||||
</div>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="success" class="mr-1">mdi-check-circle-outline</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('status.enabled') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-success">{{ filteredTools.filter(t => t.active).length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToolTable
|
||||
:items="filteredTools"
|
||||
:loading="toolsLoading"
|
||||
@toggle-tool="handleToggleTool"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 重命名对话框 -->
|
||||
<RenameDialog
|
||||
:show="renameDialog.show"
|
||||
@update:show="renameDialog.show = $event"
|
||||
:new-name="renameDialog.newName"
|
||||
@update:new-name="renameDialog.newName = $event"
|
||||
:command="renameDialog.command"
|
||||
:loading="renameDialog.loading"
|
||||
@confirm="handleConfirmRename"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<DetailsDialog
|
||||
:show="detailsDialog.show"
|
||||
@update:show="detailsDialog.show = $event"
|
||||
:command="detailsDialog.command"
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar :timeout="2000" elevation="24" :color="snackbar.color" v-model="snackbar.show">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 指令管理模块 - 类型定义
|
||||
*/
|
||||
|
||||
/** 指令项接口 */
|
||||
export interface CommandItem {
|
||||
handler_full_name: string;
|
||||
handler_name: string;
|
||||
plugin: string;
|
||||
plugin_display_name: string | null;
|
||||
module_path: string;
|
||||
description: string;
|
||||
type: CommandType;
|
||||
parent_signature: string;
|
||||
parent_group_handler: string;
|
||||
original_command: string;
|
||||
current_fragment: string;
|
||||
effective_command: string;
|
||||
aliases: string[];
|
||||
permission: PermissionType;
|
||||
enabled: boolean;
|
||||
is_group: boolean;
|
||||
has_conflict: boolean;
|
||||
reserved: boolean;
|
||||
sub_commands: CommandItem[];
|
||||
}
|
||||
|
||||
/** 指令类型 */
|
||||
export type CommandType = 'command' | 'group' | 'sub_command';
|
||||
|
||||
/** 权限类型 */
|
||||
export type PermissionType = 'admin' | 'everyone' | 'member';
|
||||
|
||||
/** 指令摘要统计 */
|
||||
export interface CommandSummary {
|
||||
disabled: number;
|
||||
conflicts: number;
|
||||
}
|
||||
|
||||
/** 过滤器状态 */
|
||||
export interface FilterState {
|
||||
searchQuery: string;
|
||||
pluginFilter: string;
|
||||
permissionFilter: string;
|
||||
statusFilter: string;
|
||||
typeFilter: string;
|
||||
showSystemPlugins: boolean;
|
||||
}
|
||||
|
||||
/** 重命名对话框状态 */
|
||||
export interface RenameDialogState {
|
||||
show: boolean;
|
||||
command: CommandItem | null;
|
||||
newName: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/** 详情对话框状态 */
|
||||
export interface DetailsDialogState {
|
||||
show: boolean;
|
||||
command: CommandItem | null;
|
||||
}
|
||||
|
||||
/** Toast 消息状态 */
|
||||
export interface SnackbarState {
|
||||
show: boolean;
|
||||
message: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/** 类型信息展示 */
|
||||
export interface TypeInfo {
|
||||
text: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/** 状态信息展示 */
|
||||
export interface StatusInfo {
|
||||
text: string;
|
||||
color: string;
|
||||
variant: 'flat' | 'outlined' | 'text' | 'elevated' | 'tonal' | 'plain';
|
||||
}
|
||||
|
||||
/** MCP/函数工具参数定义 */
|
||||
export interface ToolParameter {
|
||||
type?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** MCP/函数工具对象 */
|
||||
export interface ToolItem {
|
||||
name: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
parameters?: {
|
||||
properties?: Record<string, ToolParameter>;
|
||||
};
|
||||
origin?: string;
|
||||
origin_name?: string;
|
||||
}
|
||||
|
||||
@@ -304,32 +304,16 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Numeric input with optional slider -->
|
||||
<div
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey].items[key]?.type === 'int' || metadata[metadataKey].items[key]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
class="d-flex align-center gap-3"
|
||||
>
|
||||
<v-slider
|
||||
v-if="metadata[metadataKey].items[key]?.slider"
|
||||
v-model.number="iterable[key]"
|
||||
:min="metadata[metadataKey].items[key]?.slider?.min ?? 0"
|
||||
:max="metadata[metadataKey].items[key]?.slider?.max ?? 100"
|
||||
:step="metadata[metadataKey].items[key]?.slider?.step ?? 1"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-slider>
|
||||
<v-text-field
|
||||
v-model.number="iterable[key]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
style="max-width: 140px;"
|
||||
></v-text-field>
|
||||
</div>
|
||||
v-model="iterable[key]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
@@ -429,32 +413,16 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Numeric input with optional slider -->
|
||||
<div
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey]?.type === 'int' || metadata[metadataKey]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
class="d-flex align-center gap-3"
|
||||
>
|
||||
<v-slider
|
||||
v-if="metadata[metadataKey]?.slider"
|
||||
v-model.number="iterable[metadataKey]"
|
||||
:min="metadata[metadataKey]?.slider?.min ?? 0"
|
||||
:max="metadata[metadataKey]?.slider?.max ?? 100"
|
||||
:step="metadata[metadataKey]?.slider?.step ?? 1"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-slider>
|
||||
<v-text-field
|
||||
v-model.number="iterable[metadataKey]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
style="max-width: 140px;"
|
||||
></v-text-field>
|
||||
</div>
|
||||
v-model="iterable[metadataKey]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
|
||||
@@ -245,29 +245,10 @@ function getSpecialSubtype(value) {
|
||||
<v-text-field v-else-if="itemMeta?.type === 'string'" v-model="createSelectorModel(itemKey).value"
|
||||
density="compact" variant="outlined" class="config-field" hide-details></v-text-field>
|
||||
|
||||
<!-- Numeric input with optional slider for JSON selector -->
|
||||
<div v-else-if="itemMeta?.type === 'int' || itemMeta?.type === 'float'" class="d-flex align-center gap-3">
|
||||
<v-slider
|
||||
v-if="itemMeta?.slider"
|
||||
v-model.number="createSelectorModel(itemKey).value"
|
||||
:min="itemMeta?.slider?.min ?? 0"
|
||||
:max="itemMeta?.slider?.max ?? 100"
|
||||
:step="itemMeta?.slider?.step ?? 1"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
style="flex: 3"
|
||||
></v-slider>
|
||||
<v-text-field
|
||||
v-model.number="createSelectorModel(itemKey).value"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
style="flex: 2"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</div>
|
||||
<!-- Numeric input for JSON selector -->
|
||||
<v-text-field v-else-if="itemMeta?.type === 'int' || itemMeta?.type === 'float'"
|
||||
v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined" class="config-field"
|
||||
type="number" hide-details></v-text-field>
|
||||
|
||||
<!-- Text area for JSON selector -->
|
||||
<v-textarea v-else-if="itemMeta?.type === 'text'" v-model="createSelectorModel(itemKey).value"
|
||||
|
||||
@@ -115,7 +115,7 @@ const _show = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="_show" width="800">
|
||||
<v-dialog v-model="_show" width="800" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h5">{{ t('core.common.readme.title') }}</span>
|
||||
|
||||
@@ -121,7 +121,8 @@ import sidebarItems from '@/layouts/full/vertical-sidebar/sidebarItem';
|
||||
import {
|
||||
getSidebarCustomization,
|
||||
setSidebarCustomization,
|
||||
clearSidebarCustomization
|
||||
clearSidebarCustomization,
|
||||
resolveSidebarItems
|
||||
} from '@/utils/sidebarCustomization';
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -133,35 +134,12 @@ const draggedItem = ref(null);
|
||||
|
||||
function initializeItems() {
|
||||
const customization = getSidebarCustomization();
|
||||
|
||||
if (customization) {
|
||||
// Load from customization
|
||||
const allItemsMap = new Map();
|
||||
|
||||
sidebarItems.forEach(item => {
|
||||
if (item.children) {
|
||||
item.children.forEach(child => {
|
||||
allItemsMap.set(child.title, child);
|
||||
});
|
||||
} else {
|
||||
allItemsMap.set(item.title, item);
|
||||
}
|
||||
});
|
||||
|
||||
mainItems.value = customization.mainItems
|
||||
.map(title => allItemsMap.get(title))
|
||||
.filter(item => item);
|
||||
|
||||
moreItems.value = customization.moreItems
|
||||
.map(title => allItemsMap.get(title))
|
||||
.filter(item => item);
|
||||
} else {
|
||||
// Load default structure
|
||||
mainItems.value = sidebarItems.filter(item => !item.children);
|
||||
|
||||
const moreGroup = sidebarItems.find(item => item.title === 'core.navigation.groups.more');
|
||||
moreItems.value = moreGroup ? [...moreGroup.children] : [];
|
||||
}
|
||||
const { mainItems: resolvedMain, moreItems: resolvedMore } = resolveSidebarItems(
|
||||
sidebarItems,
|
||||
customization
|
||||
);
|
||||
mainItems.value = resolvedMain;
|
||||
moreItems.value = resolvedMore;
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
"submit": "Submit",
|
||||
"reset": "Reset",
|
||||
"clear": "Clear",
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"close": "Close"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"dashboard": "Dashboard",
|
||||
"platforms": "Platforms",
|
||||
"providers": "Providers",
|
||||
"commands": "Commands",
|
||||
"persona": "Persona",
|
||||
"toolUse": "MCP Tools",
|
||||
"config": "Config",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"title": "Command Management",
|
||||
"summary": {
|
||||
"total": "Displayed commands",
|
||||
"disabled": "Disabled",
|
||||
"conflicts": "Conflicts"
|
||||
},
|
||||
"conflictAlert": {
|
||||
"title": "Command Conflicts Detected",
|
||||
"description": "There are {count} conflicting commands. Conflicting commands will trigger multiple plugins simultaneously, which may cause unexpected behavior.",
|
||||
"hint": "Click the \"Rename\" button to rename conflicting commands and resolve conflicts."
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"command": "Command",
|
||||
"type": "Type",
|
||||
"plugin": "Plugin",
|
||||
"description": "Description",
|
||||
"permission": "Permission",
|
||||
"status": "Status",
|
||||
"actions": "Actions"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"command": "Command",
|
||||
"group": "Group",
|
||||
"subCommand": "Sub-command"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"conflict": "Conflict"
|
||||
},
|
||||
"permission": {
|
||||
"everyone": "Everyone",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"tooltips": {
|
||||
"enable": "Enable command",
|
||||
"disable": "Disable command",
|
||||
"rename": "Rename command",
|
||||
"viewDetails": "View details"
|
||||
},
|
||||
"dialogs": {
|
||||
"rename": {
|
||||
"title": "Rename Command",
|
||||
"newName": "New command name",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm"
|
||||
},
|
||||
"details": {
|
||||
"title": "Command Details",
|
||||
"type": "Command Type",
|
||||
"handler": "Handler",
|
||||
"module": "Module Path",
|
||||
"originalCommand": "Original Command",
|
||||
"effectiveCommand": "Effective Command",
|
||||
"parentGroup": "Parent Group",
|
||||
"subCommands": "Sub-commands",
|
||||
"aliases": "Aliases",
|
||||
"permission": "Permission",
|
||||
"conflictStatus": "Conflict Status"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"toggleSuccess": "Command status updated",
|
||||
"toggleFailed": "Failed to update command status",
|
||||
"renameSuccess": "Command renamed",
|
||||
"renameFailed": "Rename failed",
|
||||
"loadFailed": "Failed to load commands"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search commands..."
|
||||
},
|
||||
"empty": {
|
||||
"noCommands": "No Commands",
|
||||
"noCommandsDesc": "No commands found"
|
||||
},
|
||||
"filters": {
|
||||
"all": "All",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"conflict": "Conflict",
|
||||
"byPlugin": "Filter by plugin",
|
||||
"byType": "Filter by type",
|
||||
"byPermission": "Filter by permission",
|
||||
"byStatus": "Filter by status",
|
||||
"showSystemPlugins": "Show system plugins commands",
|
||||
"systemPluginConflictHint": "System plugin conflicts detected. Resolve conflicts to hide."
|
||||
}
|
||||
}
|
||||
@@ -57,9 +57,6 @@
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "Default Text-to-Speech Model"
|
||||
},
|
||||
"trigger_probability": {
|
||||
"description": "TTS Trigger Probability"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"title": "Extension Management",
|
||||
"subtitle": "Manage and configure system extensions",
|
||||
"tabs": {
|
||||
"installed": "Installed",
|
||||
"installedPlugins": "Installed Plugins",
|
||||
"installedMcpServers": "Installed MCP Servers",
|
||||
"handlersOperation": "Manage Components",
|
||||
"market": "Extension Market"
|
||||
},
|
||||
"search": {
|
||||
@@ -197,5 +199,12 @@
|
||||
"errors": {
|
||||
"confirmNotRegistered": "$confirm not properly registered"
|
||||
}
|
||||
},
|
||||
"conflicts": {
|
||||
"title": "Command Conflicts Detected",
|
||||
"message": "This will cause some commands to work abnormally. It is recommended to go to the [Command Management] panel to handle it.",
|
||||
"pairs": "command conflicts",
|
||||
"goToManage": "Go to Manage",
|
||||
"later": "Later"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@
|
||||
"paramName": "Parameter Name",
|
||||
"type": "Type",
|
||||
"description": "Description",
|
||||
"required": "Required"
|
||||
"required": "Required",
|
||||
"origin": "Origin",
|
||||
"originName": "Origin Name",
|
||||
"actions": "Actions"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
"submit": "提交",
|
||||
"reset": "重置",
|
||||
"clear": "清空",
|
||||
"save": "保存"
|
||||
"save": "保存",
|
||||
"close": "关闭"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"dashboard": "数据统计",
|
||||
"platforms": "机器人",
|
||||
"providers": "模型提供商",
|
||||
"commands": "指令管理",
|
||||
"persona": "人格设定",
|
||||
"toolUse": "MCP",
|
||||
"extension": "插件",
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"title": "指令管理",
|
||||
"summary": {
|
||||
"total": "展示的指令数",
|
||||
"disabled": "已禁用",
|
||||
"conflicts": "有冲突"
|
||||
},
|
||||
"conflictAlert": {
|
||||
"title": "检测到指令冲突",
|
||||
"description": "当前有 {count} 对指令存在冲突,冲突的指令会同时触发多个插件响应,可能导致意外行为。",
|
||||
"hint": "请点击「重命名」按钮修改冲突指令的名称以解决冲突。"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"command": "指令",
|
||||
"type": "类型",
|
||||
"plugin": "所属插件",
|
||||
"description": "描述",
|
||||
"permission": "权限",
|
||||
"status": "状态",
|
||||
"actions": "操作"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"command": "指令",
|
||||
"group": "指令组",
|
||||
"subCommand": "子指令"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"conflict": "有冲突"
|
||||
},
|
||||
"permission": {
|
||||
"everyone": "所有人",
|
||||
"admin": "管理员"
|
||||
},
|
||||
"tooltips": {
|
||||
"enable": "启用指令",
|
||||
"disable": "禁用指令",
|
||||
"rename": "重命名指令",
|
||||
"viewDetails": "查看详情"
|
||||
},
|
||||
"dialogs": {
|
||||
"rename": {
|
||||
"title": "重命名指令",
|
||||
"newName": "新指令名",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认"
|
||||
},
|
||||
"details": {
|
||||
"title": "指令详情",
|
||||
"type": "指令类型",
|
||||
"handler": "处理函数",
|
||||
"module": "模块路径",
|
||||
"originalCommand": "原始指令",
|
||||
"effectiveCommand": "生效指令",
|
||||
"parentGroup": "所属指令组",
|
||||
"subCommands": "子指令列表",
|
||||
"aliases": "别名",
|
||||
"permission": "权限要求",
|
||||
"conflictStatus": "冲突状态"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"toggleSuccess": "指令状态已更新",
|
||||
"toggleFailed": "更新指令状态失败",
|
||||
"renameSuccess": "指令已重命名",
|
||||
"renameFailed": "重命名失败",
|
||||
"loadFailed": "加载指令列表失败"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索指令..."
|
||||
},
|
||||
"empty": {
|
||||
"noCommands": "暂无指令",
|
||||
"noCommandsDesc": "当前筛选条件下没有找到任何指令"
|
||||
},
|
||||
"filters": {
|
||||
"all": "全部",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"conflict": "有冲突",
|
||||
"byPlugin": "按插件筛选",
|
||||
"byType": "按类型筛选",
|
||||
"byPermission": "按权限筛选",
|
||||
"byStatus": "按状态筛选",
|
||||
"showSystemPlugins": "显示系统插件指令",
|
||||
"systemPluginConflictHint": "存在涉及系统插件的冲突,需解决冲突后才能隐藏"
|
||||
}
|
||||
}
|
||||
@@ -62,9 +62,6 @@
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "默认文本转语音模型"
|
||||
},
|
||||
"trigger_probability": {
|
||||
"description": "TTS 触发概率"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"title": "插件管理",
|
||||
"subtitle": "管理和配置系统插件",
|
||||
"tabs": {
|
||||
"installed": "已安装",
|
||||
"installedPlugins": "已安装的插件",
|
||||
"installedMcpServers": "已安装的 MCP 服务器",
|
||||
"handlersOperation": "管理组件",
|
||||
"market": "插件市场"
|
||||
},
|
||||
"search": {
|
||||
@@ -197,5 +199,12 @@
|
||||
"errors": {
|
||||
"confirmNotRegistered": "$confirm 未正确注册"
|
||||
}
|
||||
},
|
||||
"conflicts": {
|
||||
"title": "检测到指令冲突",
|
||||
"message": "这会导致部分指令工作异常,建议前往【指令管理】面板进行处理。",
|
||||
"pairs": "对指令冲突",
|
||||
"goToManage": "前往处理",
|
||||
"later": "稍后处理"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@
|
||||
"paramName": "参数名",
|
||||
"type": "类型",
|
||||
"description": "描述",
|
||||
"required": "必填"
|
||||
"required": "必填",
|
||||
"origin": "来源",
|
||||
"originName": "来源名称",
|
||||
"actions": "操作"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
|
||||
@@ -32,6 +32,7 @@ import zhCNKnowledgeBaseDetail from './locales/zh-CN/features/knowledge-base/det
|
||||
import zhCNKnowledgeBaseDocument from './locales/zh-CN/features/knowledge-base/document.json';
|
||||
import zhCNPersona from './locales/zh-CN/features/persona.json';
|
||||
import zhCNMigration from './locales/zh-CN/features/migration.json';
|
||||
import zhCNCommand from './locales/zh-CN/features/command.json';
|
||||
|
||||
import zhCNErrors from './locales/zh-CN/messages/errors.json';
|
||||
import zhCNSuccess from './locales/zh-CN/messages/success.json';
|
||||
@@ -68,6 +69,7 @@ import enUSKnowledgeBaseDetail from './locales/en-US/features/knowledge-base/det
|
||||
import enUSKnowledgeBaseDocument from './locales/en-US/features/knowledge-base/document.json';
|
||||
import enUSPersona from './locales/en-US/features/persona.json';
|
||||
import enUSMigration from './locales/en-US/features/migration.json';
|
||||
import enUSCommand from './locales/en-US/features/command.json';
|
||||
|
||||
import enUSErrors from './locales/en-US/messages/errors.json';
|
||||
import enUSSuccess from './locales/en-US/messages/success.json';
|
||||
@@ -111,7 +113,8 @@ export const translations = {
|
||||
document: zhCNKnowledgeBaseDocument
|
||||
},
|
||||
persona: zhCNPersona,
|
||||
migration: zhCNMigration
|
||||
migration: zhCNMigration,
|
||||
command: zhCNCommand
|
||||
},
|
||||
messages: {
|
||||
errors: zhCNErrors,
|
||||
@@ -155,7 +158,8 @@ export const translations = {
|
||||
document: enUSKnowledgeBaseDocument
|
||||
},
|
||||
persona: enUSPersona,
|
||||
migration: enUSMigration
|
||||
migration: enUSMigration,
|
||||
command: enUSCommand
|
||||
},
|
||||
messages: {
|
||||
errors: enUSErrors,
|
||||
|
||||
@@ -33,11 +33,6 @@ const sidebarItem: menu[] = [
|
||||
icon: 'mdi-cog',
|
||||
to: '/config',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.toolUse',
|
||||
icon: 'mdi-function-variant',
|
||||
to: '/tool-use'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extension',
|
||||
icon: 'mdi-puzzle',
|
||||
|
||||
@@ -31,11 +31,6 @@ const MainRoutes = {
|
||||
path: '/providers',
|
||||
component: () => import('@/views/ProviderPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'ToolUsePage',
|
||||
path: '/tool-use',
|
||||
component: () => import('@/views/ToolUsePage.vue')
|
||||
},
|
||||
{
|
||||
name: 'Configs',
|
||||
path: '/config',
|
||||
|
||||
@@ -41,59 +41,97 @@ export function clearSidebarCustomization() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply customization to sidebar items
|
||||
* @param {Array} defaultItems - Default sidebar items array
|
||||
* @returns {Array} Customized sidebar items array (new array, doesn't mutate input)
|
||||
* 解析侧边栏默认项与用户定制,返回主区/更多区及可选的合并结果
|
||||
* @param {Array} defaultItems - 默认侧边栏结构
|
||||
* @param {Object|null} customization - 用户定制(mainItems/moreItems)
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.cloneItems=false] - 是否克隆条目以避免外部引用被修改
|
||||
* @param {boolean} [options.assembleMoreGroup=false] - 是否组装带更多分组的整体数组
|
||||
* @returns {{ mainItems: Array, moreItems: Array, merged?: Array }}
|
||||
*/
|
||||
export function resolveSidebarItems(defaultItems, customization, options = {}) {
|
||||
const { cloneItems = false, assembleMoreGroup = false } = options;
|
||||
|
||||
const all = new Map();
|
||||
const defaultMain = [];
|
||||
const defaultMore = [];
|
||||
|
||||
// 收集所有条目,按 title 建索引
|
||||
defaultItems.forEach(item => {
|
||||
if (item.children) {
|
||||
item.children.forEach(child => {
|
||||
all.set(child.title, cloneItems ? { ...child } : child);
|
||||
defaultMore.push(child.title);
|
||||
});
|
||||
} else {
|
||||
all.set(item.title, cloneItems ? { ...item } : item);
|
||||
defaultMain.push(item.title);
|
||||
}
|
||||
});
|
||||
|
||||
const hasCustomization = Boolean(customization);
|
||||
const mainKeys = hasCustomization ? customization.mainItems || [] : defaultMain;
|
||||
const moreKeys = hasCustomization ? customization.moreItems || [] : defaultMore;
|
||||
const used = hasCustomization ? new Set([...mainKeys, ...moreKeys]) : new Set(defaultMain.concat(defaultMore));
|
||||
|
||||
const mainItems = mainKeys
|
||||
.map(title => all.get(title))
|
||||
.filter(Boolean);
|
||||
|
||||
if (hasCustomization) {
|
||||
// 补充新增默认主区项
|
||||
defaultMain.forEach(title => {
|
||||
if (!used.has(title)) {
|
||||
const item = all.get(title);
|
||||
if (item) mainItems.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const moreItems = moreKeys
|
||||
.map(title => all.get(title))
|
||||
.filter(Boolean);
|
||||
|
||||
if (hasCustomization) {
|
||||
// 补充新增默认更多区项
|
||||
defaultMore.forEach(title => {
|
||||
if (!used.has(title)) {
|
||||
const item = all.get(title);
|
||||
if (item) moreItems.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let merged;
|
||||
if (assembleMoreGroup) {
|
||||
const children = cloneItems ? moreItems.map(item => ({ ...item })) : [...moreItems];
|
||||
if (children.length > 0) {
|
||||
merged = [
|
||||
...mainItems,
|
||||
{
|
||||
title: 'core.navigation.groups.more',
|
||||
icon: 'mdi-dots-horizontal',
|
||||
children
|
||||
}
|
||||
];
|
||||
} else {
|
||||
merged = [...mainItems];
|
||||
}
|
||||
}
|
||||
|
||||
return { mainItems, moreItems, merged };
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用侧边栏定制,返回包含更多分组的完整结构
|
||||
* @param {Array} defaultItems - 默认侧边栏结构
|
||||
* @returns {Array} 自定义后的结构(新数组,不修改入参)
|
||||
*/
|
||||
export function applySidebarCustomization(defaultItems) {
|
||||
const customization = getSidebarCustomization();
|
||||
if (!customization) {
|
||||
return defaultItems;
|
||||
}
|
||||
|
||||
const { mainItems, moreItems } = customization;
|
||||
|
||||
// Create a map of all items by title for quick lookup
|
||||
// Deep clone items to avoid mutating originals
|
||||
const allItemsMap = new Map();
|
||||
defaultItems.forEach(item => {
|
||||
if (item.children) {
|
||||
// If it's the "More" group, add children to map
|
||||
item.children.forEach(child => {
|
||||
allItemsMap.set(child.title, { ...child });
|
||||
});
|
||||
} else {
|
||||
allItemsMap.set(item.title, { ...item });
|
||||
}
|
||||
const { merged } = resolveSidebarItems(defaultItems, customization, {
|
||||
cloneItems: true,
|
||||
assembleMoreGroup: true
|
||||
});
|
||||
|
||||
const customizedItems = [];
|
||||
|
||||
// Add main items in custom order
|
||||
mainItems.forEach(title => {
|
||||
const item = allItemsMap.get(title);
|
||||
if (item) {
|
||||
customizedItems.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// If there are items in moreItems, create the "More Features" group
|
||||
if (moreItems && moreItems.length > 0) {
|
||||
const moreGroup = {
|
||||
title: 'core.navigation.groups.more',
|
||||
icon: 'mdi-dots-horizontal',
|
||||
children: []
|
||||
};
|
||||
|
||||
moreItems.forEach(title => {
|
||||
const item = allItemsMap.get(title);
|
||||
if (item) {
|
||||
moreGroup.children.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
customizedItems.push(moreGroup);
|
||||
}
|
||||
|
||||
return customizedItems;
|
||||
return merged || defaultItems;
|
||||
}
|
||||
|
||||
@@ -5,18 +5,45 @@ import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import ReadmeDialog from '@/components/shared/ReadmeDialog.vue';
|
||||
import ProxySelector from '@/components/shared/ProxySelector.vue';
|
||||
import UninstallConfirmDialog from '@/components/shared/UninstallConfirmDialog.vue';
|
||||
import McpServersSection from '@/components/extension/McpServersSection.vue';
|
||||
import ComponentPanel from '@/components/extension/componentPanel/index.vue';
|
||||
import axios from 'axios';
|
||||
import { pinyin } from 'pinyin-pro';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import defaultPluginIcon from '@/assets/images/plugin_icon.png';
|
||||
|
||||
import { ref, computed, onMounted, reactive, inject, watch } from 'vue';
|
||||
|
||||
import { ref, computed, onMounted, reactive, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const commonStore = useCommonStore();
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/extension');
|
||||
const router = useRouter();
|
||||
|
||||
// 检查指令冲突并提示
|
||||
const conflictDialog = reactive({
|
||||
show: false,
|
||||
count: 0
|
||||
});
|
||||
const checkAndPromptConflicts = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/commands');
|
||||
if (res.data.status === 'ok') {
|
||||
const conflicts = res.data.data.summary?.conflicts || 0;
|
||||
if (conflicts > 0) {
|
||||
conflictDialog.count = conflicts;
|
||||
conflictDialog.show = true;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug('Failed to check command conflicts:', err);
|
||||
}
|
||||
};
|
||||
const handleConflictConfirm = () => {
|
||||
activeTab.value = 'commands';
|
||||
};
|
||||
|
||||
const fileInput = ref(null);
|
||||
const activeTab = ref('installed');
|
||||
const extension_data = reactive({
|
||||
@@ -448,7 +475,9 @@ const pluginOn = async (extension) => {
|
||||
return;
|
||||
}
|
||||
toast(res.data.message, "success");
|
||||
getExtensions();
|
||||
await getExtensions();
|
||||
|
||||
await checkAndPromptConflicts();
|
||||
} catch (err) {
|
||||
toast(err, "error");
|
||||
}
|
||||
@@ -782,6 +811,8 @@ const newExtension = async () => {
|
||||
name: res.data.data.name,
|
||||
repo: res.data.data.repo || null
|
||||
});
|
||||
|
||||
await checkAndPromptConflicts();
|
||||
}).catch((err) => {
|
||||
loading_.value = false;
|
||||
onLoadingDialogResult(2, err, -1);
|
||||
@@ -808,6 +839,8 @@ const newExtension = async () => {
|
||||
name: res.data.data.name,
|
||||
repo: res.data.data.repo || null
|
||||
});
|
||||
|
||||
await checkAndPromptConflicts();
|
||||
}).catch((err) => {
|
||||
loading_.value = false;
|
||||
toast(tm('messages.installFailed') + " " + err, "error");
|
||||
@@ -900,21 +933,29 @@ watch(marketSearch, (newVal) => {
|
||||
<v-tabs v-model="activeTab" color="primary">
|
||||
<v-tab value="installed">
|
||||
<v-icon class="mr-2">mdi-puzzle</v-icon>
|
||||
{{ tm('tabs.installed') }}
|
||||
{{ tm('tabs.installedPlugins') }}
|
||||
</v-tab>
|
||||
<v-tab value="mcp">
|
||||
<v-icon class="mr-2">mdi-server-network</v-icon>
|
||||
{{ tm('tabs.installedMcpServers') }}
|
||||
</v-tab>
|
||||
<v-tab value="market">
|
||||
<v-icon class="mr-2">mdi-store</v-icon>
|
||||
{{ tm('tabs.market') }}
|
||||
</v-tab>
|
||||
<v-tab value="components">
|
||||
<v-icon class="mr-2">mdi-wrench</v-icon>
|
||||
{{ tm('tabs.handlersOperation') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- 搜索栏 - 在移动端时独占一行 -->
|
||||
<div style="flex-grow: 1; min-width: 250px; max-width: 400px; margin-left: auto; margin-top: 8px;">
|
||||
<v-text-field v-if="activeTab == 'market'" v-model="marketSearch" density="compact"
|
||||
<v-text-field v-if="activeTab === 'market'" v-model="marketSearch" density="compact"
|
||||
:label="tm('search.marketPlaceholder')" prepend-inner-icon="mdi-magnify" variant="solo-filled" flat
|
||||
hide-details single-line>
|
||||
</v-text-field>
|
||||
<v-text-field v-else v-model="pluginSearch" density="compact" :label="tm('search.placeholder')"
|
||||
<v-text-field v-else-if="activeTab === 'installed'" v-model="pluginSearch" density="compact" :label="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify" variant="solo-filled" flat hide-details single-line>
|
||||
</v-text-field>
|
||||
</div>
|
||||
@@ -1118,6 +1159,24 @@ watch(marketSearch, (newVal) => {
|
||||
</v-fade-transition>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 指令面板标签页内容 -->
|
||||
<v-tab-item v-show="activeTab === 'components'">
|
||||
<v-card class="rounded-lg" variant="flat" style="background-color: transparent;">
|
||||
<v-card-text class="pa-0">
|
||||
<ComponentPanel :active="activeTab === 'components'" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 已安装的 MCP 服务器标签页内容 -->
|
||||
<v-tab-item v-show="activeTab === 'mcp'">
|
||||
<v-card class="rounded-lg" variant="flat" style="background-color: transparent;">
|
||||
<v-card-text class="pa-0">
|
||||
<McpServersSection />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 插件市场标签页内容 -->
|
||||
<v-tab-item v-show="activeTab === 'market'">
|
||||
|
||||
@@ -1544,6 +1603,34 @@ watch(marketSearch, (newVal) => {
|
||||
<!-- 卸载插件确认对话框(列表模式用) -->
|
||||
<UninstallConfirmDialog v-model="showUninstallDialog" @confirm="handleUninstallConfirm" />
|
||||
|
||||
<!-- 指令冲突提示对话框 -->
|
||||
<v-dialog v-model="conflictDialog.show" max-width="420">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title class="d-flex align-center pa-4">
|
||||
<v-icon color="warning" class="mr-2">mdi-alert-circle</v-icon>
|
||||
{{ tm('conflicts.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text class="px-4 pb-2">
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-chip color="warning" variant="tonal" size="large" class="font-weight-bold">
|
||||
{{ conflictDialog.count }}
|
||||
</v-chip>
|
||||
<span class="ml-2 text-body-1">{{ tm('conflicts.pairs') }}</span>
|
||||
</div>
|
||||
<p class="text-body-2" style="color: rgba(var(--v-theme-on-surface), 0.7);">
|
||||
{{ tm('conflicts.message') }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4 pt-2">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="conflictDialog.show = false">{{ tm('conflicts.later') }}</v-btn>
|
||||
<v-btn color="warning" variant="flat" @click="handleConflictConfirm">
|
||||
{{ tm('conflicts.goToManage') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 危险插件确认对话框 -->
|
||||
<v-dialog v-model="dangerConfirmDialog" width="500" persistent>
|
||||
<v-card>
|
||||
@@ -1568,7 +1655,7 @@ watch(marketSearch, (newVal) => {
|
||||
|
||||
<!-- 上传插件对话框 -->
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<div class="v-card v-card--density-default rounded-lg v-card--variant-elevated">
|
||||
<div class="v-card v-theme--PurpleThemeDark v-card--density-default rounded-lg v-card--variant-elevated">
|
||||
<div class="v-card__loader">
|
||||
<v-progress-linear :indeterminate="loading_" color="primary" height="2" :active="loading_"></v-progress-linear>
|
||||
</div>
|
||||
|
||||
@@ -71,6 +71,7 @@ class AdminCommands:
|
||||
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
||||
|
||||
async def update_dashboard(self, event: AstrMessageEvent):
|
||||
"""更新管理面板"""
|
||||
await event.send(MessageChain().message("正在尝试更新管理面板..."))
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
await event.send(MessageChain().message("管理面板更新完成。"))
|
||||
|
||||
@@ -3,6 +3,7 @@ import aiohttp
|
||||
from astrbot.api import star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.star import command_management
|
||||
from astrbot.core.utils.io import get_dashboard_version
|
||||
|
||||
|
||||
@@ -21,6 +22,46 @@ class HelpCommand:
|
||||
except BaseException:
|
||||
return ""
|
||||
|
||||
async def _build_reserved_command_lines(self) -> list[str]:
|
||||
"""
|
||||
使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。
|
||||
"""
|
||||
try:
|
||||
commands = await command_management.list_commands()
|
||||
except BaseException:
|
||||
return []
|
||||
|
||||
lines: list[str] = []
|
||||
hidden_commands = {"set", "unset", "websearch"}
|
||||
|
||||
def walk(items: list[dict], indent: int = 0):
|
||||
for item in items:
|
||||
if not item.get("reserved") or not item.get("enabled"):
|
||||
continue
|
||||
# 仅展示顶级指令或指令组
|
||||
if item.get("type") == "sub_command":
|
||||
continue
|
||||
if item.get("parent_signature"):
|
||||
continue
|
||||
|
||||
effective = (
|
||||
item.get("effective_command")
|
||||
or item.get("original_command")
|
||||
or item.get("handler_name")
|
||||
)
|
||||
if not effective:
|
||||
continue
|
||||
if effective in hidden_commands:
|
||||
continue
|
||||
|
||||
description = item.get("description") or ""
|
||||
desc_text = f" - {description}" if description else ""
|
||||
indent_prefix = " " * indent
|
||||
lines.append(f"{indent_prefix}/{effective}{desc_text}")
|
||||
|
||||
walk(commands)
|
||||
return lines
|
||||
|
||||
async def help(self, event: AstrMessageEvent):
|
||||
"""查看帮助"""
|
||||
notice = ""
|
||||
@@ -30,34 +71,18 @@ class HelpCommand:
|
||||
pass
|
||||
|
||||
dashboard_version = await get_dashboard_version()
|
||||
command_lines = await self._build_reserved_command_lines()
|
||||
commands_section = (
|
||||
"\n".join(command_lines) if command_lines else "暂无启用的内置指令"
|
||||
)
|
||||
|
||||
msg = f"""AstrBot v{VERSION}(WebUI: {dashboard_version})
|
||||
内置指令:
|
||||
[System]
|
||||
/plugin: 查看插件、插件帮助
|
||||
/t2i: 开关文本转图片
|
||||
/tts: 开关文本转语音
|
||||
/sid: 获取会话 ID
|
||||
/op: 管理员
|
||||
/wl: 白名单
|
||||
/dashboard_update: 更新管理面板(op)
|
||||
/alter_cmd: 设置指令权限(op)
|
||||
|
||||
[大模型]
|
||||
/llm: 开启/关闭 LLM
|
||||
/provider: 大模型提供商
|
||||
/model: 模型列表
|
||||
/ls: 对话列表
|
||||
/new: 创建新对话
|
||||
/groupnew 群号: 为群聊创建新对话(op)
|
||||
/switch 序号: 切换对话
|
||||
/rename 新名字: 重命名当前对话
|
||||
/del: 删除当前会话对话(op)
|
||||
/reset: 重置 LLM 会话
|
||||
/history: 当前对话的对话记录
|
||||
/persona: 人格情景(op)
|
||||
/key: API Key(op)
|
||||
/websearch: 网页搜索
|
||||
{notice}"""
|
||||
msg_parts = [
|
||||
f"AstrBot v{VERSION}(WebUI: {dashboard_version})",
|
||||
"内置指令:",
|
||||
commands_section,
|
||||
]
|
||||
if notice:
|
||||
msg_parts.append(notice)
|
||||
msg = "\n".join(msg_parts)
|
||||
|
||||
event.set_result(MessageEventResult().message(msg).use_t2i(False))
|
||||
|
||||
@@ -49,7 +49,7 @@ class Main(star.Star):
|
||||
|
||||
@filter.command_group("tool")
|
||||
def tool(self):
|
||||
pass
|
||||
"""函数工具管理"""
|
||||
|
||||
@tool.command("ls")
|
||||
async def tool_ls(self, event: AstrMessageEvent):
|
||||
@@ -73,7 +73,7 @@ class Main(star.Star):
|
||||
|
||||
@filter.command_group("plugin")
|
||||
def plugin(self):
|
||||
pass
|
||||
"""插件管理"""
|
||||
|
||||
@plugin.command("ls")
|
||||
async def plugin_ls(self, event: AstrMessageEvent):
|
||||
@@ -219,6 +219,7 @@ class Main(star.Star):
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("dashboard_update")
|
||||
async def update_dashboard(self, event: AstrMessageEvent):
|
||||
"""更新管理面板"""
|
||||
await self.admin_c.update_dashboard(event)
|
||||
|
||||
@filter.command("set")
|
||||
|
||||
@@ -249,7 +249,7 @@ class Main(star.Star):
|
||||
|
||||
@filter.command_group("pi")
|
||||
def pi(self):
|
||||
pass
|
||||
"""代码执行器配置"""
|
||||
|
||||
@pi.command("absdir")
|
||||
async def pi_absdir(self, event: AstrMessageEvent, path: str = ""):
|
||||
|
||||
@@ -179,7 +179,7 @@ class Main(star.Star):
|
||||
|
||||
@filter.command_group("reminder")
|
||||
def reminder(self):
|
||||
"""The command group of the reminder."""
|
||||
"""待办提醒"""
|
||||
|
||||
async def get_upcoming_reminders(self, unified_msg_origin: str):
|
||||
"""Get upcoming reminders."""
|
||||
|
||||
@@ -185,6 +185,7 @@ class Main(star.Star):
|
||||
|
||||
@filter.command("websearch")
|
||||
async def websearch(self, event: AstrMessageEvent, oper: str | None = None):
|
||||
"""网页搜索指令(已废弃)"""
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
"此指令已经被废弃,请在 WebUI 中开启或关闭网页搜索功能。",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.9.2"
|
||||
version = "4.9.0"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -160,6 +160,34 @@ async def test_plugins(app: Quart, authenticated_header: dict):
|
||||
assert exists is False, "插件 astrbot_plugin_essential 未成功卸载"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commands_api(app: Quart, authenticated_header: dict):
|
||||
"""Tests the command management API endpoints."""
|
||||
test_client = app.test_client()
|
||||
|
||||
# GET /api/commands - list commands
|
||||
response = await test_client.get("/api/commands", headers=authenticated_header)
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data["status"] == "ok"
|
||||
assert "items" in data["data"]
|
||||
assert "summary" in data["data"]
|
||||
summary = data["data"]["summary"]
|
||||
assert "total" in summary
|
||||
assert "disabled" in summary
|
||||
assert "conflicts" in summary
|
||||
|
||||
# GET /api/commands/conflicts - list conflicts
|
||||
response = await test_client.get(
|
||||
"/api/commands/conflicts", headers=authenticated_header
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data["status"] == "ok"
|
||||
# conflicts is a list
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_update(app: Quart, authenticated_header: dict):
|
||||
test_client = app.test_client()
|
||||
|
||||
Reference in New Issue
Block a user