refactor: 重构创建平台时的流程及一些 UI 优化 (#3102)
* refactor: 支持在平台直接选择配置文件 * add webchat * feat: 支持新建平台时现场预览、创建和编辑配置文件 * fix: update configuration file descriptions and visibility based on updating mode * perf: use incremental decoder * perf: update descriptions * fix: UI update issues in config file dialog * fix: update UI elements for better readability and organization * feat: enhance sidebar navigation with group feature and dynamic resizing Co-authored-by: IGCrystal <3811541171@qq.com> * refactor: persona selector * perf: 修改部分默认行为 * fix: adjust ExtensionCard layout and improve responsiveness * refactor: 配置文件绑定消息平台重构为消息平台绑定配文件 * style: add custom styling for v-select selection text * fix: correct subtitle text in provider.json * refactor: update conversation management terminology and improve session ID handling * refactor: add Conversation ID localization and update table header reference * Update astrbot/core/db/migration/migra_45_to_46.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * style: format logger warning for better readability * refactor: comment out WebChat configuration for future reference --------- Co-authored-by: IGCrystal <3811541171@qq.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from astrbot.core.utils.shared_preferences import SharedPreferences
|
||||
from astrbot.core.config.astrbot_config import ASTRBOT_CONFIG_PATH
|
||||
from astrbot.core.config.default import DEFAULT_CONFIG
|
||||
from astrbot.core.platform.message_session import MessageSession
|
||||
from astrbot.core.umop_config_router import UmopConfigRouter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_config_path
|
||||
from typing import TypeVar, TypedDict
|
||||
|
||||
@@ -15,14 +16,12 @@ class ConfInfo(TypedDict):
|
||||
"""Configuration information for a specific session or platform."""
|
||||
|
||||
id: str # UUID of the configuration or "default"
|
||||
umop: list[str] # Unified Message Origin Pattern
|
||||
name: str
|
||||
path: str # File name to the configuration file
|
||||
|
||||
|
||||
DEFAULT_CONFIG_CONF_INFO = ConfInfo(
|
||||
id="default",
|
||||
umop=["::"],
|
||||
name="default",
|
||||
path=ASTRBOT_CONFIG_PATH,
|
||||
)
|
||||
@@ -31,8 +30,14 @@ DEFAULT_CONFIG_CONF_INFO = ConfInfo(
|
||||
class AstrBotConfigManager:
|
||||
"""A class to manage the system configuration of AstrBot, aka ACM"""
|
||||
|
||||
def __init__(self, default_config: AstrBotConfig, sp: SharedPreferences):
|
||||
def __init__(
|
||||
self,
|
||||
default_config: AstrBotConfig,
|
||||
ucr: UmopConfigRouter,
|
||||
sp: SharedPreferences,
|
||||
):
|
||||
self.sp = sp
|
||||
self.ucr = ucr
|
||||
self.confs: dict[str, AstrBotConfig] = {}
|
||||
"""uuid / "default" -> AstrBotConfig"""
|
||||
self.confs["default"] = default_config
|
||||
@@ -63,24 +68,15 @@ class AstrBotConfigManager:
|
||||
)
|
||||
continue
|
||||
|
||||
def _is_umo_match(self, p1: str, p2: str) -> bool:
|
||||
"""判断 p2 umo 是否逻辑包含于 p1 umo"""
|
||||
p1_ls = p1.split(":")
|
||||
p2_ls = p2.split(":")
|
||||
|
||||
if len(p1_ls) != 3 or len(p2_ls) != 3:
|
||||
return False # 非法格式
|
||||
|
||||
return all(p == "" or p == "*" or p == t for p, t in zip(p1_ls, p2_ls))
|
||||
|
||||
def _load_conf_mapping(self, umo: str | MessageSession) -> ConfInfo:
|
||||
"""获取指定 umo 的配置文件 uuid, 如果不存在则返回默认配置(返回 "default")
|
||||
|
||||
Returns:
|
||||
ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型
|
||||
"""
|
||||
# uuid -> { "umop": list, "path": str, "name": str }
|
||||
# uuid -> { "path": str, "name": str }
|
||||
abconf_data = self._get_abconf_data()
|
||||
|
||||
if isinstance(umo, MessageSession):
|
||||
umo = str(umo)
|
||||
else:
|
||||
@@ -89,10 +85,13 @@ class AstrBotConfigManager:
|
||||
except Exception:
|
||||
return DEFAULT_CONFIG_CONF_INFO
|
||||
|
||||
for uuid_, meta in abconf_data.items():
|
||||
for pattern in meta["umop"]:
|
||||
if self._is_umo_match(pattern, umo):
|
||||
return ConfInfo(**meta, id=uuid_)
|
||||
conf_id = self.ucr.get_conf_id_for_umop(umo)
|
||||
if conf_id:
|
||||
meta = abconf_data.get(conf_id)
|
||||
if meta and isinstance(meta, dict):
|
||||
# the bind relation between umo and conf is defined in ucr now, so we remove "umop" here
|
||||
meta.pop("umop", None)
|
||||
return ConfInfo(**meta, id=conf_id)
|
||||
|
||||
return DEFAULT_CONFIG_CONF_INFO
|
||||
|
||||
@@ -100,23 +99,14 @@ class AstrBotConfigManager:
|
||||
self,
|
||||
abconf_path: str,
|
||||
abconf_id: str,
|
||||
umo_parts: list[str] | list[MessageSession],
|
||||
abconf_name: str | None = None,
|
||||
) -> None:
|
||||
"""保存配置文件的映射关系"""
|
||||
for part in umo_parts:
|
||||
if isinstance(part, MessageSession):
|
||||
part = str(part)
|
||||
elif not isinstance(part, str):
|
||||
raise ValueError(
|
||||
"umo_parts must be a list of strings or MessageSession instances"
|
||||
)
|
||||
abconf_data = self.sp.get(
|
||||
"abconf_mapping", {}, scope="global", scope_id="global"
|
||||
)
|
||||
random_word = abconf_name or uuid.uuid4().hex[:8]
|
||||
abconf_data[abconf_id] = {
|
||||
"umop": umo_parts,
|
||||
"path": abconf_path,
|
||||
"name": random_word,
|
||||
}
|
||||
@@ -153,29 +143,26 @@ class AstrBotConfigManager:
|
||||
def get_conf_list(self) -> list[ConfInfo]:
|
||||
"""获取所有配置文件的元数据列表"""
|
||||
conf_list = []
|
||||
conf_list.append(DEFAULT_CONFIG_CONF_INFO)
|
||||
abconf_mapping = self._get_abconf_data()
|
||||
for uuid_, meta in abconf_mapping.items():
|
||||
if not isinstance(meta, dict):
|
||||
continue
|
||||
meta.pop("umop", None)
|
||||
conf_list.append(ConfInfo(**meta, id=uuid_))
|
||||
conf_list.append(DEFAULT_CONFIG_CONF_INFO)
|
||||
return conf_list
|
||||
|
||||
def create_conf(
|
||||
self,
|
||||
umo_parts: list[str] | list[MessageSession],
|
||||
config: dict = DEFAULT_CONFIG,
|
||||
name: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
umo 由三个部分组成 [platform_id]:[message_type]:[session_id]。
|
||||
|
||||
umo_parts 可以是 "::" (代表所有), 可以是 "[platform_id]::" (代表指定平台下的所有类型消息和会话)。
|
||||
"""
|
||||
conf_uuid = str(uuid.uuid4())
|
||||
conf_file_name = f"abconf_{conf_uuid}.json"
|
||||
conf_path = os.path.join(get_astrbot_config_path(), conf_file_name)
|
||||
conf = AstrBotConfig(config_path=conf_path, default_config=config)
|
||||
conf.save_config()
|
||||
self._save_conf_mapping(conf_file_name, conf_uuid, umo_parts, abconf_name=name)
|
||||
self._save_conf_mapping(conf_file_name, conf_uuid, abconf_name=name)
|
||||
self.confs[conf_uuid] = conf
|
||||
return conf_uuid
|
||||
|
||||
@@ -228,15 +215,12 @@ class AstrBotConfigManager:
|
||||
logger.info(f"成功删除配置文件 {conf_id}")
|
||||
return True
|
||||
|
||||
def update_conf_info(
|
||||
self, conf_id: str, name: str | None = None, umo_parts: list[str] | None = None
|
||||
) -> bool:
|
||||
def update_conf_info(self, conf_id: str, name: str | None = None) -> bool:
|
||||
"""更新配置文件信息
|
||||
|
||||
Args:
|
||||
conf_id: 配置文件的 UUID
|
||||
name: 新的配置文件名称 (可选)
|
||||
umo_parts: 新的 UMO 部分列表 (可选)
|
||||
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
@@ -255,18 +239,6 @@ class AstrBotConfigManager:
|
||||
if name is not None:
|
||||
abconf_data[conf_id]["name"] = name
|
||||
|
||||
# 更新 UMO 部分
|
||||
if umo_parts is not None:
|
||||
# 验证 UMO 部分格式
|
||||
for part in umo_parts:
|
||||
if isinstance(part, MessageSession):
|
||||
part = str(part)
|
||||
elif not isinstance(part, str):
|
||||
raise ValueError(
|
||||
"umo_parts must be a list of strings or MessageSession instances"
|
||||
)
|
||||
abconf_data[conf_id]["umop"] = umo_parts
|
||||
|
||||
# 保存更新
|
||||
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
||||
self.abconf_data = abconf_data
|
||||
|
||||
@@ -166,7 +166,7 @@ CONFIG_METADATA_2 = {
|
||||
"callback_server_host": "0.0.0.0",
|
||||
"port": 6196,
|
||||
},
|
||||
"QQ 个人号(aiocqhttp)": {
|
||||
"QQ 个人号(OneBot v11)": {
|
||||
"id": "default",
|
||||
"type": "aiocqhttp",
|
||||
"enable": False,
|
||||
@@ -174,7 +174,7 @@ CONFIG_METADATA_2 = {
|
||||
"ws_reverse_port": 6199,
|
||||
"ws_reverse_token": "",
|
||||
},
|
||||
"微信个人号(WeChatPadPro)": {
|
||||
"WeChatPadPro": {
|
||||
"id": "wechatpadpro",
|
||||
"type": "wechatpadpro",
|
||||
"enable": False,
|
||||
@@ -301,8 +301,26 @@ CONFIG_METADATA_2 = {
|
||||
"satori_heartbeat_interval": 10,
|
||||
"satori_reconnect_delay": 5,
|
||||
},
|
||||
# "WebChat": {
|
||||
# "id": "webchat",
|
||||
# "type": "webchat",
|
||||
# "enable": False,
|
||||
# "webchat_link_path": "",
|
||||
# "webchat_present_type": "fullscreen",
|
||||
# },
|
||||
},
|
||||
"items": {
|
||||
# "webchat_link_path": {
|
||||
# "description": "链接路径",
|
||||
# "_special": "webchat_link_path",
|
||||
# "type": "string",
|
||||
# },
|
||||
# "webchat_present_type": {
|
||||
# "_special": "webchat_present_type",
|
||||
# "description": "展现形式",
|
||||
# "type": "string",
|
||||
# "options": ["fullscreen", "embedded"],
|
||||
# },
|
||||
"satori_api_base_url": {
|
||||
"description": "Satori API 终结点",
|
||||
"type": "string",
|
||||
@@ -491,19 +509,18 @@ CONFIG_METADATA_2 = {
|
||||
"hint": "启用后,机器人可以接收到频道的私聊消息。",
|
||||
},
|
||||
"ws_reverse_host": {
|
||||
"description": "反向 Websocket 主机地址(AstrBot 为服务器端)",
|
||||
"description": "反向 Websocket 主机",
|
||||
"type": "string",
|
||||
"hint": "aiocqhttp 适配器的反向 Websocket 服务器 IP 地址,不包含端口号。",
|
||||
"hint": "AstrBot 将作为服务器端。",
|
||||
},
|
||||
"ws_reverse_port": {
|
||||
"description": "反向 Websocket 端口",
|
||||
"type": "int",
|
||||
"hint": "aiocqhttp 适配器的反向 Websocket 端口。",
|
||||
},
|
||||
"ws_reverse_token": {
|
||||
"description": "反向 Websocket Token",
|
||||
"type": "string",
|
||||
"hint": "aiocqhttp 适配器的反向 Websocket Token。未设置则不启用 Token 验证。",
|
||||
"hint": "反向 Websocket Token。未设置则不启用 Token 验证。",
|
||||
},
|
||||
"wecom_ai_bot_name": {
|
||||
"description": "企业微信智能机器人的名字",
|
||||
@@ -2219,7 +2236,7 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.wake_prefix": {
|
||||
"description": "LLM 聊天额外唤醒前缀 ",
|
||||
"type": "string",
|
||||
"hint": "例子: 如果唤醒前缀为 `/`, 额外聊天唤醒前缀为 `chat`,则需要 `/chat` 才会触发 LLM 请求。默认为空。",
|
||||
"hint": "如果唤醒前缀为 `/`, 额外聊天唤醒前缀为 `chat`,则需要 `/chat` 才会触发 LLM 请求。默认为空。",
|
||||
},
|
||||
"provider_settings.prompt_prefix": {
|
||||
"description": "用户提示词",
|
||||
|
||||
@@ -26,11 +26,13 @@ from astrbot.core.persona_mgr import PersonaManager
|
||||
from astrbot.core.provider.manager import ProviderManager
|
||||
from astrbot.core import LogBroker
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46
|
||||
from astrbot.core.updator import AstrBotUpdator
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.conversation_mgr import ConversationManager
|
||||
from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryManager
|
||||
from astrbot.core.umop_config_router import UmopConfigRouter
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
||||
from astrbot.core.star.star_handler import star_map
|
||||
@@ -84,11 +86,21 @@ class AstrBotCoreLifecycle:
|
||||
|
||||
await html_renderer.initialize()
|
||||
|
||||
# 初始化 UMOP 配置路由器
|
||||
self.umop_config_router = UmopConfigRouter(sp=sp)
|
||||
|
||||
# 初始化 AstrBot 配置管理器
|
||||
self.astrbot_config_mgr = AstrBotConfigManager(
|
||||
default_config=self.astrbot_config, sp=sp
|
||||
default_config=self.astrbot_config, ucr=self.umop_config_router, sp=sp
|
||||
)
|
||||
|
||||
# 4.5 to 4.6 migration for umop_config_router
|
||||
try:
|
||||
await migrate_45_to_46(self.astrbot_config_mgr, self.umop_config_router)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration from version 4.5 to 4.6 failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 初始化事件队列
|
||||
self.event_queue = Queue()
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from astrbot.api import logger, sp
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.umop_config_router import UmopConfigRouter
|
||||
|
||||
|
||||
async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter):
|
||||
abconf_data = acm.abconf_data
|
||||
|
||||
if not isinstance(abconf_data, dict):
|
||||
# should be unreachable
|
||||
logger.warning(
|
||||
f"migrate_45_to_46: abconf_data is not a dict (type={type(abconf_data)}). Value: {abconf_data!r}"
|
||||
)
|
||||
return
|
||||
|
||||
# 如果任何一项带有 umop,则说明需要迁移
|
||||
need_migration = False
|
||||
for conf_id, conf_info in abconf_data.items():
|
||||
if isinstance(conf_info, dict) and "umop" in conf_info:
|
||||
need_migration = True
|
||||
break
|
||||
|
||||
if not need_migration:
|
||||
return
|
||||
|
||||
logger.info("Starting migration from version 4.5 to 4.6")
|
||||
|
||||
# extract umo->conf_id mapping
|
||||
umo_to_conf_id = {}
|
||||
for conf_id, conf_info in abconf_data.items():
|
||||
if isinstance(conf_info, dict) and "umop" in conf_info:
|
||||
umop_ls = conf_info.pop("umop")
|
||||
if not isinstance(umop_ls, list):
|
||||
continue
|
||||
for umo in umop_ls:
|
||||
if isinstance(umo, str) and umo not in umo_to_conf_id:
|
||||
umo_to_conf_id[umo] = conf_id
|
||||
|
||||
# update the abconf data
|
||||
await sp.global_put("abconf_mapping", abconf_data)
|
||||
# update the umop config router
|
||||
await ucr.update_routing_data(umo_to_conf_id)
|
||||
|
||||
logger.info("Migration from version 45 to 46 completed successfully")
|
||||
@@ -0,0 +1,81 @@
|
||||
from astrbot.core.utils.shared_preferences import SharedPreferences
|
||||
|
||||
|
||||
class UmopConfigRouter:
|
||||
"""UMOP 配置路由器"""
|
||||
|
||||
def __init__(self, sp: SharedPreferences):
|
||||
self.umop_to_conf_id: dict[str, str] = {}
|
||||
"""UMOP 到配置文件 ID 的映射"""
|
||||
self.sp = sp
|
||||
|
||||
self._load_routing_table()
|
||||
|
||||
def _load_routing_table(self):
|
||||
"""加载路由表"""
|
||||
# 从 SharedPreferences 中加载 umop_to_conf_id 映射
|
||||
sp_data = self.sp.get(
|
||||
"umop_config_routing", {}, scope="global", scope_id="global"
|
||||
)
|
||||
self.umop_to_conf_id = sp_data
|
||||
|
||||
def _is_umo_match(self, p1: str, p2: str) -> bool:
|
||||
"""判断 p2 umo 是否逻辑包含于 p1 umo"""
|
||||
p1_ls = p1.split(":")
|
||||
p2_ls = p2.split(":")
|
||||
|
||||
if len(p1_ls) != 3 or len(p2_ls) != 3:
|
||||
return False # 非法格式
|
||||
|
||||
return all(p == "" or p == "*" or p == t for p, t in zip(p1_ls, p2_ls))
|
||||
|
||||
def get_conf_id_for_umop(self, umo: str) -> str | None:
|
||||
"""根据 UMO 获取对应的配置文件 ID
|
||||
|
||||
Args:
|
||||
umo (str): UMO 字符串
|
||||
|
||||
Returns:
|
||||
str | None: 配置文件 ID,如果没有找到则返回 None
|
||||
"""
|
||||
for pattern, conf_id in self.umop_to_conf_id.items():
|
||||
if self._is_umo_match(pattern, umo):
|
||||
return conf_id
|
||||
return None
|
||||
|
||||
async def update_routing_data(self, new_routing: dict[str, str]):
|
||||
"""更新路由表
|
||||
|
||||
Args:
|
||||
new_routing (dict[str, str]): 新的 UMOP 到配置文件 ID 的映射。umo 由三个部分组成 [platform_id]:[message_type]:[session_id]。
|
||||
umop 可以是 "::" (代表所有), 可以是 "[platform_id]::" (代表指定平台下的所有类型消息和会话)。
|
||||
|
||||
Raises:
|
||||
ValueError: 如果 new_routing 中的 key 格式不正确
|
||||
"""
|
||||
for part in new_routing.keys():
|
||||
if not isinstance(part, str) or len(part.split(":")) != 3:
|
||||
raise ValueError(
|
||||
"umop keys must be strings in the format [platform_id]:[message_type]:[session_id], with optional wildcards * or empty for all"
|
||||
)
|
||||
|
||||
self.umop_to_conf_id = new_routing
|
||||
await self.sp.global_put("umop_config_routing", self.umop_to_conf_id)
|
||||
|
||||
async def update_route(self, umo: str, conf_id: str):
|
||||
"""更新一条路由
|
||||
|
||||
Args:
|
||||
umo (str): UMO 字符串
|
||||
conf_id (str): 配置文件 ID
|
||||
|
||||
Raises:
|
||||
ValueError: 如果 umo 格式不正确
|
||||
"""
|
||||
if not isinstance(umo, str) or len(umo.split(":")) != 3:
|
||||
raise ValueError(
|
||||
"umop must be a string in the format [platform_id]:[message_type]:[session_id], with optional wildcards * or empty for all"
|
||||
)
|
||||
|
||||
self.umop_to_conf_id[umo] = conf_id
|
||||
await self.sp.global_put("umop_config_routing", self.umop_to_conf_id)
|
||||
@@ -6,6 +6,7 @@ from .route import Route, Response, RouteContext
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from quart import request
|
||||
from astrbot.core.config.default import (
|
||||
DEFAULT_CONFIG,
|
||||
CONFIG_METADATA_2,
|
||||
DEFAULT_VALUE_MAP,
|
||||
CONFIG_METADATA_3,
|
||||
@@ -152,13 +153,19 @@ class ConfigRoute(Route):
|
||||
self.config: AstrBotConfig = core_lifecycle.astrbot_config
|
||||
self._logo_token_cache = {} # 缓存logo token,避免重复注册
|
||||
self.acm = core_lifecycle.astrbot_config_mgr
|
||||
self.ucr = core_lifecycle.umop_config_router
|
||||
self.routes = {
|
||||
"/config/abconf/new": ("POST", self.create_abconf),
|
||||
"/config/abconf": ("GET", self.get_abconf),
|
||||
"/config/abconfs": ("GET", self.get_abconf_list),
|
||||
"/config/abconf/delete": ("POST", self.delete_abconf),
|
||||
"/config/abconf/update": ("POST", self.update_abconf),
|
||||
"/config/umo_abconf_routes": ("GET", self.get_uc_table),
|
||||
"/config/umo_abconf_route/update_all": ("POST", self.update_ucr_all),
|
||||
"/config/umo_abconf_route/update": ("POST", self.update_ucr),
|
||||
"/config/umo_abconf_route/delete": ("POST", self.delete_ucr),
|
||||
"/config/get": ("GET", self.get_configs),
|
||||
"/config/default": ("GET", self.get_default_config),
|
||||
"/config/astrbot/update": ("POST", self.post_astrbot_configs),
|
||||
"/config/plugin/update": ("POST", self.post_plugin_configs),
|
||||
"/config/platform/new": ("POST", self.post_new_platform),
|
||||
@@ -174,6 +181,75 @@ class ConfigRoute(Route):
|
||||
}
|
||||
self.register_routes()
|
||||
|
||||
async def get_uc_table(self):
|
||||
"""获取 UMOP 配置路由表"""
|
||||
return Response().ok({"routing": self.ucr.umop_to_conf_id}).__dict__
|
||||
|
||||
async def update_ucr_all(self):
|
||||
"""更新 UMOP 配置路由表的全部内容"""
|
||||
post_data = await request.json
|
||||
if not post_data:
|
||||
return Response().error("缺少配置数据").__dict__
|
||||
|
||||
new_routing = post_data.get("routing", None)
|
||||
|
||||
if not new_routing or not isinstance(new_routing, dict):
|
||||
return Response().error("缺少或错误的路由表数据").__dict__
|
||||
|
||||
try:
|
||||
await self.ucr.update_routing_data(new_routing)
|
||||
return Response().ok(message="更新成功").__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"更新路由表失败: {str(e)}").__dict__
|
||||
|
||||
async def update_ucr(self):
|
||||
"""更新 UMOP 配置路由表"""
|
||||
post_data = await request.json
|
||||
if not post_data:
|
||||
return Response().error("缺少配置数据").__dict__
|
||||
|
||||
umo = post_data.get("umo", None)
|
||||
conf_id = post_data.get("conf_id", None)
|
||||
|
||||
if not umo or not conf_id:
|
||||
return Response().error("缺少 UMO 或配置文件 ID").__dict__
|
||||
|
||||
try:
|
||||
await self.ucr.update_route(umo, conf_id)
|
||||
return Response().ok(message="更新成功").__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"更新路由表失败: {str(e)}").__dict__
|
||||
|
||||
async def delete_ucr(self):
|
||||
"""删除 UMOP 配置路由表中的一项"""
|
||||
post_data = await request.json
|
||||
if not post_data:
|
||||
return Response().error("缺少配置数据").__dict__
|
||||
|
||||
umo = post_data.get("umo", None)
|
||||
|
||||
if not umo:
|
||||
return Response().error("缺少 UMO").__dict__
|
||||
|
||||
try:
|
||||
if umo in self.ucr.umop_to_conf_id:
|
||||
del self.ucr.umop_to_conf_id[umo]
|
||||
await self.ucr.update_routing_data(self.ucr.umop_to_conf_id)
|
||||
return Response().ok(message="删除成功").__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"删除路由表项失败: {str(e)}").__dict__
|
||||
|
||||
async def get_default_config(self):
|
||||
"""获取默认配置文件"""
|
||||
return (
|
||||
Response()
|
||||
.ok({"config": DEFAULT_CONFIG, "metadata": CONFIG_METADATA_3})
|
||||
.__dict__
|
||||
)
|
||||
|
||||
async def get_abconf_list(self):
|
||||
"""获取所有 AstrBot 配置文件的列表"""
|
||||
abconf_list = self.acm.get_conf_list()
|
||||
@@ -184,11 +260,11 @@ class ConfigRoute(Route):
|
||||
post_data = await request.json
|
||||
if not post_data:
|
||||
return Response().error("缺少配置数据").__dict__
|
||||
umo_parts = post_data["umo_parts"]
|
||||
name = post_data.get("name", None)
|
||||
config = post_data.get("config", DEFAULT_CONFIG)
|
||||
|
||||
try:
|
||||
conf_id = self.acm.create_conf(umo_parts=umo_parts, name=name)
|
||||
conf_id = self.acm.create_conf(name=name, config=config)
|
||||
return Response().ok(message="创建成功", data={"conf_id": conf_id}).__dict__
|
||||
except ValueError as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
@@ -250,10 +326,9 @@ class ConfigRoute(Route):
|
||||
return Response().error("缺少配置文件 ID").__dict__
|
||||
|
||||
name = post_data.get("name")
|
||||
umo_parts = post_data.get("umo_parts")
|
||||
|
||||
try:
|
||||
success = self.acm.update_conf_info(conf_id, name=name, umo_parts=umo_parts)
|
||||
success = self.acm.update_conf_info(conf_id, name=name)
|
||||
if success:
|
||||
return Response().ok(message="更新成功").__dict__
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
|
||||
<v-tabs v-model="tab" :direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
|
||||
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
|
||||
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
|
||||
style="font-weight: 1000; font-size: 15px">
|
||||
{{ metadata[key]['name'] }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-window v-model="tab" class="config-tabs-window" :style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''">
|
||||
<v-tabs-window-item v-for="(val, key, index) in metadata" v-show="index == tab" :key="index">
|
||||
<v-container fluid>
|
||||
<div v-for="(val2, key2, index2) in metadata[key]['metadata']" :key="key2">
|
||||
<!-- Support both traditional and JSON selector metadata -->
|
||||
<AstrBotConfigV4 :metadata="{ [key2]: metadata[key]['metadata'][key2] }" :iterable="config_data"
|
||||
:metadataKey="key2">
|
||||
</AstrBotConfigV4>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-tabs-window-item>
|
||||
|
||||
|
||||
<div style="margin-left: 16px; padding-bottom: 16px">
|
||||
<small>{{ tm('help.helpPrefix') }}
|
||||
<a href="https://astrbot.app/" target="_blank">{{ tm('help.documentation') }}</a>
|
||||
{{ tm('help.helpMiddle') }}
|
||||
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft"
|
||||
target="_blank">{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AstrBotConfigV4 from '@/components/shared/AstrBotConfigV4.vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'AstrBotCoreConfigWrapper',
|
||||
components: {
|
||||
AstrBotConfigV4
|
||||
},
|
||||
props: {
|
||||
metadata: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({})
|
||||
},
|
||||
config_data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({})
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/config');
|
||||
return {
|
||||
tm
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tab: 0, // 用于切换配置标签页
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 如果需要添加其他方法,可以在这里添加
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media (min-width: 768px) {
|
||||
.config-tabs {
|
||||
display: flex;
|
||||
margin: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.config-tabs-window {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.config-tabs .v-tab {
|
||||
justify-content: flex-start !important;
|
||||
text-align: left;
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.config-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-tabs-window {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,7 +135,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<!-- Regular Property -->
|
||||
<template v-else>
|
||||
<v-row v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-col cols="12" sm="7" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
<span v-if="metadata[metadataKey].items[key]?.description">
|
||||
@@ -153,16 +153,6 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
|
||||
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
variant="flat">
|
||||
{{ metadata[metadataKey].items[key]?.type || 'string' }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="5" class="config-input">
|
||||
<div v-if="metadata[metadataKey].items[key]" class="w-100">
|
||||
<!-- Special handling for specific metadata types -->
|
||||
@@ -335,7 +325,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<!-- Simple Value Configuration -->
|
||||
<div v-else class="simple-config">
|
||||
<v-row class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-col cols="12" sm="7" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ metadata[metadataKey]?.description }}
|
||||
@@ -349,16 +339,6 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
|
||||
<v-chip v-if="!metadata[metadataKey]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
variant="flat">
|
||||
{{ metadata[metadataKey]?.type }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="5" class="config-input">
|
||||
<div class="w-100">
|
||||
<!-- Select input -->
|
||||
@@ -548,8 +528,8 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
}
|
||||
|
||||
.config-divider {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
margin: 4px 0;
|
||||
border-color: rgba(0, 0, 0, 0.05);
|
||||
margin: 0px 16px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
|
||||
@@ -120,7 +120,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
|
||||
|
||||
<v-card style="margin-bottom: 16px; padding-bottom: 8px; background-color: rgb(var(--v-theme-background));" rounded="md" variant="outlined">
|
||||
<v-card-text class="config-section" v-if="metadata[metadataKey]?.type === 'object'">
|
||||
<v-card-text class="config-section" v-if="metadata[metadataKey]?.type === 'object'" style="padding-bottom: 8px;">
|
||||
<v-list-item-title class="config-title">
|
||||
{{ metadata[metadataKey]?.description }}
|
||||
</v-list-item-title>
|
||||
@@ -365,7 +365,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
.config-row {
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
padding: 10px 8px;
|
||||
padding: 8px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import {useCustomizerStore} from "@/stores/customizer";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -84,130 +84,130 @@ const viewReadme = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="mx-auto d-flex flex-column" :elevation="highlight ? 0 : 1"
|
||||
:style="{ height: $vuetify.display.xs ? '250px' : '220px',
|
||||
backgroundColor: useCustomizerStore().uiTheme==='PurpleTheme' ? marketMode ? '#f8f0dd' : '#ffffff' : '#282833',
|
||||
color: useCustomizerStore().uiTheme==='PurpleTheme' ? '#000000dd' : '#ffffff'}">
|
||||
<v-card-text style="padding: 16px; padding-bottom: 0px; display: flex; justify-content: space-between;">
|
||||
<v-card class="mx-auto d-flex flex-column" elevation="2" :style="{
|
||||
position: 'relative',
|
||||
backgroundColor: useCustomizerStore().uiTheme === 'PurpleTheme' ? marketMode ? '#f8f0dd' : '#ffffff' : '#282833',
|
||||
color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff'
|
||||
}">
|
||||
<v-card-text style="padding: 16px; padding-bottom: 0px; display: flex; gap: 16px; width: 100%;">
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<div>{{ extension.author }} /</div>
|
||||
<div v-if="extension?.icon">
|
||||
<v-avatar size="65">
|
||||
<v-img :src="extension.icon"
|
||||
:alt="extension.name" cover></v-img>
|
||||
</v-avatar>
|
||||
</div>
|
||||
|
||||
<p class="text-h4 font-weight-black" :class="{ 'text-h4': $vuetify.display.xs }">
|
||||
{{ extension.name }}
|
||||
<v-tooltip location="top" v-if="extension?.has_update && !marketMode">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps" color="warning" class="ml-2" icon="mdi-update" size="small"></v-icon>
|
||||
<div style="width: 100%;">
|
||||
<!-- Top-right three-dot menu -->
|
||||
<div style="position: absolute; right: 8px; top: 8px; z-index: 5;">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-btn v-bind="menuProps" icon variant="text" aria-label="more">
|
||||
<v-icon icon="mdi-dots-vertical"></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ tm("card.status.hasUpdate") }}: {{ extension.online_version }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip location="top" v-if="!extension.activated && !marketMode">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps" color="error" class="ml-2" icon="mdi-cancel" size="small"></v-icon>
|
||||
</template>
|
||||
<span>{{ tm("card.status.disabled") }}</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
|
||||
<div class="mt-1 d-flex flex-wrap">
|
||||
<v-chip color="primary" label size="small">
|
||||
<v-icon icon="mdi-source-branch" start></v-icon>
|
||||
{{ extension.version }}
|
||||
</v-chip>
|
||||
<v-chip v-if="extension?.has_update " color="warning" label size="small" class="ml-2">
|
||||
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
|
||||
{{ extension.online_version }}
|
||||
</v-chip>
|
||||
<v-chip color="primary" label size="small" class="ml-2" v-if="extension.handlers?.length">
|
||||
<v-icon icon="mdi-cogs" start></v-icon>
|
||||
{{ extension.handlers?.length }}{{ tm("card.status.handlersCount") }}
|
||||
</v-chip>
|
||||
<v-chip v-for="tag in extension.tags" :key="tag" :color="tag === 'danger' ? 'error' : 'primary'" label
|
||||
size="small" class="ml-2">
|
||||
{{ tag === 'danger' ? tm('tags.danger') : tag }}
|
||||
</v-chip>
|
||||
<v-list>
|
||||
<v-list-item @click="viewReadme">
|
||||
<v-list-item-title>📄 {{ tm('buttons.viewDocs') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="marketMode && !extension?.installed" @click="installExtension">
|
||||
<v-list-item-title>
|
||||
{{ tm('buttons.install') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="marketMode && extension?.installed">
|
||||
<v-list-item-title class="text--disabled">{{ tm('status.installed') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Divider between market actions and plugin actions -->
|
||||
<v-divider v-if="!marketMode" />
|
||||
|
||||
<template v-if="!marketMode">
|
||||
<v-list-item @click="configure">
|
||||
<v-list-item-title>
|
||||
{{ tm('card.actions.pluginConfig') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="uninstallExtension">
|
||||
<v-list-item-title class="text-error">{{ tm('card.actions.uninstallPlugin') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="reloadExtension">
|
||||
<v-list-item-title>{{ tm('card.actions.reloadPlugin') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="toggleActivation">
|
||||
<v-list-item-title>
|
||||
{{ extension.activated ? tm('buttons.disable') : tm('buttons.enable') }}{{
|
||||
tm('card.actions.togglePlugin') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="viewHandlers">
|
||||
<v-list-item-title>{{ tm('card.actions.viewHandlers') }} ({{ extension.handlers.length
|
||||
}})</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="updateExtension" :disabled="!extension?.has_update">
|
||||
<v-list-item-title>
|
||||
{{ tm('card.actions.updateTo') }} {{ extension.online_version || extension.version }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<div class="mt-2" :class="{ 'text-caption': $vuetify.display.xs }" style="max-height: 65px; overflow-y: auto;">
|
||||
{{ extension.desc }}
|
||||
<div style="width: 100%; margin-bottom: 24px;">
|
||||
<!-- 最多一行 -->
|
||||
<div class="text-caption" style="color: gray; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 36px;">
|
||||
{{ extension.author }} / {{ extension.name }}
|
||||
</div>
|
||||
<p class="text-h3 font-weight-black" :class="{ 'text-h4': $vuetify.display.xs }">
|
||||
{{ extension.name }}
|
||||
<v-tooltip location="top" v-if="extension?.has_update && !marketMode">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps" color="warning" class="ml-2" icon="mdi-update" size="small"></v-icon>
|
||||
</template>
|
||||
<span>{{ tm("card.status.hasUpdate") }}: {{ extension.online_version }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip location="top" v-if="!extension.activated && !marketMode">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps" color="error" class="ml-2" icon="mdi-cancel" size="small"></v-icon>
|
||||
</template>
|
||||
<span>{{ tm("card.status.disabled") }}</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
|
||||
<div class="mt-1 d-flex flex-wrap">
|
||||
<v-chip color="primary" label size="small">
|
||||
<v-icon icon="mdi-source-branch" start></v-icon>
|
||||
{{ extension.version }}
|
||||
</v-chip>
|
||||
<v-chip v-if="extension?.has_update" color="warning" label size="small" class="ml-2">
|
||||
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
|
||||
{{ extension.online_version }}
|
||||
</v-chip>
|
||||
<v-chip color="primary" label size="small" class="ml-2" v-if="extension.handlers?.length">
|
||||
<v-icon icon="mdi-cogs" start></v-icon>
|
||||
{{ extension.handlers?.length }}{{ tm("card.status.handlersCount") }}
|
||||
</v-chip>
|
||||
<v-chip v-for="tag in extension.tags" :key="tag" :color="tag === 'danger' ? 'error' : 'primary'" label
|
||||
size="small" class="ml-2">
|
||||
{{ tag === 'danger' ? tm('tags.danger') : tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="mt-2" :class="{ 'text-caption': $vuetify.display.xs }" style="overflow-y: auto; height: 60px;">
|
||||
{{ extension.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="extension-image-container" v-if="extension.logo">
|
||||
<img :src="extension.logo" :style="{
|
||||
height: $vuetify.display.xs ? '75px' : '100px',
|
||||
width: $vuetify.display.xs ? '75px' : '100px',
|
||||
borderRadius: '8px',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center'
|
||||
}" :alt="tm('card.alt.logo')" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions style="margin-left: 0px; gap: 2px;">
|
||||
<v-btn color="teal-accent-4" :text="tm('buttons.viewDocs')" variant="text" @click="viewReadme"></v-btn>
|
||||
<v-btn v-if="!marketMode" color="teal-accent-4" :text="tm('buttons.actions')" variant="text" @click="reveal = true"></v-btn>
|
||||
<v-btn v-if="marketMode && !extension?.installed" color="teal-accent-4" :text="tm('buttons.install')" variant="text"
|
||||
@click="installExtension"></v-btn>
|
||||
<v-btn v-if="marketMode && extension?.installed" color="teal-accent-4" :text="tm('status.installed')" variant="text" disabled></v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<v-expand-transition v-if="!marketMode">
|
||||
<v-card v-if="reveal" class="position-absolute w-100" height="100%"
|
||||
style="bottom: 0; display: flex; flex-direction: column;">
|
||||
<v-card-text style="overflow-y: auto;">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<img v-if="extension.logo" :src="extension.logo"
|
||||
style="height: 50px; width: 50px; border-radius: 8px; margin-right: 16px;" :alt="tm('card.alt.extensionIcon')" />
|
||||
<h3>{{ extension.name }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" :style="{
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px',
|
||||
flexDirection: $vuetify.display.xs ? 'column' : 'row'
|
||||
}">
|
||||
<v-btn prepend-icon="mdi-cog" color="primary" variant="tonal" @click="configure"
|
||||
:block="$vuetify.display.xs">
|
||||
{{ tm("card.actions.pluginConfig") }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn prepend-icon="mdi-delete" color="error" variant="tonal" @click="uninstallExtension"
|
||||
:block="$vuetify.display.xs">
|
||||
{{ tm("card.actions.uninstallPlugin") }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn prepend-icon="mdi-reload" color="primary" variant="tonal" @click="reloadExtension"
|
||||
:block="$vuetify.display.xs">
|
||||
{{ tm("card.actions.reloadPlugin") }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn :prepend-icon="extension.activated ? 'mdi-cancel' : 'mdi-check-circle'"
|
||||
:color="extension.activated ? 'error' : 'success'" variant="tonal" @click="toggleActivation"
|
||||
:block="$vuetify.display.xs">
|
||||
{{ extension.activated ? tm('buttons.disable') : tm('buttons.enable') }}{{ tm("card.actions.togglePlugin") }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn prepend-icon="mdi-cogs" color="info" variant="tonal" @click="viewHandlers"
|
||||
:block="$vuetify.display.xs">
|
||||
{{ tm("card.actions.viewHandlers") }} ({{ extension.handlers.length }})
|
||||
</v-btn>
|
||||
|
||||
<v-btn prepend-icon="mdi-update" color="primary" variant="tonal" :disabled="!extension?.has_update "
|
||||
@click="updateExtension" :block="$vuetify.display.xs">
|
||||
{{ tm("card.actions.updateTo") }} {{ extension.online_version || extension.version }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pt-0 d-flex justify-center">
|
||||
<v-btn color="teal-accent-4" :text="tm('buttons.back')" variant="text" @click="reveal = false"></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -127,7 +127,6 @@ export default {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
min-height: 220px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
<template>
|
||||
<v-dialog v-model="showDialog" max-width="500px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h2">
|
||||
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="personaForm" v-model="formValid">
|
||||
<v-text-field v-model="personaForm.persona_id" :label="tm('form.personaId')"
|
||||
:rules="personaIdRules" :disabled="editingPersona" variant="outlined" density="comfortable"
|
||||
class="mb-4" />
|
||||
|
||||
<v-textarea v-model="personaForm.system_prompt" :label="tm('form.systemPrompt')"
|
||||
:rules="systemPromptRules" variant="outlined" rows="6" class="mb-4" />
|
||||
|
||||
<v-expansion-panels v-model="expandedPanels" multiple>
|
||||
<!-- 工具选择面板 -->
|
||||
<v-expansion-panel value="tools">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">mdi-tools</v-icon>
|
||||
{{ tm('form.tools') }}
|
||||
<v-chip v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
|
||||
size="small" color="primary" variant="tonal" class="ml-2">
|
||||
{{ personaForm.tools.length }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.toolsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-radio-group class="mt-2" v-model="toolSelectValue" hide-details="true">
|
||||
<v-radio label="默认使用全部函数工具" value="0"></v-radio>
|
||||
<v-radio label="选择指定函数工具" value="1">
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="toolSelectValue === '1'" class="mt-3 ml-8">
|
||||
|
||||
<!-- 工具搜索 -->
|
||||
<v-text-field v-model="toolSearch" :label="tm('form.searchTools')"
|
||||
prepend-inner-icon="mdi-magnify" variant="outlined" density="compact"
|
||||
hide-details clearable class="mb-3" />
|
||||
|
||||
|
||||
<!-- MCP 服务器 -->
|
||||
<div v-if="mcpServers.length > 0" class="mb-4">
|
||||
<h4 class="text-subtitle-2 mb-2">{{ tm('form.mcpServersQuickSelect') }}</h4>
|
||||
<div class="d-flex flex-wrap ga-2">
|
||||
<v-chip v-for="server in mcpServers" :key="server.name"
|
||||
:color="isServerSelected(server) ? 'primary' : 'default'"
|
||||
:variant="isServerSelected(server) ? 'flat' : 'outlined'"
|
||||
size="small" clickable @click="toggleMcpServer(server)"
|
||||
:disabled="!server.tools || server.tools.length === 0">
|
||||
<v-icon start size="small">mdi-server</v-icon>
|
||||
{{ server.name }}
|
||||
<v-chip-text v-if="server.tools" class="ml-1">
|
||||
({{ server.tools.length }})
|
||||
</v-chip-text>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具选择列表 -->
|
||||
<div v-if="filteredTools.length > 0" class="tools-selection">
|
||||
<v-virtual-scroll :items="filteredTools" height="300" item-height="48">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-list-item :key="item.name" density="comfortable"
|
||||
@click="toggleTool(item.name)">
|
||||
<template v-slot:prepend>
|
||||
<v-checkbox-btn :model-value="isToolSelected(item.name)"
|
||||
@click.stop="toggleTool(item.name)" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title>
|
||||
{{ item.name }}
|
||||
<v-chip v-if="item.mcp_server_name" size="x-small"
|
||||
color="secondary" variant="tonal" class="ml-2">
|
||||
{{ item.mcp_server_name }}
|
||||
</v-chip>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle v-if="item.description">
|
||||
{{ truncateText(item.description, 100) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loadingTools && availableTools.length === 0"
|
||||
class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-tools</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsAvailable')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loadingTools && filteredTools.length === 0"
|
||||
class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-magnify</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsFound') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loadingTools" class="text-center pa-4">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<p class="text-body-2 text-medium-emphasis mt-2">{{ tm('form.loadingTools')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 已选择的工具 -->
|
||||
<div class="mt-4">
|
||||
<h4 class="text-subtitle-2 mb-2">
|
||||
{{ tm('form.selectedTools') }}
|
||||
<span v-if="personaForm.tools === null" class="text-success">
|
||||
({{ tm('form.allSelected') }})
|
||||
</span>
|
||||
<span v-else-if="Array.isArray(personaForm.tools)">
|
||||
({{ personaForm.tools.length }})
|
||||
</span>
|
||||
</h4>
|
||||
<div v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
|
||||
class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;">
|
||||
<v-chip v-for="toolName in personaForm.tools" :key="toolName"
|
||||
size="small" color="primary" variant="tonal" closable
|
||||
@click:close="removeTool(toolName)">
|
||||
{{ toolName }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-else class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.noToolsSelected') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- 预设对话面板 -->
|
||||
<v-expansion-panel value="dialogs">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">mdi-chat</v-icon>
|
||||
{{ tm('form.presetDialogs') }}
|
||||
<v-chip v-if="personaForm.begin_dialogs.length > 0" size="small" color="primary"
|
||||
variant="tonal" class="ml-2">
|
||||
{{ personaForm.begin_dialogs.length / 2 }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.presetDialogsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(dialog, index) in personaForm.begin_dialogs" :key="index" class="mb-3">
|
||||
<v-textarea v-model="personaForm.begin_dialogs[index]"
|
||||
:label="index % 2 === 0 ? tm('form.userMessage') : tm('form.assistantMessage')"
|
||||
:rules="getDialogRules(index)" variant="outlined" rows="2"
|
||||
density="comfortable">
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-delete" variant="text" size="small" color="error"
|
||||
@click="removeDialog(index)" />
|
||||
</template>
|
||||
</v-textarea>
|
||||
</div>
|
||||
|
||||
<v-btn variant="outlined" prepend-icon="mdi-plus" @click="addDialogPair" block>
|
||||
{{ tm('buttons.addDialogPair') }}
|
||||
</v-btn>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="closeDialog">
|
||||
{{ tm('buttons.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="savePersona" :loading="saving" :disabled="!formValid">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'PersonaForm',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editingPersona: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'saved', 'error'],
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/persona');
|
||||
return { tm };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toolSelectValue: '0', // 默认选择全部工具
|
||||
saving: false,
|
||||
expandedPanels: [],
|
||||
formValid: false,
|
||||
mcpServers: [],
|
||||
availableTools: [],
|
||||
loadingTools: false,
|
||||
personaForm: {
|
||||
persona_id: '',
|
||||
system_prompt: '',
|
||||
begin_dialogs: [],
|
||||
tools: []
|
||||
},
|
||||
personaIdRules: [
|
||||
v => !!v || this.tm('validation.required'),
|
||||
v => (v && v.length >= 0) || this.tm('validation.minLength', { min: 2 }),
|
||||
],
|
||||
systemPromptRules: [
|
||||
v => !!v || this.tm('validation.required'),
|
||||
v => (v && v.length >= 10) || this.tm('validation.minLength', { min: 10 })
|
||||
],
|
||||
toolSearch: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showDialog: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
},
|
||||
filteredTools() {
|
||||
if (!this.toolSearch) {
|
||||
return this.availableTools;
|
||||
}
|
||||
const search = this.toolSearch.toLowerCase();
|
||||
return this.availableTools.filter(tool =>
|
||||
tool.name.toLowerCase().includes(search) ||
|
||||
(tool.description && tool.description.toLowerCase().includes(search)) ||
|
||||
(tool.mcp_server_name && tool.mcp_server_name.toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
modelValue(newValue) {
|
||||
if (newValue) {
|
||||
this.initForm();
|
||||
this.loadMcpServers();
|
||||
this.loadTools();
|
||||
}
|
||||
},
|
||||
editingPersona: {
|
||||
immediate: true,
|
||||
handler(newPersona) {
|
||||
if (newPersona) {
|
||||
this.initFormWithPersona(newPersona);
|
||||
} else {
|
||||
this.initForm();
|
||||
}
|
||||
}
|
||||
},
|
||||
toolSelectValue(newValue) {
|
||||
if (newValue === '0') {
|
||||
// 选择全部工具
|
||||
this.personaForm.tools = null;
|
||||
} else if (newValue === '1') {
|
||||
// 选择指定工具,如果当前是null,则转换为空数组
|
||||
if (this.personaForm.tools === null) {
|
||||
this.personaForm.tools = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
initForm() {
|
||||
this.personaForm = {
|
||||
persona_id: '',
|
||||
system_prompt: '',
|
||||
begin_dialogs: [],
|
||||
tools: []
|
||||
};
|
||||
this.toolSelectValue = '0';
|
||||
this.expandedPanels = [];
|
||||
},
|
||||
|
||||
initFormWithPersona(persona) {
|
||||
this.personaForm = {
|
||||
persona_id: persona.persona_id,
|
||||
system_prompt: persona.system_prompt,
|
||||
begin_dialogs: [...(persona.begin_dialogs || [])],
|
||||
tools: persona.tools === null ? null : [...(persona.tools || [])]
|
||||
};
|
||||
// 根据 tools 的值设置 toolSelectValue
|
||||
this.toolSelectValue = persona.tools === null ? '0' : '1';
|
||||
this.expandedPanels = [];
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
},
|
||||
|
||||
async loadMcpServers() {
|
||||
try {
|
||||
const response = await axios.get('/api/tools/mcp/servers');
|
||||
if (response.data.status === 'ok') {
|
||||
this.mcpServers = response.data.data || [];
|
||||
} else {
|
||||
this.$emit('error', response.data.message || 'Failed to load MCP servers');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$emit('error', error.response?.data?.message || 'Failed to load MCP servers');
|
||||
this.mcpServers = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadTools() {
|
||||
this.loadingTools = true;
|
||||
try {
|
||||
const response = await axios.get('/api/tools/list');
|
||||
if (response.data.status === 'ok') {
|
||||
this.availableTools = response.data.data || [];
|
||||
} else {
|
||||
this.$emit('error', response.data.message || 'Failed to load tools');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$emit('error', error.response?.data?.message || 'Failed to load tools');
|
||||
this.availableTools = [];
|
||||
} finally {
|
||||
this.loadingTools = false;
|
||||
}
|
||||
},
|
||||
|
||||
async savePersona() {
|
||||
if (!this.formValid) return;
|
||||
|
||||
// 验证预设对话不能为空
|
||||
if (this.personaForm.begin_dialogs.length > 0) {
|
||||
for (let i = 0; i < this.personaForm.begin_dialogs.length; i++) {
|
||||
if (!this.personaForm.begin_dialogs[i] || this.personaForm.begin_dialogs[i].trim() === '') {
|
||||
const dialogType = i % 2 === 0 ? this.tm('form.userMessage') : this.tm('form.assistantMessage');
|
||||
this.$emit('error', this.tm('validation.dialogRequired', { type: dialogType }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const url = this.editingPersona ? '/api/persona/update' : '/api/persona/create';
|
||||
const response = await axios.post(url, this.personaForm);
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.$emit('saved', response.data.message || this.tm('messages.saveSuccess'));
|
||||
this.closeDialog();
|
||||
} else {
|
||||
this.$emit('error', response.data.message || this.tm('messages.saveError'));
|
||||
}
|
||||
} catch (error) {
|
||||
this.$emit('error', error.response?.data?.message || this.tm('messages.saveError'));
|
||||
}
|
||||
this.saving = false;
|
||||
},
|
||||
|
||||
addDialogPair() {
|
||||
this.personaForm.begin_dialogs.push('', '');
|
||||
// 自动展开预设对话面板
|
||||
if (!this.expandedPanels.includes('dialogs')) {
|
||||
this.expandedPanels.push('dialogs');
|
||||
}
|
||||
},
|
||||
|
||||
removeDialog(index) {
|
||||
// 如果是偶数索引(用户消息),删除用户消息和对应的助手消息
|
||||
if (index % 2 === 0 && index + 1 < this.personaForm.begin_dialogs.length) {
|
||||
this.personaForm.begin_dialogs.splice(index, 2);
|
||||
}
|
||||
// 如果是奇数索引(助手消息),删除助手消息和对应的用户消息
|
||||
else if (index % 2 === 1 && index - 1 >= 0) {
|
||||
this.personaForm.begin_dialogs.splice(index - 1, 2);
|
||||
}
|
||||
},
|
||||
|
||||
toggleMcpServer(server) {
|
||||
if (!server.tools || server.tools.length === 0) return;
|
||||
|
||||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||||
if (this.personaForm.tools === null) {
|
||||
// 从全选状态转换为去除该服务器工具的状态
|
||||
this.personaForm.tools = this.availableTools.map(tool => tool.name)
|
||||
.filter(toolName => !server.tools.includes(toolName));
|
||||
this.toolSelectValue = '1'; // 切换到指定工具模式
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保tools是数组
|
||||
if (!Array.isArray(this.personaForm.tools)) {
|
||||
this.personaForm.tools = [];
|
||||
this.toolSelectValue = '1';
|
||||
}
|
||||
|
||||
// 检查是否所有服务器的工具都已选中
|
||||
const serverTools = server.tools;
|
||||
const allSelected = serverTools.every(toolName => this.personaForm.tools.includes(toolName));
|
||||
|
||||
if (allSelected) {
|
||||
// 移除所有服务器工具
|
||||
this.personaForm.tools = this.personaForm.tools.filter(
|
||||
toolName => !serverTools.includes(toolName)
|
||||
);
|
||||
} else {
|
||||
// 添加所有服务器工具
|
||||
serverTools.forEach(toolName => {
|
||||
if (!this.personaForm.tools.includes(toolName)) {
|
||||
this.personaForm.tools.push(toolName);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleTool(toolName) {
|
||||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||||
if (this.personaForm.tools === null) {
|
||||
// 如果是全选状态,点击某个工具表示要取消选择该工具
|
||||
// 所以创建一个包含所有其他工具的数组
|
||||
this.personaForm.tools = this.availableTools.map(tool => tool.name).filter(name => name !== toolName);
|
||||
this.toolSelectValue = '1'; // 切换到指定工具模式
|
||||
} else if (Array.isArray(this.personaForm.tools)) {
|
||||
const index = this.personaForm.tools.indexOf(toolName);
|
||||
if (index !== -1) {
|
||||
// 如果工具已选择,移除工具
|
||||
this.personaForm.tools.splice(index, 1);
|
||||
} else {
|
||||
// 如果工具未选择,添加工具
|
||||
this.personaForm.tools.push(toolName);
|
||||
}
|
||||
} else {
|
||||
// 如果tools不是数组也不是null,初始化为数组
|
||||
this.personaForm.tools = [toolName];
|
||||
this.toolSelectValue = '1';
|
||||
}
|
||||
},
|
||||
|
||||
removeTool(toolName) {
|
||||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||||
if (this.personaForm.tools === null) {
|
||||
// 创建一个包含所有工具的数组,然后移除指定工具
|
||||
this.personaForm.tools = this.availableTools.map(tool => tool.name).filter(name => name !== toolName);
|
||||
this.toolSelectValue = '1'; // 切换到指定工具模式
|
||||
} else if (Array.isArray(this.personaForm.tools)) {
|
||||
const index = this.personaForm.tools.indexOf(toolName);
|
||||
if (index !== -1) {
|
||||
this.personaForm.tools.splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
truncateText(text, maxLength) {
|
||||
if (!text) return '';
|
||||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
||||
},
|
||||
|
||||
getDialogRules(index) {
|
||||
const dialogType = index % 2 === 0 ? this.tm('form.userMessage') : this.tm('form.assistantMessage');
|
||||
return [
|
||||
v => !!v || this.tm('validation.dialogRequired', { type: dialogType }),
|
||||
v => (v && v.trim().length > 0) || this.tm('validation.dialogRequired', { type: dialogType })
|
||||
];
|
||||
},
|
||||
|
||||
isToolSelected(toolName) {
|
||||
// 如果是全选状态,所有工具都被选中
|
||||
if (this.personaForm.tools === null) {
|
||||
return true;
|
||||
}
|
||||
return Array.isArray(this.personaForm.tools) && this.personaForm.tools.includes(toolName);
|
||||
},
|
||||
|
||||
isServerSelected(server) {
|
||||
if (!server.tools || server.tools.length === 0) return false;
|
||||
|
||||
// 如果是全选状态,所有服务器都被选中
|
||||
if (this.personaForm.tools === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查服务器的所有工具是否都已选中
|
||||
return Array.isArray(this.personaForm.tools) &&
|
||||
server.tools.every(toolName => this.personaForm.tools.includes(toolName));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tools-selection {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-virtual-scroll {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@
|
||||
选择人格
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<v-card-text class="pa-2" style="max-height: 400px; overflow-y: auto;">
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
|
||||
<v-list v-if="!loading && personaList.length > 0" density="compact">
|
||||
@@ -48,6 +48,9 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-btn variant="text" color="primary" prepend-icon="mdi-plus" @click="openCreatePersona">
|
||||
创建新人格
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
|
||||
<v-btn
|
||||
@@ -59,11 +62,22 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 创建人格对话框 -->
|
||||
<PersonaForm
|
||||
v-model="showCreateDialog"
|
||||
:editing-persona="null"
|
||||
:mcp-servers="mcpServers"
|
||||
:available-tools="availableTools"
|
||||
:loading-tools="loadingTools"
|
||||
@saved="handlePersonaCreated"
|
||||
@error="handleError" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import PersonaForm from './PersonaForm.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -82,6 +96,7 @@ const dialog = ref(false)
|
||||
const personaList = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedPersona = ref('')
|
||||
const showCreateDialog = ref(false)
|
||||
|
||||
// 监听 modelValue 变化,同步到 selectedPersona
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
@@ -135,6 +150,21 @@ function cancelSelection() {
|
||||
selectedPersona.value = props.modelValue || ''
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function openCreatePersona() {
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
async function handlePersonaCreated(message) {
|
||||
console.log('人格创建成功:', message)
|
||||
showCreateDialog.value = false
|
||||
// 刷新人格列表
|
||||
await loadPersonas()
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
console.error('创建人格失败:', error)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dashboard": "Dashboard",
|
||||
"dashboard": "Dashboard",
|
||||
"platforms": "Platforms",
|
||||
"providers": "Providers",
|
||||
"persona": "Persona",
|
||||
@@ -16,5 +16,8 @@
|
||||
"settings": "Settings",
|
||||
"documentation": "Documentation",
|
||||
"github": "GitHub",
|
||||
"drag": "Drag"
|
||||
}
|
||||
"drag": "Drag",
|
||||
"groups": {
|
||||
"more": "More Features"
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@
|
||||
"title": "Conversation Title",
|
||||
"platform": "Platform",
|
||||
"type": "Type",
|
||||
"sessionId": "ID",
|
||||
"cid": "Conversation ID",
|
||||
"umo": "Unified Message Origin",
|
||||
"sessionId": "Session ID",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At",
|
||||
"actions": "Actions"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"dashboard": "统计",
|
||||
"platforms": "消息平台",
|
||||
"providers": "服务提供商",
|
||||
"dashboard": "数据统计",
|
||||
"platforms": "机器人",
|
||||
"providers": "模型提供商",
|
||||
"persona": "人格设定",
|
||||
"toolUse": "MCP",
|
||||
"extension": "插件",
|
||||
@@ -16,5 +16,8 @@
|
||||
"settings": "设置",
|
||||
"documentation": "官方文档",
|
||||
"github": "GitHub",
|
||||
"drag": "拖拽"
|
||||
}
|
||||
"drag": "拖拽",
|
||||
"groups": {
|
||||
"more": "更多功能"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"subtitle": "管理和查看用户对话历史记录",
|
||||
"filters": {
|
||||
"title": "筛选条件",
|
||||
"platform": "消息平台 ID",
|
||||
"platform": "机器人 ID",
|
||||
"type": "类型",
|
||||
"search": "搜索关键词",
|
||||
"reset": "重置"
|
||||
@@ -22,9 +22,11 @@
|
||||
"table": {
|
||||
"headers": {
|
||||
"title": "对话标题",
|
||||
"platform": "消息平台 ID",
|
||||
"type": "类型",
|
||||
"sessionId": "ID (UMO)",
|
||||
"platform": "机器人 ID",
|
||||
"type": "消息类型",
|
||||
"cid": "对话 ID",
|
||||
"umo": "消息会话来源",
|
||||
"sessionId": "会话 ID",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"actions": "操作"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"title": "平台适配器管理",
|
||||
"subtitle": "管理机器人的平台适配器,连接到不同的聊天平台",
|
||||
"title": "机器人",
|
||||
"subtitle": "管理平台适配器实例,连接到不同的消息平台",
|
||||
"adapters": "平台适配器",
|
||||
"addAdapter": "新增适配器",
|
||||
"emptyText": "暂无平台适配器,点击 新增适配器 添加",
|
||||
"addAdapter": "创建机器人",
|
||||
"emptyText": "暂无平台适配器,点击 创建机器人 添加",
|
||||
"details": {
|
||||
"adapterType": "适配器类型",
|
||||
"token": "Token",
|
||||
@@ -17,11 +17,11 @@
|
||||
"dialog": {
|
||||
"add": "新增",
|
||||
"edit": "编辑",
|
||||
"adapter": "平台适配器",
|
||||
"adapter": "机器人",
|
||||
"refresh": "刷新",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"addPlatform": "添加平台适配器",
|
||||
"addPlatform": "创建机器人",
|
||||
"connectTitle": "接入 {name}",
|
||||
"viewTutorial": "查看接入教程",
|
||||
"noTemplates": "暂无平台模板",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"title": "服务提供商管理",
|
||||
"subtitle": "管理模型服务提供商",
|
||||
"title": "模型提供商",
|
||||
"subtitle": "管理模型提供商",
|
||||
"providers": {
|
||||
"title": "服务提供商",
|
||||
"title": "模型提供商",
|
||||
"settings": "设置",
|
||||
"addProvider": "新增服务提供商",
|
||||
"addProvider": "新增模型提供商",
|
||||
"providerType": "提供商类型",
|
||||
"tabs": {
|
||||
"all": "全部",
|
||||
@@ -15,8 +15,8 @@
|
||||
"rerank": "重排序(Rerank)"
|
||||
},
|
||||
"empty": {
|
||||
"all": "暂无服务提供商,点击 新增服务提供商 添加",
|
||||
"typed": "暂无{type}类型的服务提供商,点击 新增服务提供商 添加"
|
||||
"all": "暂无模型提供商,点击 新增模型提供商 添加",
|
||||
"typed": "暂无{type}类型的模型提供商,点击 新增模型提供商 添加"
|
||||
},
|
||||
"description": {
|
||||
"openai": "也支持所有兼容 OpenAI API 的模型提供商。",
|
||||
@@ -25,10 +25,10 @@
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
"title": "服务提供商可用性",
|
||||
"title": "模型提供商可用性",
|
||||
"subtitle": "通过测试模型对话可用性判断,可能产生API费用",
|
||||
"refresh": "刷新状态",
|
||||
"noData": "点击\"刷新状态\"按钮获取服务提供商可用性",
|
||||
"noData": "点击\"刷新状态\"按钮获取模型提供商可用性",
|
||||
"available": "可用",
|
||||
"unavailable": "不可用",
|
||||
"pending": "检查中...",
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"dialogs": {
|
||||
"addProvider": {
|
||||
"title": "服务提供商",
|
||||
"title": "模型提供商",
|
||||
"tabs": {
|
||||
"basic": "基本",
|
||||
"speechToText": "语音转文字",
|
||||
@@ -55,15 +55,15 @@
|
||||
"config": {
|
||||
"addTitle": "新增",
|
||||
"editTitle": "编辑",
|
||||
"provider": "服务提供商",
|
||||
"provider": "模型提供商",
|
||||
"cancel": "取消",
|
||||
"save": "保存"
|
||||
},
|
||||
"settings": {
|
||||
"title": "服务提供商设置",
|
||||
"title": "模型提供商设置",
|
||||
"sessionSeparation": {
|
||||
"title": "启用提供商会话隔离",
|
||||
"description": "不同会话将可独立选择文本生成、TTS、STT 等服务提供商。"
|
||||
"description": "不同会话将可独立选择文本生成、TTS、STT 等模型提供商。"
|
||||
},
|
||||
"close": "关闭"
|
||||
}
|
||||
@@ -78,11 +78,11 @@
|
||||
},
|
||||
"error": {
|
||||
"sessionSeparation": "获取会话隔离配置失败",
|
||||
"fetchStatus": "获取服务提供商状态失败",
|
||||
"fetchStatus": "获取模型提供商状态失败",
|
||||
"testError": "测试 {id} 失败: {error}"
|
||||
},
|
||||
"confirm": {
|
||||
"delete": "确定要删除服务提供商 {id} 吗?"
|
||||
"delete": "确定要删除模型提供商 {id} 吗?"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"table": {
|
||||
"headers": {
|
||||
"sessionStatus": "会话状态",
|
||||
"sessionInfo": "ID (UMO)",
|
||||
"sessionInfo": "消息会话来源",
|
||||
"persona": "人格",
|
||||
"chatProvider": "聊天模型",
|
||||
"sttProvider": "语音识别模型",
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
<script setup>
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({ item: Object, level: Number });
|
||||
const { t } = useI18n();
|
||||
const customizer = useCustomizerStore();
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
const lvl = props.level ?? 0;
|
||||
const indent = customizer.mini_sidebar ? '0px' : `${lvl * 24}px`;
|
||||
return { '--indent-padding': indent };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item
|
||||
:to="item.type === 'external' ? '' : item.to"
|
||||
:href="item.type === 'external' ? item.to : ''"
|
||||
rounded
|
||||
class="mb-1"
|
||||
color="secondary"
|
||||
:disabled="item.disabled"
|
||||
:target="item.type === 'external' ? '_blank' : ''"
|
||||
>
|
||||
<v-list-group v-if="item.children" :value="item.title" :class="{ 'group-bordered': customizer.mini_sidebar }">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-item v-bind="props" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
|
||||
:style="{ '--indent-padding': '0px' }">
|
||||
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
|
||||
{{ t(item.title) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<!-- children -->
|
||||
<template v-for="(child, index) in item.children" :key="index">
|
||||
<NavItem :item="child" :level="(level || 0) + 1" />
|
||||
</template>
|
||||
</v-list-group>
|
||||
|
||||
<v-list-item v-else :to="item.type === 'external' ? '' : item.to" :href="item.type === 'external' ? item.to : ''" rounded
|
||||
class="mb-1" color="secondary" :disabled="item.disabled" :target="item.type === 'external' ? '_blank' : ''" :style="itemStyle">
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
@@ -23,15 +41,19 @@ const { t } = useI18n();
|
||||
{{ item.subCaption }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append v-if="item.chip">
|
||||
<v-chip
|
||||
:color="item.chipColor"
|
||||
class="sidebarchip hide-menu"
|
||||
:size="item.chipIcon ? 'small' : 'default'"
|
||||
:variant="item.chipVariant"
|
||||
:prepend-icon="item.chipIcon"
|
||||
>
|
||||
<v-chip :color="item.chipColor" class="sidebarchip hide-menu" :size="item.chipIcon ? 'small' : 'default'"
|
||||
:variant="item.chipVariant" :prepend-icon="item.chipIcon">
|
||||
{{ item.chip }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 在折叠(mini)状态下,分组展开时给整个分组(母项+子项)加边框以便区分 */
|
||||
.group-bordered.v-list-group--open {
|
||||
border: 2px solid rgba(var(--v-theme-borderLight), 0.35);
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--v-theme-borderLight), 0.04);
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,11 @@ const sidebarMenu = shallowRef(sidebarItems);
|
||||
|
||||
const showIframe = ref(false);
|
||||
|
||||
// 默认桌面端 iframe 样式
|
||||
const sidebarWidth = ref(235);
|
||||
const minSidebarWidth = 200;
|
||||
const maxSidebarWidth = 300;
|
||||
const isResizing = ref(false);
|
||||
|
||||
const iframeStyle = ref({
|
||||
position: 'fixed',
|
||||
bottom: '16px',
|
||||
@@ -29,14 +33,13 @@ const iframeStyle = ref({
|
||||
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
});
|
||||
|
||||
// 如果为移动端,则采用百分比尺寸,并设置初始位置
|
||||
if (window.innerWidth < 768) {
|
||||
iframeStyle.value = {
|
||||
position: 'fixed',
|
||||
top: '10%',
|
||||
left: '0%',
|
||||
width: '100%',
|
||||
height: '50%',
|
||||
height: '80%',
|
||||
minWidth: '300px',
|
||||
minHeight: '200px',
|
||||
background: 'white',
|
||||
@@ -46,7 +49,6 @@ if (window.innerWidth < 768) {
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
};
|
||||
// 移动端默认关闭侧边栏
|
||||
customizer.Sidebar_drawer = false;
|
||||
}
|
||||
|
||||
@@ -74,12 +76,10 @@ function openIframeLink(url) {
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽相关变量与函数
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let isDragging = false;
|
||||
|
||||
// 辅助函数:限制数值在一定范围内
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
@@ -91,7 +91,6 @@ function startDrag(clientX, clientY) {
|
||||
offsetX = clientX - rect.left;
|
||||
offsetY = clientY - rect.top;
|
||||
document.body.style.userSelect = 'none';
|
||||
// 绑定全局鼠标和触摸事件
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
@@ -149,6 +148,34 @@ function endDrag() {
|
||||
document.removeEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
|
||||
function startSidebarResize(event) {
|
||||
isResizing.value = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
|
||||
const startX = event.clientX;
|
||||
const startWidth = sidebarWidth.value;
|
||||
|
||||
function onMouseMoveResize(event) {
|
||||
if (!isResizing.value) return;
|
||||
|
||||
const deltaX = event.clientX - startX;
|
||||
const newWidth = Math.max(minSidebarWidth, Math.min(maxSidebarWidth, startWidth + deltaX));
|
||||
sidebarWidth.value = newWidth;
|
||||
}
|
||||
|
||||
function onMouseUpResize() {
|
||||
isResizing.value = false;
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
document.removeEventListener('mousemove', onMouseMoveResize);
|
||||
document.removeEventListener('mouseup', onMouseUpResize);
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMoveResize);
|
||||
document.addEventListener('mouseup', onMouseUpResize);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -159,7 +186,7 @@ function endDrag() {
|
||||
rail-width="80"
|
||||
app
|
||||
class="leftSidebar"
|
||||
width="220"
|
||||
:width="sidebarWidth"
|
||||
:rail="customizer.mini_sidebar"
|
||||
>
|
||||
<div class="sidebar-container">
|
||||
@@ -180,6 +207,14 @@ function endDrag() {
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!customizer.mini_sidebar && customizer.Sidebar_drawer"
|
||||
class="sidebar-resize-handle"
|
||||
@mousedown="startSidebarResize"
|
||||
:class="{ 'resizing': isResizing }"
|
||||
>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<div
|
||||
@@ -187,14 +222,13 @@ function endDrag() {
|
||||
id="draggable-iframe"
|
||||
:style="iframeStyle"
|
||||
>
|
||||
<!-- 拖拽头部:支持鼠标和触摸 -->
|
||||
|
||||
<div :style="dragHeaderStyle" @mousedown="onMouseDown" @touchstart="onTouchStart">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<v-icon icon="mdi-cursor-move" />
|
||||
<span style="margin-left: 8px;">{{ t('core.navigation.drag') }}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<!-- 跳转按钮 -->
|
||||
<v-btn
|
||||
icon
|
||||
@click.stop="openIframeLink('https://astrbot.app')"
|
||||
@@ -203,7 +237,6 @@ function endDrag() {
|
||||
>
|
||||
<v-icon icon="mdi-open-in-new" />
|
||||
</v-btn>
|
||||
<!-- 关闭按钮 -->
|
||||
<v-btn
|
||||
icon
|
||||
@click.stop="toggleIframe"
|
||||
@@ -214,10 +247,53 @@ function endDrag() {
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<!-- iframe 区域 -->
|
||||
<iframe
|
||||
src="https://astrbot.app"
|
||||
style="width: 100%; height: calc(100% - 56px); border: none; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px;"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
cursor: ew-resize;
|
||||
user-select: none;
|
||||
z-index: 1000;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle:hover,
|
||||
.sidebar-resize-handle.resizing {
|
||||
background: rgba(var(--v-theme-primary), 0.3);
|
||||
}
|
||||
|
||||
.sidebar-resize-handle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: rgba(var(--v-theme-on-surface), 0.3);
|
||||
border-radius: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle:hover::before,
|
||||
.sidebar-resize-handle.resizing::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 确保侧边栏容器支持相对定位 */
|
||||
.leftSidebar .v-navigation-drawer__content {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -18,31 +18,26 @@ export interface menu {
|
||||
// 在组件中使用时需要通过t()函数进行翻译
|
||||
// 所有键名都使用 core.navigation.* 格式
|
||||
const sidebarItem: menu[] = [
|
||||
{
|
||||
title: 'core.navigation.dashboard',
|
||||
icon: 'mdi-view-dashboard',
|
||||
to: '/dashboard/default'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.platforms',
|
||||
icon: 'mdi-message-processing',
|
||||
to: '/platforms',
|
||||
icon: 'mdi-robot',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.providers',
|
||||
icon: 'mdi-creation',
|
||||
to: '/providers',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.config',
|
||||
icon: 'mdi-cog',
|
||||
to: '/config',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.toolUse',
|
||||
icon: 'mdi-function-variant',
|
||||
to: '/tool-use'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.persona',
|
||||
icon: 'mdi-heart',
|
||||
to: '/persona'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extension',
|
||||
icon: 'mdi-puzzle',
|
||||
@@ -53,31 +48,42 @@ const sidebarItem: menu[] = [
|
||||
icon: 'mdi-text-box-search',
|
||||
to: '/alkaid/knowledge-base',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.config',
|
||||
icon: 'mdi-cog',
|
||||
to: '/config',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.chat',
|
||||
icon: 'mdi-chat',
|
||||
to: '/chat'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.conversation',
|
||||
icon: 'mdi-database',
|
||||
to: '/conversation'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.sessionManagement',
|
||||
icon: 'mdi-account-group',
|
||||
to: '/session-management'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.console',
|
||||
icon: 'mdi-console',
|
||||
to: '/console'
|
||||
},
|
||||
title: 'core.navigation.groups.more',
|
||||
icon: 'mdi-dots-horizontal',
|
||||
children: [
|
||||
{
|
||||
title: 'core.navigation.persona',
|
||||
icon: 'mdi-heart',
|
||||
to: '/persona'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.conversation',
|
||||
icon: 'mdi-database',
|
||||
to: '/conversation'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.sessionManagement',
|
||||
icon: 'mdi-account-group',
|
||||
to: '/session-management'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.dashboard',
|
||||
icon: 'mdi-view-dashboard',
|
||||
to: '/dashboard/default'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.console',
|
||||
icon: 'mdi-console',
|
||||
to: '/console'
|
||||
},
|
||||
]
|
||||
}
|
||||
// {
|
||||
// title: 'Project ATRI',
|
||||
// icon: 'mdi-grain',
|
||||
|
||||
@@ -3,13 +3,13 @@ const MainRoutes = {
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
},
|
||||
redirect: '/main/dashboard/default',
|
||||
redirect: '/main/platforms',
|
||||
component: () => import('@/layouts/full/FullLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
name: 'MainPage',
|
||||
path: '/',
|
||||
component: () => import('@/views/dashboards/default/DefaultDashboard.vue')
|
||||
component: () => import('@/views/PlatformPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'Extensions',
|
||||
|
||||
@@ -11,12 +11,6 @@
|
||||
<v-select style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name"
|
||||
v-if="!isSystemConfig" item-value="id" label="选择配置文件" hide-details density="compact" rounded="md"
|
||||
variant="outlined" @update:model-value="onConfigSelect">
|
||||
<template v-slot:item="{ props: itemProps, item }">
|
||||
<v-list-item v-bind="itemProps"
|
||||
:subtitle="item.raw.id === '_%manage%_' ? '管理所有配置文件' : formatUmop(item.raw.umop)"
|
||||
:class="item.raw.id === '_%manage%_' ? 'text-primary' : ''">
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<a style="color: inherit;" href="https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" target="_blank"><v-btn icon="mdi-help-circle" size="small" variant="plain"></v-btn></a>
|
||||
|
||||
@@ -37,38 +31,10 @@
|
||||
|
||||
<div v-if="(selectedConfigID || isSystemConfig) && fetched" style="width: 100%;">
|
||||
<!-- 可视化编辑 -->
|
||||
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
|
||||
<v-tabs v-model="tab" :direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
|
||||
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
|
||||
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
|
||||
style="font-weight: 1000; font-size: 15px">
|
||||
{{ metadata[key]['name'] }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-window v-model="tab" class="config-tabs-window">
|
||||
<v-tabs-window-item v-for="(val, key, index) in metadata" v-show="index == tab" :key="index">
|
||||
<v-container fluid>
|
||||
<div v-for="(val2, key2, index2) in metadata[key]['metadata']" :key="key2">
|
||||
<!-- Support both traditional and JSON selector metadata -->
|
||||
<AstrBotConfigV4 :metadata="{ [key2]: metadata[key]['metadata'][key2] }" :iterable="config_data"
|
||||
:metadataKey="key2">
|
||||
</AstrBotConfigV4>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-tabs-window-item>
|
||||
|
||||
|
||||
<div style="margin-left: 16px; padding-bottom: 16px">
|
||||
<small>{{ tm('help.helpPrefix') }}
|
||||
<a href="https://astrbot.app/" target="_blank">{{ tm('help.documentation') }}</a>
|
||||
{{ tm('help.helpMiddle') }}
|
||||
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft"
|
||||
target="_blank">{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
<AstrBotCoreConfigWrapper
|
||||
:metadata="metadata"
|
||||
:config_data="config_data"
|
||||
/>
|
||||
|
||||
<v-btn icon="mdi-content-save" size="x-large" style="position: fixed; right: 52px; bottom: 52px;"
|
||||
color="darkprimary" @click="updateConfig">
|
||||
@@ -118,7 +84,7 @@
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<small>AstrBot 支持针对不同消息平台实例分别设置配置文件。默认会使用 `default` 配置。</small>
|
||||
<small>AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。</small>
|
||||
<div class="mt-6 mb-4">
|
||||
<v-btn prepend-icon="mdi-plus" @click="startCreateConfig" variant="tonal" color="primary">
|
||||
新建配置文件
|
||||
@@ -128,8 +94,6 @@
|
||||
<!-- Config List -->
|
||||
<v-list lines="two">
|
||||
<v-list-item v-for="config in configInfoList" :key="config.id" :title="config.name">
|
||||
<v-list-item-subtitle>当前应用于: {{ formatUmop(config.umop) }} </v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append v-if="config.id !== 'default'">
|
||||
<div class="d-flex align-center" style="gap: 8px;">
|
||||
<v-btn icon="mdi-pencil" size="small" variant="text" color="warning"
|
||||
@@ -147,149 +111,15 @@
|
||||
<div v-if="showConfigForm">
|
||||
<h3 class="mb-4">{{ isEditingConfig ? '编辑配置文件' : '新建配置文件' }}</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<div v-if="conflictMessage" class="text-warning">
|
||||
<div v-html="conflictMessage" style="font-size: 0.875rem; line-height: 1.4;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>名称</h4>
|
||||
|
||||
<v-text-field v-model="configFormData.name" label="填写配置文件名称" variant="outlined" class="mt-4 mb-4"
|
||||
hide-details></v-text-field>
|
||||
|
||||
<h4>应用于</h4>
|
||||
|
||||
<v-radio-group class="mt-2" v-model="appliedToRadioValue" hide-details="true">
|
||||
<v-radio value="0">
|
||||
<template v-slot:label>
|
||||
<span>指定消息平台...</span>
|
||||
</template>
|
||||
</v-radio>
|
||||
<v-select v-if="appliedToRadioValue === '0'" v-model="configFormData.umop" :items="platformList" item-title="id" item-value="id"
|
||||
label="选择已配置的消息平台(可多选)" variant="outlined" hide-details multiple class="ma-2"
|
||||
@update:model-value="checkPlatformConflictOnForm">
|
||||
<template v-slot:item="{ props: itemProps, item }">
|
||||
<v-list-item v-bind="itemProps" :subtitle="item.raw.type"></v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-radio value="1" label="自定义规则(实验性)">
|
||||
</v-radio>
|
||||
|
||||
<!-- 自定义规则界面 -->
|
||||
<div v-if="appliedToRadioValue === '1'" class="ma-2">
|
||||
<small class="text-medium-emphasis mb-4 d-block">UMO 格式: [platform_id]:[message_type]:[session_id]。通配符 * 或留空表示全部。使用 /sid 查看某个聊天的 UMO。</small>
|
||||
|
||||
<!-- 输入方式切换 -->
|
||||
<v-btn-toggle v-model="customRuleInputMode" mandatory color="primary" variant="outlined" density="compact"
|
||||
rounded="md" class="mb-4">
|
||||
<v-btn value="builder" prepend-icon="mdi-tune" size="x-small">
|
||||
可视化
|
||||
</v-btn>
|
||||
<v-btn value="manual" prepend-icon="mdi-code-tags" size="x-small">
|
||||
手动编辑
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<!-- 快速规则构建 -->
|
||||
<div v-if="customRuleInputMode === 'builder'" class="mb-4">
|
||||
<div v-for="(rule, index) in customRules" :key="index" class="d-flex align-center mb-2" style="gap: 8px;">
|
||||
<v-select
|
||||
v-model="rule.platform"
|
||||
:items="[{ id: '*', type: '所有平台' }, ...platformList]"
|
||||
item-title="id"
|
||||
item-value="id"
|
||||
label="平台"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
style="min-width: 120px;"
|
||||
@update:model-value="updateCustomRule(index)">
|
||||
<template v-slot:item="{ props: itemProps, item }">
|
||||
<v-list-item v-bind="itemProps" :subtitle="item.raw.type"></v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<v-select
|
||||
v-model="rule.messageType"
|
||||
:items="messageTypeOptions"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
label="消息类型"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
style="min-width: 130px;"
|
||||
@update:model-value="updateCustomRule(index)">
|
||||
</v-select>
|
||||
|
||||
<v-text-field
|
||||
v-model="rule.sessionId"
|
||||
label="会话ID"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="* 或留空表示全部"
|
||||
style="min-width: 120px;"
|
||||
@update:model-value="updateCustomRule(index)">
|
||||
</v-text-field>
|
||||
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="removeCustomRule(index)"
|
||||
:disabled="customRules.length === 1">
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
prepend-icon="mdi-plus"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="addCustomRule">
|
||||
添加规则
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 手动输入 -->
|
||||
<div v-if="customRuleInputMode === 'manual'" class="mb-4">
|
||||
<v-textarea
|
||||
v-model="manualRulesText"
|
||||
label="手动输入规则(每行一个)"
|
||||
variant="outlined"
|
||||
rows="4"
|
||||
placeholder="每行一个规则,例如: platform1:GroupMessage:* *:FriendMessage:session123 *:*:*"
|
||||
@update:model-value="updateManualRules">
|
||||
</v-textarea>
|
||||
</div>
|
||||
|
||||
<!-- 规则预览 -->
|
||||
<div class="mb-2">
|
||||
<small class="text-medium-emphasis">
|
||||
<strong>预览:</strong>
|
||||
<span v-if="!configFormData.umop.length" class="text-error">未配置任何规则</span>
|
||||
<div v-else class="mt-1">
|
||||
<v-chip
|
||||
v-for="(rule, index) in configFormData.umop"
|
||||
:key="index"
|
||||
size="x-small"
|
||||
rounded="sm"
|
||||
class="mr-1">
|
||||
{{ rule }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<small>这些规则对应的会话将使用此配置文件。</small>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</v-radio-group>
|
||||
|
||||
|
||||
|
||||
<div class="d-flex justify-end mt-4" style="gap: 8px;">
|
||||
<v-btn variant="text" @click="cancelConfigForm">取消</v-btn>
|
||||
<v-btn color="primary" @click="saveConfigForm"
|
||||
:disabled="!configFormData.name || !configFormData.umop.length">
|
||||
:disabled="!configFormData.name">
|
||||
{{ isEditingConfig ? '更新' : '创建' }}
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -308,7 +138,7 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import AstrBotConfigV4 from '@/components/shared/AstrBotConfigV4.vue';
|
||||
import AstrBotCoreConfigWrapper from '@/components/config/AstrBotCoreConfigWrapper.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
@@ -316,7 +146,7 @@ import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
export default {
|
||||
name: 'ConfigPage',
|
||||
components: {
|
||||
AstrBotConfigV4,
|
||||
AstrBotCoreConfigWrapper,
|
||||
VueMonacoEditor,
|
||||
WaitingForRestart
|
||||
},
|
||||
@@ -359,15 +189,6 @@ export default {
|
||||
watch: {
|
||||
config_data_str: function (val) {
|
||||
this.config_data_has_changed = true;
|
||||
},
|
||||
customRuleInputMode: function (newVal) {
|
||||
if (newVal === 'builder') {
|
||||
// 切换到快速构建,从手动输入同步数据
|
||||
this.syncCustomRulesFromManual();
|
||||
} else if (newVal === 'manual') {
|
||||
// 切换到手动输入,从快速构建同步数据
|
||||
this.syncManualRulesText();
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -387,8 +208,6 @@ export default {
|
||||
save_message: "",
|
||||
save_message_success: "",
|
||||
|
||||
tab: 0, // 用于切换配置标签页
|
||||
|
||||
// 配置类型切换
|
||||
configType: 'normal', // 'normal' 或 'system'
|
||||
|
||||
@@ -396,32 +215,12 @@ export default {
|
||||
isSystemConfig: false,
|
||||
|
||||
// 多配置文件管理
|
||||
appliedToRadioValue: '0',
|
||||
selectedConfigID: null, // 用于存储当前选中的配置项信息
|
||||
configInfoList: [],
|
||||
platformList: [],
|
||||
configFormData: {
|
||||
name: '',
|
||||
umop: [],
|
||||
},
|
||||
editingConfigId: null,
|
||||
conflictMessage: '', // 冲突提示信息
|
||||
|
||||
// 自定义规则相关
|
||||
customRuleInputMode: 'builder', // 'builder' 或 'manual'
|
||||
customRules: [
|
||||
{
|
||||
platform: '*',
|
||||
messageType: '*',
|
||||
sessionId: '*'
|
||||
}
|
||||
],
|
||||
manualRulesText: '',
|
||||
messageTypeOptions: [
|
||||
{ label: '所有消息类型', value: '*' },
|
||||
{ label: '群组消息', value: 'GroupMessage' },
|
||||
{ label: '私聊消息', value: 'FriendMessage' }
|
||||
],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -450,13 +249,6 @@ export default {
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
},
|
||||
getPlatformList() {
|
||||
axios.get('/api/config/platform/list').then((res) => {
|
||||
this.platformList = res.data.data.platforms;
|
||||
}).catch((err) => {
|
||||
console.error(this.t('status.dataError'), err);
|
||||
});
|
||||
},
|
||||
getConfig(abconf_id) {
|
||||
this.fetched = false
|
||||
const params = {};
|
||||
@@ -532,18 +324,7 @@ export default {
|
||||
}
|
||||
},
|
||||
createNewConfig() {
|
||||
let umo_parts = [];
|
||||
|
||||
if (this.appliedToRadioValue === '0') {
|
||||
// 修正为 umo part 形式 - 指定平台
|
||||
umo_parts = this.configFormData.umop.map(platform => platform + "::");
|
||||
} else if (this.appliedToRadioValue === '1') {
|
||||
// 自定义规则
|
||||
umo_parts = [...this.configFormData.umop]; // 直接使用 umop,它已经包含完整的规则
|
||||
}
|
||||
|
||||
axios.post('/api/config/abconf/new', {
|
||||
umo_parts: umo_parts,
|
||||
name: this.configFormData.name
|
||||
}).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
@@ -564,210 +345,9 @@ export default {
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
},
|
||||
checkPlatformConflict(newRules) {
|
||||
const conflictConfigs = [];
|
||||
|
||||
// 遍历现有的配置文件,排除名为 "default" 的配置
|
||||
for (const config of this.configInfoList) {
|
||||
if (config.name === 'default') {
|
||||
continue; // 跳过 default 配置
|
||||
}
|
||||
|
||||
if (config.umop && config.umop.length > 0) {
|
||||
// 检查是否有冲突
|
||||
const hasConflict = this.hasUmoConflict(newRules, config.umop);
|
||||
|
||||
if (hasConflict) {
|
||||
conflictConfigs.push(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflictConfigs;
|
||||
},
|
||||
|
||||
hasUmoConflict(newRules, existingRules) {
|
||||
// 检查新规则与现有规则是否有冲突
|
||||
for (const newRule of newRules) {
|
||||
for (const existingRule of existingRules) {
|
||||
if (this.isUmoMatch(newRule, existingRule) || this.isUmoMatch(existingRule, newRule)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isUmoMatch(p1, p2) {
|
||||
// 判断 p2 umo 是否逻辑包含于 p1 umo
|
||||
// 基于后端的 _is_umo_match 逻辑
|
||||
|
||||
// 先标准化规则格式
|
||||
const p1_normalized = this.normalizeUmoRule(p1);
|
||||
const p2_normalized = this.normalizeUmoRule(p2);
|
||||
|
||||
const p1_parts = p1_normalized.split(":");
|
||||
const p2_parts = p2_normalized.split(":");
|
||||
|
||||
if (p1_parts.length !== 3 || p2_parts.length !== 3) {
|
||||
return false; // 非法格式
|
||||
}
|
||||
|
||||
// 检查每个部分是否匹配
|
||||
return p1_parts.every((p, index) => {
|
||||
const t = p2_parts[index];
|
||||
return p === "" || p === "*" || p === t;
|
||||
});
|
||||
},
|
||||
|
||||
normalizeUmoRule(rule) {
|
||||
// 标准化规则格式
|
||||
if (typeof rule !== 'string') {
|
||||
return "*:*:*";
|
||||
}
|
||||
|
||||
const parts = rule.split(":");
|
||||
|
||||
if (parts.length === 2 && parts[1] === "") {
|
||||
// 传统格式 "platform::" -> "platform:*:*"
|
||||
return `${parts[0] || "*"}:*:*`;
|
||||
} else if (parts.length === 3) {
|
||||
// 已经是完整格式,只需要处理空字符串
|
||||
return parts.map(part => part === "" ? "*" : part).join(":");
|
||||
} else if (parts.length === 1) {
|
||||
// 只有平台 "platform" -> "platform:*:*"
|
||||
return `${parts[0] || "*"}:*:*`;
|
||||
}
|
||||
|
||||
// 默认返回通配符
|
||||
return "*:*:*";
|
||||
},
|
||||
|
||||
getDetailedConflictInfo(newRules) {
|
||||
const conflictDetails = [];
|
||||
|
||||
// 获取所有配置文件及其优先级(按创建时间排序,早创建的优先级高)
|
||||
const sortedConfigs = [...this.configInfoList]
|
||||
.filter(config => config.name !== 'default')
|
||||
.sort((a, b) => {
|
||||
// 假设按字母顺序排序作为优先级(实际应该按创建时间)
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
|
||||
for (const config of sortedConfigs) {
|
||||
if (!config.umop || config.umop.length === 0) continue;
|
||||
|
||||
const conflictingRules = [];
|
||||
|
||||
for (const newRule of newRules) {
|
||||
for (const existingRule of config.umop) {
|
||||
if (this.isUmoMatch(newRule, existingRule) || this.isUmoMatch(existingRule, newRule)) {
|
||||
conflictingRules.push({
|
||||
newRule: newRule,
|
||||
existingRule: existingRule,
|
||||
matchType: this.getMatchType(newRule, existingRule)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (conflictingRules.length > 0) {
|
||||
conflictDetails.push({
|
||||
config: config,
|
||||
conflicts: conflictingRules
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return conflictDetails;
|
||||
},
|
||||
|
||||
getMatchType(rule1, rule2) {
|
||||
const r1_normalized = this.normalizeUmoRule(rule1);
|
||||
const r2_normalized = this.normalizeUmoRule(rule2);
|
||||
|
||||
const isR1MatchR2 = this.isUmoMatch(rule1, rule2);
|
||||
const isR2MatchR1 = this.isUmoMatch(rule2, rule1);
|
||||
|
||||
if (isR1MatchR2 && isR2MatchR1) {
|
||||
return 'exact'; // 完全匹配
|
||||
} else if (isR1MatchR2) {
|
||||
return 'new_covers_existing'; // 新规则覆盖现有规则
|
||||
} else if (isR2MatchR1) {
|
||||
return 'existing_covers_new'; // 现有规则覆盖新规则
|
||||
}
|
||||
|
||||
return 'overlap'; // 部分重叠
|
||||
},
|
||||
|
||||
formatConflictMessage(conflictDetails) {
|
||||
if (conflictDetails.length === 0) return '';
|
||||
|
||||
let message = '⚠️ <strong>规则冲突警告:</strong><br><br>';
|
||||
|
||||
// 按优先级排序(最先创建的配置文件优先级最高)
|
||||
const sortedDetails = [...conflictDetails].sort((a, b) =>
|
||||
a.config.id.localeCompare(b.config.id)
|
||||
);
|
||||
|
||||
sortedDetails.forEach((detail, index) => {
|
||||
const configName = detail.config.name || detail.config.id;
|
||||
message += `<strong>${index + 1}. 与配置文件 "${configName}" 冲突:</strong><br>`;
|
||||
|
||||
detail.conflicts.forEach(conflict => {
|
||||
const newRuleFormatted = this.formatRuleForDisplay(conflict.newRule);
|
||||
const existingRuleFormatted = this.formatRuleForDisplay(conflict.existingRule);
|
||||
|
||||
switch (conflict.matchType) {
|
||||
case 'exact':
|
||||
message += `规则完全相同: <code>${newRuleFormatted}</code><br>`;
|
||||
message += `<span style="color: orange;">"${configName}" 将覆盖当前配置</span><br>`;
|
||||
break;
|
||||
case 'new_covers_existing':
|
||||
message += `当前规则 <code>${newRuleFormatted}</code> 包含现有规则 <code>${existingRuleFormatted}</code><br>`;
|
||||
message += `<span style="color: red;">"${configName}" 的规则将优先匹配</span><br>`;
|
||||
break;
|
||||
case 'existing_covers_new':
|
||||
message += `现有规则 <code>${existingRuleFormatted}</code> 包含当前规则 <code>${newRuleFormatted}</code><br>`;
|
||||
message += `<span style="color: red;">"${configName}" 的规则将优先匹配</span><br>`;
|
||||
break;
|
||||
case 'overlap':
|
||||
message += `规则重叠: <code>${newRuleFormatted}</code> ↔ <code>${existingRuleFormatted}</code><br>`;
|
||||
message += `<span style="color: orange;">"${configName}" 在匹配范围内优先</span><br>`;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (index < sortedDetails.length - 1) {
|
||||
message += '<br>';
|
||||
}
|
||||
});
|
||||
|
||||
message += '<br><small><strong>💡 说明:</strong> 您仍可创建此配置文件。AstrBot 按配置文件创建顺序匹配规则,先创建的配置文件优先级更高。当多个配置文件的规则匹配同一个消息会话来源时,优先级最高的配置文件会生效(default 配置文件除外)。</small>';
|
||||
|
||||
return message;
|
||||
},
|
||||
|
||||
formatRuleForDisplay(rule) {
|
||||
const parts = this.normalizeUmoRule(rule).split(':');
|
||||
const platform = parts[0] === '*' || parts[0] === '' ? '任意平台' : parts[0];
|
||||
const messageType = parts[1] === '*' || parts[1] === '' ? '任意消息' : this.getMessageTypeLabel(parts[1]);
|
||||
const sessionId = parts[2] === '*' || parts[2] === '' ? '任意会话' : parts[2];
|
||||
|
||||
return `${platform}:${messageType}:${sessionId}`;
|
||||
},
|
||||
|
||||
getMessageTypeLabel(messageType) {
|
||||
const typeMap = {
|
||||
'GroupMessage': '群组消息',
|
||||
'FriendMessage': '私聊消息',
|
||||
};
|
||||
return typeMap[messageType] || messageType;
|
||||
},
|
||||
onConfigSelect(value) {
|
||||
if (value === '_%manage%_') {
|
||||
this.configManageDialog = true;
|
||||
this.getPlatformList();
|
||||
// 重置选择到之前的值
|
||||
this.$nextTick(() => {
|
||||
this.selectedConfigID = this.selectedConfigInfo.id || 'default';
|
||||
@@ -781,26 +361,17 @@ export default {
|
||||
this.isEditingConfig = false;
|
||||
this.configFormData = {
|
||||
name: '',
|
||||
umop: [],
|
||||
};
|
||||
this.editingConfigId = null;
|
||||
this.conflictMessage = '';
|
||||
this.resetCustomRules();
|
||||
},
|
||||
startEditConfig(config) {
|
||||
this.appliedToRadioValue = "1";
|
||||
this.showConfigForm = true;
|
||||
this.isEditingConfig = true;
|
||||
this.editingConfigId = config.id;
|
||||
|
||||
this.parseExistingCustomRules(config.umop || []);
|
||||
|
||||
this.configFormData = {
|
||||
name: config.name || '',
|
||||
umop: [...(config.umop || [])],
|
||||
};
|
||||
|
||||
this.conflictMessage = '';
|
||||
},
|
||||
cancelConfigForm() {
|
||||
this.showConfigForm = false;
|
||||
@@ -808,14 +379,11 @@ export default {
|
||||
this.editingConfigId = null;
|
||||
this.configFormData = {
|
||||
name: '',
|
||||
umop: [],
|
||||
};
|
||||
this.conflictMessage = '';
|
||||
this.resetCustomRules();
|
||||
},
|
||||
saveConfigForm() {
|
||||
if (!this.configFormData.name || !this.configFormData.umop.length) {
|
||||
this.save_message = "请填写配置名称和选择应用平台";
|
||||
if (!this.configFormData.name) {
|
||||
this.save_message = "请填写配置名称";
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
return;
|
||||
@@ -827,106 +395,6 @@ export default {
|
||||
this.createNewConfig();
|
||||
}
|
||||
},
|
||||
|
||||
// 自定义规则相关方法
|
||||
addCustomRule() {
|
||||
this.customRules.push({
|
||||
platform: '*',
|
||||
messageType: '*',
|
||||
sessionId: '*'
|
||||
});
|
||||
this.updateCustomRulesFromBuilder();
|
||||
},
|
||||
|
||||
removeCustomRule(index) {
|
||||
if (this.customRules.length > 1) {
|
||||
this.customRules.splice(index, 1);
|
||||
this.updateCustomRulesFromBuilder();
|
||||
}
|
||||
},
|
||||
|
||||
updateCustomRule(index) {
|
||||
this.updateCustomRulesFromBuilder();
|
||||
},
|
||||
|
||||
updateCustomRulesFromBuilder() {
|
||||
// 从规则构建器更新 umop
|
||||
const rules = this.customRules.map(rule => {
|
||||
const platform = rule.platform === '*' ? '' : rule.platform;
|
||||
const messageType = rule.messageType === '*' ? '' : rule.messageType;
|
||||
const sessionId = rule.sessionId === '*' ? '' : (rule.sessionId || '');
|
||||
return `${platform}:${messageType}:${sessionId}`;
|
||||
});
|
||||
|
||||
this.configFormData.umop = rules;
|
||||
this.syncManualRulesText();
|
||||
// 触发冲突检测
|
||||
this.checkPlatformConflictOnForm();
|
||||
},
|
||||
|
||||
updateManualRules() {
|
||||
// 从手动输入更新 umop
|
||||
const rules = this.manualRulesText
|
||||
.split('\n')
|
||||
.map(rule => rule.trim())
|
||||
.filter(rule => rule);
|
||||
|
||||
this.configFormData.umop = rules;
|
||||
this.syncCustomRulesFromManual();
|
||||
// 触发冲突检测
|
||||
this.checkPlatformConflictOnForm();
|
||||
},
|
||||
|
||||
syncManualRulesText() {
|
||||
// 同步到手动输入文本区域
|
||||
this.manualRulesText = this.configFormData.umop.join('\n');
|
||||
},
|
||||
|
||||
syncCustomRulesFromManual() {
|
||||
// 从手动输入同步到规则构建器
|
||||
this.customRules = this.configFormData.umop.map(rule => {
|
||||
const parts = rule.split(':');
|
||||
return {
|
||||
platform: parts[0] || '*',
|
||||
messageType: parts[1] || '*',
|
||||
sessionId: parts[2] || '*'
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
resetCustomRules() {
|
||||
this.customRuleInputMode = 'builder'; // 重置为快速构建模式
|
||||
this.customRules = [
|
||||
{
|
||||
platform: '*',
|
||||
messageType: '*',
|
||||
sessionId: '*'
|
||||
}
|
||||
];
|
||||
this.manualRulesText = '';
|
||||
if (this.appliedToRadioValue === '1') {
|
||||
this.updateCustomRulesFromBuilder();
|
||||
}
|
||||
},
|
||||
|
||||
parseExistingCustomRules(umop) {
|
||||
// 解析现有的自定义规则
|
||||
if (!umop || umop.length === 0) {
|
||||
this.resetCustomRules();
|
||||
return;
|
||||
}
|
||||
|
||||
this.customRules = umop.map(rule => {
|
||||
const parts = rule.split(':');
|
||||
return {
|
||||
platform: parts[0] || '*',
|
||||
messageType: parts[1] || '*',
|
||||
sessionId: parts[2] || '*'
|
||||
};
|
||||
});
|
||||
|
||||
this.syncManualRulesText();
|
||||
},
|
||||
confirmDeleteConfig(config) {
|
||||
if (confirm(`确定要删除配置文件 "${config.name}" 吗?此操作不可恢复。`)) {
|
||||
this.deleteConfig(config.id);
|
||||
@@ -940,6 +408,7 @@ export default {
|
||||
this.save_message = res.data.message;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
this.cancelConfigForm();
|
||||
// 删除成功后,更新配置列表
|
||||
this.getConfigInfoList("default");
|
||||
} else {
|
||||
@@ -954,52 +423,10 @@ export default {
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
},
|
||||
checkPlatformConflictOnForm() {
|
||||
if (!this.configFormData.umop || this.configFormData.umop.length === 0) {
|
||||
this.conflictMessage = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备用于冲突检测的规则列表
|
||||
let rulesToCheck = [];
|
||||
|
||||
if (this.appliedToRadioValue === '0') {
|
||||
// 平台模式:转换为标准UMO格式
|
||||
rulesToCheck = this.configFormData.umop.map(platform => `${platform}:*:*`);
|
||||
} else {
|
||||
// 自定义模式:直接使用规则
|
||||
rulesToCheck = [...this.configFormData.umop];
|
||||
}
|
||||
|
||||
// 检查与其他配置文件的冲突
|
||||
let conflictDetails = this.getDetailedConflictInfo(rulesToCheck);
|
||||
|
||||
// 如果是编辑模式,排除当前编辑的配置文件
|
||||
if (this.isEditingConfig && this.editingConfigId) {
|
||||
conflictDetails = conflictDetails.filter(detail => detail.config.id !== this.editingConfigId);
|
||||
}
|
||||
|
||||
if (conflictDetails.length > 0) {
|
||||
this.conflictMessage = this.formatConflictMessage(conflictDetails);
|
||||
} else {
|
||||
this.conflictMessage = '';
|
||||
}
|
||||
},
|
||||
updateConfigInfo() {
|
||||
let umo_parts = [];
|
||||
|
||||
if (this.appliedToRadioValue === '0') {
|
||||
// 修正为 umo part 形式 - 指定平台
|
||||
umo_parts = this.configFormData.umop.map(platform => platform + "::");
|
||||
} else if (this.appliedToRadioValue === '1') {
|
||||
// 自定义规则
|
||||
umo_parts = [...this.configFormData.umop]; // 直接使用 umop,它已经包含完整的规则
|
||||
}
|
||||
|
||||
axios.post('/api/config/abconf/update', {
|
||||
id: this.editingConfigId,
|
||||
name: this.configFormData.name,
|
||||
umo_parts: umo_parts
|
||||
name: this.configFormData.name
|
||||
}).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
this.save_message = res.data.message;
|
||||
@@ -1019,38 +446,8 @@ export default {
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
},
|
||||
formatUmop(umop) {
|
||||
if (!umop) {
|
||||
return
|
||||
}
|
||||
let ret = ""
|
||||
for (let i = 0; i < umop.length; i++) {
|
||||
const parts = umop[i].split(":");
|
||||
if (parts.length === 3) {
|
||||
// 自定义规则格式 platform:messageType:sessionId
|
||||
const platform = parts[0] || "*";
|
||||
const messageType = parts[1] || "*";
|
||||
const sessionId = parts[2] || "*";
|
||||
if (platform === "*" && messageType === "*" && sessionId === "*") {
|
||||
return "所有平台";
|
||||
}
|
||||
ret += `${platform}:${messageType}:${sessionId},`;
|
||||
} else {
|
||||
// 传统平台格式
|
||||
let platformPart = umop[i].split(":")[0];
|
||||
if (platformPart === "") {
|
||||
return "所有平台";
|
||||
} else {
|
||||
ret += platformPart + ",";
|
||||
}
|
||||
}
|
||||
}
|
||||
ret = ret.slice(0, -1);
|
||||
return ret;
|
||||
},
|
||||
onConfigTypeToggle() {
|
||||
this.isSystemConfig = this.configType === 'system';
|
||||
this.tab = 0; // 重置标签页
|
||||
this.fetched = false; // 重置加载状态
|
||||
|
||||
if (this.isSystemConfig) {
|
||||
@@ -1069,7 +466,6 @@ export default {
|
||||
// 保持向后兼容性,更新 configType
|
||||
this.configType = this.isSystemConfig ? 'system' : 'normal';
|
||||
|
||||
this.tab = 0; // 重置标签页
|
||||
this.fetched = false; // 重置加载状态
|
||||
|
||||
if (this.isSystemConfig) {
|
||||
@@ -1128,31 +524,12 @@ export default {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.config-tabs {
|
||||
display: flex;
|
||||
margin: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
width: 750px;
|
||||
}
|
||||
|
||||
.config-tabs-window {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.config-tabs .v-tab {
|
||||
justify-content: flex-start !important;
|
||||
text-align: left;
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.config-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.v-container {
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -1160,9 +537,5 @@ export default {
|
||||
.config-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-tabs-window {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -433,14 +433,14 @@ export default {
|
||||
tableHeaders() {
|
||||
return [
|
||||
{ title: this.tm('table.headers.title'), key: 'title', sortable: true },
|
||||
{ title: '会话 ID', key: 'cid', sortable: true, width: '100px' },
|
||||
{ title: this.tm('table.headers.cid'), key: 'cid', sortable: true, width: '100px' },
|
||||
{
|
||||
title: this.tm('table.headers.sessionId'),
|
||||
title: this.tm('table.headers.umo'),
|
||||
align: 'center',
|
||||
children: [
|
||||
{ title: this.tm('table.headers.platform'), key: 'platform', sortable: true, width: '120px' },
|
||||
{ title: this.tm('table.headers.type'), key: 'messageType', sortable: true, width: '100px' },
|
||||
{ title: '用户 ID', key: 'sessionId', sortable: true, width: '100px' },
|
||||
{ title: this.tm('table.headers.sessionId'), key: 'sessionId', sortable: true, width: '100px' },
|
||||
],
|
||||
},
|
||||
{ title: this.tm('table.headers.createdAt'), key: 'created_at', sortable: true, width: '180px' },
|
||||
|
||||
@@ -749,9 +749,9 @@ onMounted(async () => {
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" lg="4" v-for="extension in filteredPlugins" :key="extension.name"
|
||||
<v-col cols="12" md="6" lg="6" v-for="extension in filteredPlugins" :key="extension.name"
|
||||
class="pb-4">
|
||||
<ExtensionCard :extension="extension" class="h-120 rounded-lg"
|
||||
<ExtensionCard :extension="extension" class="rounded-lg"
|
||||
@configure="openExtensionConfig(extension.name)" @uninstall="uninstallExtension(extension.name)"
|
||||
@update="updateExtension(extension.name)" @reload="reloadPlugin(extension.name)"
|
||||
@toggle-activation="extension.activated ? pluginOff(extension) : pluginOn(extension)"
|
||||
|
||||
@@ -90,200 +90,11 @@
|
||||
</v-container>
|
||||
|
||||
<!-- 创建/编辑人格对话框 -->
|
||||
<v-dialog v-model="showPersonaDialog" max-width="800px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h2">
|
||||
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="personaForm" v-model="formValid">
|
||||
<v-text-field v-model="personaForm.persona_id" :label="tm('form.personaId')"
|
||||
:rules="personaIdRules" :disabled="editingPersona" variant="outlined" density="comfortable"
|
||||
class="mb-4" />
|
||||
|
||||
<v-textarea v-model="personaForm.system_prompt" :label="tm('form.systemPrompt')"
|
||||
:rules="systemPromptRules" variant="outlined" rows="6" class="mb-4" />
|
||||
|
||||
<v-expansion-panels v-model="expandedPanels" multiple>
|
||||
<!-- 工具选择面板 -->
|
||||
<v-expansion-panel value="tools">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">mdi-tools</v-icon>
|
||||
{{ tm('form.tools') }}
|
||||
<v-chip v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
|
||||
size="small" color="primary" variant="tonal" class="ml-2">
|
||||
{{ personaForm.tools.length }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.toolsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-radio-group class="mt-2" v-model="toolSelectValue" hide-details="true">
|
||||
<v-radio label="默认使用全部函数工具" value="0"></v-radio>
|
||||
<v-radio label="选择指定函数工具" value="1">
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="toolSelectValue === '1'" class="mt-3 ml-8">
|
||||
|
||||
<!-- 工具搜索 -->
|
||||
<v-text-field v-model="toolSearch" :label="tm('form.searchTools')"
|
||||
prepend-inner-icon="mdi-magnify" variant="outlined" density="compact"
|
||||
hide-details clearable class="mb-3" />
|
||||
|
||||
|
||||
<!-- MCP 服务器 -->
|
||||
<div v-if="mcpServers.length > 0" class="mb-4">
|
||||
<h4 class="text-subtitle-2 mb-2">{{ tm('form.mcpServersQuickSelect') }}</h4>
|
||||
<div class="d-flex flex-wrap ga-2">
|
||||
<v-chip v-for="server in mcpServers" :key="server.name"
|
||||
:color="isServerSelected(server) ? 'primary' : 'default'"
|
||||
:variant="isServerSelected(server) ? 'flat' : 'outlined'"
|
||||
size="small" clickable @click="toggleMcpServer(server)"
|
||||
:disabled="!server.tools || server.tools.length === 0">
|
||||
<v-icon start size="small">mdi-server</v-icon>
|
||||
{{ server.name }}
|
||||
<v-chip-text v-if="server.tools" class="ml-1">
|
||||
({{ server.tools.length }})
|
||||
</v-chip-text>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具选择列表 -->
|
||||
<div v-if="filteredTools.length > 0" class="tools-selection">
|
||||
<v-virtual-scroll :items="filteredTools" height="300" item-height="48">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-list-item :key="item.name" density="comfortable"
|
||||
@click="toggleTool(item.name)">
|
||||
<template v-slot:prepend>
|
||||
<v-checkbox-btn :model-value="isToolSelected(item.name)"
|
||||
@click.stop="toggleTool(item.name)" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title>
|
||||
{{ item.name }}
|
||||
<v-chip v-if="item.mcp_server_name" size="x-small"
|
||||
color="secondary" variant="tonal" class="ml-2">
|
||||
{{ item.mcp_server_name }}
|
||||
</v-chip>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle v-if="item.description">
|
||||
{{ truncateText(item.description, 100) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loadingTools && availableTools.length === 0"
|
||||
class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-tools</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsAvailable')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loadingTools && filteredTools.length === 0"
|
||||
class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-magnify</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsFound') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loadingTools" class="text-center pa-4">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<p class="text-body-2 text-medium-emphasis mt-2">{{ tm('form.loadingTools')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 已选择的工具 -->
|
||||
<div class="mt-4">
|
||||
<h4 class="text-subtitle-2 mb-2">
|
||||
{{ tm('form.selectedTools') }}
|
||||
<span v-if="personaForm.tools === null" class="text-success">
|
||||
({{ tm('form.allSelected') }})
|
||||
</span>
|
||||
<span v-else-if="Array.isArray(personaForm.tools)">
|
||||
({{ personaForm.tools.length }})
|
||||
</span>
|
||||
</h4>
|
||||
<div v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
|
||||
class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;">
|
||||
<v-chip v-for="toolName in personaForm.tools" :key="toolName"
|
||||
size="small" color="primary" variant="tonal" closable
|
||||
@click:close="removeTool(toolName)">
|
||||
{{ toolName }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-else class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.noToolsSelected') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- 预设对话面板 -->
|
||||
<v-expansion-panel value="dialogs">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">mdi-chat</v-icon>
|
||||
{{ tm('form.presetDialogs') }}
|
||||
<v-chip v-if="personaForm.begin_dialogs.length > 0" size="small" color="primary"
|
||||
variant="tonal" class="ml-2">
|
||||
{{ personaForm.begin_dialogs.length / 2 }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.presetDialogsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(dialog, index) in personaForm.begin_dialogs" :key="index" class="mb-3">
|
||||
<v-textarea v-model="personaForm.begin_dialogs[index]"
|
||||
:label="index % 2 === 0 ? tm('form.userMessage') : tm('form.assistantMessage')"
|
||||
:rules="getDialogRules(index)" variant="outlined" rows="2"
|
||||
density="comfortable">
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-delete" variant="text" size="small" color="error"
|
||||
@click="removeDialog(index)" />
|
||||
</template>
|
||||
</v-textarea>
|
||||
</div>
|
||||
|
||||
<v-btn variant="outlined" prepend-icon="mdi-plus" @click="addDialogPair" block>
|
||||
{{ tm('buttons.addDialogPair') }}
|
||||
</v-btn>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="closePersonaDialog">
|
||||
{{ tm('buttons.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="savePersona" :loading="saving" :disabled="!formValid">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<PersonaForm
|
||||
v-model="showPersonaDialog"
|
||||
:editing-persona="editingPersona"
|
||||
@saved="handlePersonaSaved"
|
||||
@error="showError" />
|
||||
|
||||
<!-- 查看人格详情对话框 -->
|
||||
<v-dialog v-model="showViewDialog" max-width="700px">
|
||||
@@ -352,9 +163,13 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import PersonaForm from '@/components/shared/PersonaForm.vue';
|
||||
|
||||
export default {
|
||||
name: 'PersonaPage',
|
||||
components: {
|
||||
PersonaForm
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/persona');
|
||||
@@ -362,76 +177,20 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toolSelectValue: '0', // 默认选择全部工具
|
||||
personas: [],
|
||||
loading: false,
|
||||
saving: false,
|
||||
showPersonaDialog: false,
|
||||
showViewDialog: false,
|
||||
editingPersona: null,
|
||||
viewingPersona: null,
|
||||
expandedPanels: [],
|
||||
formValid: false,
|
||||
personaForm: {
|
||||
persona_id: '',
|
||||
system_prompt: '',
|
||||
begin_dialogs: [],
|
||||
tools: []
|
||||
},
|
||||
showMessage: false,
|
||||
message: '',
|
||||
messageType: 'success',
|
||||
personaIdRules: [
|
||||
v => !!v || this.tm('validation.required'),
|
||||
v => (v && v.length >= 0) || this.tm('validation.minLength', { min: 2 }),
|
||||
],
|
||||
systemPromptRules: [
|
||||
v => !!v || this.tm('validation.required'),
|
||||
v => (v && v.length >= 10) || this.tm('validation.minLength', { min: 10 })
|
||||
],
|
||||
mcpServers: [],
|
||||
availableTools: [],
|
||||
loadingTools: false,
|
||||
toolSearch: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredTools() {
|
||||
if (!this.toolSearch) {
|
||||
return this.availableTools;
|
||||
}
|
||||
const search = this.toolSearch.toLowerCase();
|
||||
return this.availableTools.filter(tool =>
|
||||
tool.name.toLowerCase().includes(search) ||
|
||||
(tool.description && tool.description.toLowerCase().includes(search)) ||
|
||||
(tool.mcp_server_name && tool.mcp_server_name.toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
toolSearch() {
|
||||
// 响应式搜索,无需额外处理
|
||||
},
|
||||
|
||||
toolSelectValue(newValue) {
|
||||
if (newValue === '0') {
|
||||
// 选择全部工具
|
||||
this.personaForm.tools = null;
|
||||
} else if (newValue === '1') {
|
||||
// 选择指定工具,如果当前是null,则转换为空数组
|
||||
if (this.personaForm.tools === null) {
|
||||
this.personaForm.tools = [];
|
||||
}
|
||||
}
|
||||
messageType: 'success'
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadPersonas();
|
||||
this.loadMcpServers();
|
||||
this.loadTools();
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -450,58 +209,13 @@ export default {
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async loadMcpServers() {
|
||||
try {
|
||||
const response = await axios.get('/api/tools/mcp/servers');
|
||||
if (response.data.status === 'ok') {
|
||||
this.mcpServers = response.data.data;
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.loadError'));
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.loadError'));
|
||||
}
|
||||
},
|
||||
|
||||
async loadTools() {
|
||||
this.loadingTools = true;
|
||||
try {
|
||||
const response = await axios.get('/api/tools/list');
|
||||
if (response.data.status === 'ok') {
|
||||
this.availableTools = response.data.data;
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.loadError'));
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.loadError'));
|
||||
}
|
||||
this.loadingTools = false;
|
||||
},
|
||||
|
||||
openCreateDialog() {
|
||||
this.editingPersona = null;
|
||||
this.personaForm = {
|
||||
persona_id: '',
|
||||
system_prompt: '',
|
||||
begin_dialogs: [],
|
||||
tools: []
|
||||
};
|
||||
this.toolSelectValue = '0';
|
||||
this.expandedPanels = [];
|
||||
this.showPersonaDialog = true;
|
||||
},
|
||||
|
||||
editPersona(persona) {
|
||||
this.editingPersona = persona;
|
||||
this.personaForm = {
|
||||
persona_id: persona.persona_id,
|
||||
system_prompt: persona.system_prompt,
|
||||
begin_dialogs: [...(persona.begin_dialogs || [])],
|
||||
tools: persona.tools === null ? null : [...(persona.tools || [])]
|
||||
};
|
||||
// 根据 tools 的值设置 toolSelectValue
|
||||
this.toolSelectValue = persona.tools === null ? '0' : '1';
|
||||
this.expandedPanels = [];
|
||||
this.showPersonaDialog = true;
|
||||
},
|
||||
|
||||
@@ -510,48 +224,9 @@ export default {
|
||||
this.showViewDialog = true;
|
||||
},
|
||||
|
||||
closePersonaDialog() {
|
||||
this.showPersonaDialog = false;
|
||||
this.editingPersona = null;
|
||||
this.personaForm = {
|
||||
persona_id: '',
|
||||
system_prompt: '',
|
||||
begin_dialogs: [],
|
||||
tools: []
|
||||
};
|
||||
this.toolSelectValue = '1'; // 重置为默认值
|
||||
},
|
||||
|
||||
async savePersona() {
|
||||
if (!this.formValid) return;
|
||||
|
||||
// 验证预设对话不能为空
|
||||
if (this.personaForm.begin_dialogs.length > 0) {
|
||||
for (let i = 0; i < this.personaForm.begin_dialogs.length; i++) {
|
||||
if (!this.personaForm.begin_dialogs[i] || this.personaForm.begin_dialogs[i].trim() === '') {
|
||||
const dialogType = i % 2 === 0 ? this.tm('form.userMessage') : this.tm('form.assistantMessage');
|
||||
this.showError(this.tm('validation.dialogRequired', { type: dialogType }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const url = this.editingPersona ? '/api/persona/update' : '/api/persona/create';
|
||||
const response = await axios.post(url, this.personaForm);
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(response.data.message || this.tm('messages.saveSuccess'));
|
||||
this.closePersonaDialog();
|
||||
await this.loadPersonas();
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.saveError'));
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'));
|
||||
}
|
||||
this.saving = false;
|
||||
handlePersonaSaved(message) {
|
||||
this.showSuccess(message);
|
||||
this.loadPersonas();
|
||||
},
|
||||
|
||||
async deletePersona(persona) {
|
||||
@@ -575,124 +250,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
addDialogPair() {
|
||||
this.personaForm.begin_dialogs.push('', '');
|
||||
// 自动展开预设对话面板
|
||||
if (!this.expandedPanels.includes('dialogs')) {
|
||||
this.expandedPanels.push('dialogs');
|
||||
}
|
||||
},
|
||||
|
||||
removeDialog(index) {
|
||||
// 如果是偶数索引(用户消息),删除用户消息和对应的助手消息
|
||||
if (index % 2 === 0 && index + 1 < this.personaForm.begin_dialogs.length) {
|
||||
this.personaForm.begin_dialogs.splice(index, 2);
|
||||
}
|
||||
// 如果是奇数索引(助手消息),删除助手消息和对应的用户消息
|
||||
else if (index % 2 === 1 && index - 1 >= 0) {
|
||||
this.personaForm.begin_dialogs.splice(index - 1, 2);
|
||||
}
|
||||
},
|
||||
|
||||
toggleMcpServer(server) {
|
||||
if (!server.tools || server.tools.length === 0) return;
|
||||
|
||||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||||
if (this.personaForm.tools === null) {
|
||||
// 从全选状态转换为去除该服务器工具的状态
|
||||
this.personaForm.tools = this.availableTools.map(tool => tool.name)
|
||||
.filter(toolName => !server.tools.includes(toolName));
|
||||
this.toolSelectValue = '1'; // 切换到指定工具模式
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保tools是数组
|
||||
if (!Array.isArray(this.personaForm.tools)) {
|
||||
this.personaForm.tools = [];
|
||||
this.toolSelectValue = '1';
|
||||
}
|
||||
|
||||
// 检查是否所有服务器的工具都已选中
|
||||
const serverTools = server.tools;
|
||||
const allSelected = serverTools.every(toolName => this.personaForm.tools.includes(toolName));
|
||||
|
||||
if (allSelected) {
|
||||
// 移除所有服务器工具
|
||||
this.personaForm.tools = this.personaForm.tools.filter(
|
||||
toolName => !serverTools.includes(toolName)
|
||||
);
|
||||
} else {
|
||||
// 添加所有服务器工具
|
||||
serverTools.forEach(toolName => {
|
||||
if (!this.personaForm.tools.includes(toolName)) {
|
||||
this.personaForm.tools.push(toolName);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleTool(toolName) {
|
||||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||||
if (this.personaForm.tools === null) {
|
||||
// 如果是全选状态,点击某个工具表示要取消选择该工具
|
||||
// 所以创建一个包含所有其他工具的数组
|
||||
this.personaForm.tools = this.availableTools.map(tool => tool.name).filter(name => name !== toolName);
|
||||
this.toolSelectValue = '1'; // 切换到指定工具模式
|
||||
} else if (Array.isArray(this.personaForm.tools)) {
|
||||
const index = this.personaForm.tools.indexOf(toolName);
|
||||
if (index !== -1) {
|
||||
// 如果工具已选择,移除工具
|
||||
this.personaForm.tools.splice(index, 1);
|
||||
} else {
|
||||
// 如果工具未选择,添加工具
|
||||
this.personaForm.tools.push(toolName);
|
||||
}
|
||||
} else {
|
||||
// 如果tools不是数组也不是null,初始化为数组
|
||||
this.personaForm.tools = [toolName];
|
||||
this.toolSelectValue = '1';
|
||||
}
|
||||
},
|
||||
|
||||
toggleAllTools() {
|
||||
// 如果当前是全选状态,则清空选择
|
||||
if (this.isAllToolsSelected()) {
|
||||
this.personaForm.tools = [];
|
||||
} else {
|
||||
// 否则设置为全选(null表示所有工具)
|
||||
this.personaForm.tools = null;
|
||||
}
|
||||
},
|
||||
|
||||
clearAllTools() {
|
||||
// 清空所有工具选择
|
||||
this.personaForm.tools = [];
|
||||
},
|
||||
|
||||
isAllToolsSelected() {
|
||||
// 检查是否为全选状态(tools为null)
|
||||
return this.personaForm.tools === null;
|
||||
},
|
||||
|
||||
isNoToolsSelected() {
|
||||
// 检查是否没有选择任何工具
|
||||
return Array.isArray(this.personaForm.tools) && this.personaForm.tools.length === 0;
|
||||
},
|
||||
|
||||
removeTool(toolName) {
|
||||
// 如果当前是全选状态,需要先转换为具体的工具列表
|
||||
if (this.personaForm.tools === null) {
|
||||
// 创建一个包含所有工具的数组,然后移除指定工具
|
||||
this.personaForm.tools = this.availableTools.map(tool => tool.name).filter(name => name !== toolName);
|
||||
this.toolSelectValue = '1'; // 切换到指定工具模式
|
||||
} else if (Array.isArray(this.personaForm.tools)) {
|
||||
const index = this.personaForm.tools.indexOf(toolName);
|
||||
if (index !== -1) {
|
||||
this.personaForm.tools.splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
truncateText(text, maxLength) {
|
||||
if (!text) return '';
|
||||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
||||
@@ -713,35 +270,6 @@ export default {
|
||||
this.message = message;
|
||||
this.messageType = 'error';
|
||||
this.showMessage = true;
|
||||
},
|
||||
|
||||
getDialogRules(index) {
|
||||
const dialogType = index % 2 === 0 ? this.tm('form.userMessage') : this.tm('form.assistantMessage');
|
||||
return [
|
||||
v => !!v || this.tm('validation.dialogRequired', { type: dialogType }),
|
||||
v => (v && v.trim().length > 0) || this.tm('validation.dialogRequired', { type: dialogType })
|
||||
];
|
||||
},
|
||||
|
||||
isToolSelected(toolName) {
|
||||
// 如果是全选状态,所有工具都被选中
|
||||
if (this.personaForm.tools === null) {
|
||||
return true;
|
||||
}
|
||||
return Array.isArray(this.personaForm.tools) && this.personaForm.tools.includes(toolName);
|
||||
},
|
||||
|
||||
isServerSelected(server) {
|
||||
if (!server.tools || server.tools.length === 0) return false;
|
||||
|
||||
// 如果是全选状态,所有服务器都被选中
|
||||
if (this.personaForm.tools === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查服务器的所有工具是否都已选中
|
||||
return Array.isArray(this.personaForm.tools) &&
|
||||
server.tools.every(toolName => this.personaForm.tools.includes(toolName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -791,13 +319,4 @@ export default {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tools-selection {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-virtual-scroll {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<v-container fluid class="pa-0">
|
||||
<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-connection</v-icon>{{ tm('title') }}
|
||||
<h1 class="text-h1 font-weight-bold mb-2 d-flex align-center">
|
||||
<v-icon color="black" class="me-2">mdi-robot</v-icon>{{ tm('title') }}
|
||||
</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis mb-4">
|
||||
{{ tm('subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddPlatformDialog = true"
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="updatingMode = false; showAddPlatformDialog = true"
|
||||
rounded="xl" size="x-large">
|
||||
{{ tm('addAdapter') }}
|
||||
</v-btn>
|
||||
@@ -46,7 +46,6 @@
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-0" v-if="showConsole">
|
||||
@@ -57,91 +56,15 @@
|
||||
</v-container>
|
||||
|
||||
<!-- 添加平台适配器对话框 -->
|
||||
<AddNewPlatform v-model:show="showAddPlatformDialog" :metadata="metadata"
|
||||
@select-template="selectPlatformTemplate" />
|
||||
|
||||
<!-- 配置对话框 -->
|
||||
<v-dialog v-model="showPlatformCfg" persistent width="900px" max-width="90%">
|
||||
<v-card
|
||||
:title="updatingMode ? tm('dialog.edit') : tm('dialog.add') + ` ${newSelectedPlatformName} ` + tm('dialog.adapter')">
|
||||
<v-card-text class="py-4">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<AstrBotConfig :iterable="newSelectedPlatformConfig" :metadata="metadata['platform_group']?.metadata"
|
||||
metadataKey="platform" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-2">
|
||||
<v-col cols="12" class="text-center">
|
||||
<v-btn color="info" variant="outlined" @click="openTutorial">
|
||||
<v-icon start>mdi-book-open-variant</v-icon>
|
||||
{{ tm('dialog.viewTutorial') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showPlatformCfg = false" :disabled="loading">
|
||||
{{ tm('dialog.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="newPlatform" :loading="loading">
|
||||
{{ tm('dialog.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<AddNewPlatform v-model:show="showAddPlatformDialog" :metadata="metadata" :config_data="config_data" ref="addPlatformDialog"
|
||||
:updating-mode="updatingMode" :updating-platform-config="updatingPlatformConfig" @update="getConfig"
|
||||
@show-toast="showToast" @refresh-config="getConfig"/>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
|
||||
location="top">
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
|
||||
<!-- ID冲突确认对话框 -->
|
||||
<v-dialog v-model="showIdConflictDialog" max-width="450" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h6 bg-warning d-flex align-center">
|
||||
<v-icon start class="me-2">mdi-alert-circle-outline</v-icon>
|
||||
{{ tm('dialog.idConflict.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text class="py-4 text-body-1 text-medium-emphasis">
|
||||
{{ tm('dialog.idConflict.message', { id: conflictId }) }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm')
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 安全警告对话框 -->
|
||||
<v-dialog v-model="showOneBotEmptyTokenWarnDialog" max-width="600" persistent>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ tm('dialog.securityWarning.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text class="py-4">
|
||||
<p>{{ tm('dialog.securityWarning.aiocqhttpTokenMissing') }}</p>
|
||||
<span><a
|
||||
href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7"
|
||||
target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
|
||||
</v-card-text>
|
||||
<v-card-actions class="px-4 pb-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" @click="handleOneBotEmptyTokenWarningDismiss(true)">
|
||||
无视警告并继续创建
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="handleOneBotEmptyTokenWarningDismiss(false)">
|
||||
重新修改
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -191,30 +114,17 @@ export default {
|
||||
config_data: {},
|
||||
fetched: false,
|
||||
metadata: {},
|
||||
showPlatformCfg: false,
|
||||
showAddPlatformDialog: false,
|
||||
|
||||
newSelectedPlatformName: '',
|
||||
newSelectedPlatformConfig: {},
|
||||
updatingPlatformConfig: {},
|
||||
updatingMode: false,
|
||||
|
||||
loading: false,
|
||||
|
||||
save_message_snack: false,
|
||||
save_message: "",
|
||||
save_message_success: "success",
|
||||
|
||||
showConsole: false,
|
||||
|
||||
// ID冲突确认对话框
|
||||
showIdConflictDialog: false,
|
||||
conflictId: '',
|
||||
idConflictResolve: null,
|
||||
|
||||
// OneBot Empty Token Warning #2639
|
||||
showOneBotEmptyTokenWarnDialog: false,
|
||||
oneBotEmptyTokenWarningResolve: null,
|
||||
|
||||
store: useCommonStore()
|
||||
}
|
||||
},
|
||||
@@ -251,11 +161,6 @@ export default {
|
||||
return getPlatformIcon(platform_id);
|
||||
},
|
||||
|
||||
openTutorial() {
|
||||
const tutorialUrl = getTutorialLink(this.newSelectedPlatformConfig.type);
|
||||
window.open(tutorialUrl, '_blank');
|
||||
},
|
||||
|
||||
getConfig() {
|
||||
axios.get('/api/config/get').then((res) => {
|
||||
this.config_data = res.data.data.config;
|
||||
@@ -266,134 +171,13 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 选择平台模板
|
||||
selectPlatformTemplate(name) {
|
||||
this.newSelectedPlatformName = name;
|
||||
this.showPlatformCfg = true;
|
||||
this.updatingMode = false;
|
||||
this.newSelectedPlatformConfig = JSON.parse(JSON.stringify(
|
||||
this.metadata['platform_group']?.metadata?.platform?.config_template[name] || {}
|
||||
));
|
||||
},
|
||||
|
||||
addFromDefaultConfigTmpl(index) {
|
||||
this.newSelectedPlatformName = index[0];
|
||||
this.showPlatformCfg = true;
|
||||
this.updatingMode = false;
|
||||
this.newSelectedPlatformConfig = JSON.parse(JSON.stringify(
|
||||
this.metadata['platform_group']?.metadata?.platform?.config_template[index[0]] || {}
|
||||
));
|
||||
},
|
||||
|
||||
editPlatform(platform) {
|
||||
this.newSelectedPlatformName = platform.id;
|
||||
this.newSelectedPlatformConfig = JSON.parse(JSON.stringify(platform));
|
||||
this.updatingPlatformConfig = JSON.parse(JSON.stringify(platform));
|
||||
this.updatingMode = true;
|
||||
this.showPlatformCfg = true;
|
||||
},
|
||||
|
||||
newPlatform() {
|
||||
this.loading = true;
|
||||
if (this.updatingMode) {
|
||||
if (this.newSelectedPlatformConfig.type === 'aiocqhttp') {
|
||||
const token = this.newSelectedPlatformConfig.ws_reverse_token;
|
||||
if (!token || token.trim() === '') {
|
||||
this.showOneBotEmptyTokenWarning().then((continueWithWarning) => {
|
||||
if (continueWithWarning) {
|
||||
this.updatePlatform();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.updatePlatform();
|
||||
} else {
|
||||
this.savePlatform();
|
||||
}
|
||||
},
|
||||
|
||||
updatePlatform() {
|
||||
axios.post('/api/config/platform/update', {
|
||||
id: this.newSelectedPlatformName,
|
||||
config: this.newSelectedPlatformConfig
|
||||
}).then((res) => {
|
||||
this.loading = false;
|
||||
this.showPlatformCfg = false;
|
||||
this.getConfig();
|
||||
this.showSuccess(res.data.message || this.messages.updateSuccess);
|
||||
}).catch((err) => {
|
||||
this.loading = false;
|
||||
this.showError(err.response?.data?.message || err.message);
|
||||
this.showAddPlatformDialog = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.addPlatformDialog.toggleShowConfigSection();
|
||||
});
|
||||
this.updatingMode = false;
|
||||
},
|
||||
|
||||
async savePlatform() {
|
||||
// 检查 ID 是否已存在
|
||||
const existingPlatform = this.config_data.platform?.find(p => p.id === this.newSelectedPlatformConfig.id);
|
||||
if (existingPlatform) {
|
||||
const confirmed = await this.confirmIdConflict(this.newSelectedPlatformConfig.id);
|
||||
if (!confirmed) {
|
||||
this.loading = false;
|
||||
return; // 如果用户取消,则中止保存
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 aiocqhttp 适配器的安全设置
|
||||
if (this.newSelectedPlatformConfig.type === 'aiocqhttp') {
|
||||
const token = this.newSelectedPlatformConfig.ws_reverse_token;
|
||||
if (!token || token.trim() === '') {
|
||||
const continueWithWarning = await this.showOneBotEmptyTokenWarning();
|
||||
if (!continueWithWarning) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post('/api/config/platform/new', this.newSelectedPlatformConfig);
|
||||
this.loading = false;
|
||||
this.showPlatformCfg = false;
|
||||
this.getConfig();
|
||||
this.showSuccess(res.data.message || this.messages.addSuccess);
|
||||
} catch (err) {
|
||||
this.loading = false;
|
||||
this.showError(err.response?.data?.message || err.message);
|
||||
}
|
||||
},
|
||||
|
||||
confirmIdConflict(id) {
|
||||
this.conflictId = id;
|
||||
this.showIdConflictDialog = true;
|
||||
return new Promise((resolve) => {
|
||||
this.idConflictResolve = resolve;
|
||||
});
|
||||
},
|
||||
|
||||
handleIdConflictConfirm(confirmed) {
|
||||
if (this.idConflictResolve) {
|
||||
this.idConflictResolve(confirmed);
|
||||
}
|
||||
this.showIdConflictDialog = false;
|
||||
},
|
||||
|
||||
showOneBotEmptyTokenWarning() {
|
||||
this.showOneBotEmptyTokenWarnDialog = true;
|
||||
return new Promise((resolve) => {
|
||||
this.oneBotEmptyTokenWarningResolve = resolve;
|
||||
});
|
||||
},
|
||||
|
||||
handleOneBotEmptyTokenWarningDismiss(continueWithWarning) {
|
||||
this.showOneBotEmptyTokenWarnDialog = false;
|
||||
if (this.oneBotEmptyTokenWarningResolve) {
|
||||
this.oneBotEmptyTokenWarningResolve(continueWithWarning);
|
||||
this.oneBotEmptyTokenWarningResolve = null;
|
||||
}
|
||||
|
||||
if (!continueWithWarning) {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
deletePlatform(platform) {
|
||||
@@ -422,6 +206,14 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
showToast({ message, type }) {
|
||||
if (type === 'success') {
|
||||
this.showSuccess(message);
|
||||
} else if (type === 'error') {
|
||||
this.showError(message);
|
||||
}
|
||||
},
|
||||
|
||||
showSuccess(message) {
|
||||
this.save_message = message;
|
||||
this.save_message_success = "success";
|
||||
|
||||
@@ -103,8 +103,6 @@
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-0" v-if="showStatus">
|
||||
<v-card-text class="px-4 py-3">
|
||||
@@ -158,8 +156,6 @@
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-0" v-if="showConsole">
|
||||
<ConsoleDisplayer style="background-color: #1e1e1e; height: 300px; border-radius: 0"></ConsoleDisplayer>
|
||||
@@ -234,7 +230,7 @@
|
||||
确认保存
|
||||
</v-card-title>
|
||||
<v-card-text class="py-4 text-body-1 text-medium-emphasis">
|
||||
您没有填写 API Key,确定要保存吗?这可能会导致该服务提供商无法正常工作。
|
||||
您没有填写 API Key,确定要保存吗?这可能会导致该模型无法正常工作。
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
<p>使用 /sid 指令可查看会话 ID。</p>
|
||||
<p>会话信息:</p>
|
||||
<ul>
|
||||
<li>平台: {{ item.platform }}</li>
|
||||
<li v-if="item.user_name">用户: {{ item.user_name }}</li>
|
||||
<li>机器人 ID: {{ item.platform }}</li>
|
||||
<li v-if="item.message_type">消息类型: {{ item.message_type }}</li>
|
||||
<li v-if="item.session_raw_name">会话 ID: {{ item.session_raw_name }}</li>
|
||||
<li v-if="item.user_name">用户: {{ item.user_name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-tooltip>
|
||||
|
||||
@@ -129,7 +129,7 @@ class ProviderCommands:
|
||||
)
|
||||
return
|
||||
i = 1
|
||||
ret = "下面列出了此服务提供商可用模型:"
|
||||
ret = "下面列出了此模型提供商可用模型:"
|
||||
for model in models:
|
||||
ret += f"\n{i}. {model}"
|
||||
i += 1
|
||||
|
||||
@@ -11,19 +11,26 @@ class SIDCommand:
|
||||
self.context = context
|
||||
|
||||
async def sid(self, event: AstrMessageEvent):
|
||||
"""获取会话 ID 和 管理员 ID"""
|
||||
"""获取消息来源信息"""
|
||||
sid = event.unified_msg_origin
|
||||
user_id = str(event.get_sender_id())
|
||||
ret = f"""SID: {sid} 此 ID 可用于设置会话白名单。
|
||||
/wl <SID> 添加白名单, /dwl <SID> 删除白名单。
|
||||
|
||||
UID: {user_id} 此 ID 可用于设置管理员。
|
||||
/op <UID> 授权管理员, /deop <UID> 取消管理员。"""
|
||||
umo_platform = event.session.platform_id
|
||||
umo_msg_type = event.session.message_type.value
|
||||
umo_session_id = event.session.session_id
|
||||
ret = (
|
||||
f"UMO: 「{sid}」 此值可用于设置白名单。\n"
|
||||
f"UID: 「{user_id}」 此值可用于设置管理员。\n"
|
||||
f"消息会话来源信息:\n"
|
||||
f" 机器人 ID: 「{umo_platform}」\n"
|
||||
f" 消息类型: 「{umo_msg_type}」\n"
|
||||
f" 会话 ID: 「{umo_session_id}」\n"
|
||||
f"消息来源可用于配置机器人的配置文件路由。"
|
||||
)
|
||||
|
||||
if (
|
||||
self.context.get_config()["platform_settings"]["unique_session"]
|
||||
and event.get_group_id()
|
||||
):
|
||||
ret += f"\n\n当前处于独立会话模式, 此群 ID: {event.get_group_id()}, 也可将此 ID 加入白名单来放行整个群聊。"
|
||||
ret += f"\n\n当前处于独立会话模式, 此群 ID: 「{event.get_group_id()}」, 也可将此 ID 加入白名单来放行整个群聊。"
|
||||
|
||||
event.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||
|
||||
Reference in New Issue
Block a user