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:
Soulter
2025-10-20 12:01:06 +08:00
committed by GitHub
parent e1ca645a32
commit 2c5f68e696
34 changed files with 2172 additions and 1799 deletions
+23 -51
View File
@@ -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
+24 -7
View File
@@ -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": "用户提示词",
+13 -1
View File
@@ -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")
+81
View File
@@ -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)
+79 -4
View File
@@ -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;
}
+115 -115
View File
@@ -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 {
// toolsnull
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 -3
View File
@@ -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',
+12 -639
View File
@@ -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="每行一个规则,例如:&#10;platform1:GroupMessage:*&#10;*:FriendMessage:session123&#10;*:*:*"
@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>
+3 -3
View File
@@ -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' },
+2 -2
View File
@@ -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)"
+13 -494
View File
@@ -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 {
// toolsnull
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() {
// toolsnull
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>
+19 -227
View File
@@ -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";
+1 -5
View File
@@ -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>
+1 -1
View File
@@ -129,7 +129,7 @@ class ProviderCommands:
)
return
i = 1
ret = "下面列出了此服务提供商可用模型:"
ret = "下面列出了此模型提供商可用模型:"
for model in models:
ret += f"\n{i}. {model}"
i += 1
+14 -7
View File
@@ -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))