Refactor: 重构配置文件管理,以支持更灵活的、会话粒度的(基于 umo part)配置文件隔离 (#2328)
* refactor: 重构配置文件管理,以支持更灵活的、基于 umo part 的配置文件隔离 * Refactor: 重构配置前端页面,新增数个配置项 (#2331) * refactor: 重构配置前端页面,新增数个配置项 * feat: 完善多配置文件结构 * perf: 系统配置入口 * fix: normal config item list not display * fix: 修复 axios 请求中的上下文引用问题
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
import os
|
||||
import uuid
|
||||
from astrbot.core import AstrBotConfig, logger
|
||||
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.utils.astrbot_path import get_astrbot_config_path
|
||||
from typing import TypeVar, TypedDict
|
||||
|
||||
_VT = TypeVar("_VT")
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class AstrBotConfigManager:
|
||||
"""A class to manage the system configuration of AstrBot, aka ACM"""
|
||||
|
||||
def __init__(self, default_config: AstrBotConfig, sp: SharedPreferences):
|
||||
self.sp = sp
|
||||
self.confs: dict[str, AstrBotConfig] = {}
|
||||
"""uuid / "default" -> AstrBotConfig"""
|
||||
self.confs["default"] = default_config
|
||||
self._load_all_configs()
|
||||
|
||||
def _load_all_configs(self):
|
||||
"""Load all configurations from the shared preferences."""
|
||||
abconf_data = self.sp.get("abconf_mapping", {})
|
||||
for uuid_, meta in abconf_data.items():
|
||||
filename = meta["path"]
|
||||
conf_path = os.path.join(get_astrbot_config_path(), filename)
|
||||
if os.path.exists(conf_path):
|
||||
conf = AstrBotConfig(config_path=conf_path)
|
||||
self.confs[uuid_] = conf
|
||||
else:
|
||||
logger.warning(
|
||||
f"Config file {conf_path} for UUID {uuid_} does not exist, skipping."
|
||||
)
|
||||
continue
|
||||
|
||||
def _is_umo_match(self, p1: str, p2: str) -> bool:
|
||||
"""判断 p2 umo 是否逻辑包含于 p1 umo"""
|
||||
p1 = p1.split(":")
|
||||
p2 = p2.split(":")
|
||||
|
||||
if len(p1) != 3 or len(p2) != 3:
|
||||
return False # 非法格式
|
||||
|
||||
return all(p == "" or p == t for p, t in zip(p1, p2))
|
||||
|
||||
def _load_conf_mapping(self, umo: str | MessageSession) -> ConfInfo:
|
||||
"""获取指定 umo 的配置文件 uuid, 如果不存在则返回默认配置(返回 "default")
|
||||
|
||||
Returns:
|
||||
ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型
|
||||
"""
|
||||
# uuid -> { "umop": list, "path": str, "name": str }
|
||||
abconf_data = self.sp.get("abconf_mapping", {}) # default is not included here
|
||||
if isinstance(umo, MessageSession):
|
||||
umo = str(umo)
|
||||
else:
|
||||
umo = str(MessageSession.from_str(umo)) # validate
|
||||
|
||||
for uuid_, meta in abconf_data.items():
|
||||
for pattern in meta["umop"]:
|
||||
if self._is_umo_match(pattern, umo):
|
||||
return ConfInfo(**meta, id=uuid_)
|
||||
|
||||
return DEFAULT_CONFIG_CONF_INFO
|
||||
|
||||
def _save_conf_mapping(
|
||||
self,
|
||||
abconf_path: str,
|
||||
abconf_id: str,
|
||||
umo_parts: list[str] | list[MessageSession],
|
||||
abconf_name: str = 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", {})
|
||||
random_word = abconf_name or uuid.uuid4().hex[:8]
|
||||
abconf_data[abconf_id] = {
|
||||
"umop": umo_parts,
|
||||
"path": abconf_path,
|
||||
"name": random_word,
|
||||
}
|
||||
self.sp.put("abconf_mapping", abconf_data)
|
||||
|
||||
def get_conf(self, umo: str | MessageSession) -> AstrBotConfig:
|
||||
"""获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。"""
|
||||
if isinstance(umo, MessageSession):
|
||||
umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}"
|
||||
|
||||
uuid_ = self._load_conf_mapping(umo)["id"]
|
||||
|
||||
conf = self.confs.get(uuid_)
|
||||
if not conf:
|
||||
conf = self.confs["default"] # default MUST exists
|
||||
|
||||
return conf
|
||||
|
||||
@property
|
||||
def default_conf(self) -> AstrBotConfig:
|
||||
"""获取默认配置文件"""
|
||||
return self.confs["default"]
|
||||
|
||||
def get_conf_info(self, umo: str | MessageSession) -> ConfInfo:
|
||||
"""获取指定 umo 的配置文件元数据"""
|
||||
if isinstance(umo, MessageSession):
|
||||
umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}"
|
||||
|
||||
return self._load_conf_mapping(umo)
|
||||
|
||||
def get_conf_list(self) -> list[ConfInfo]:
|
||||
"""获取所有配置文件的元数据列表"""
|
||||
conf_list = []
|
||||
conf_list.append(DEFAULT_CONFIG_CONF_INFO)
|
||||
for uuid_, meta in self.sp.get("abconf_mapping", {}).items():
|
||||
conf_list.append(ConfInfo(**meta, id=uuid_))
|
||||
return conf_list
|
||||
|
||||
def create_conf(
|
||||
self,
|
||||
umo_parts: list[str] | list[MessageSession],
|
||||
config: dict = DEFAULT_CONFIG,
|
||||
name: str = 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.confs[conf_uuid] = conf
|
||||
return conf_uuid
|
||||
|
||||
def delete_conf(self, conf_id: str) -> bool:
|
||||
"""删除指定配置文件
|
||||
|
||||
Args:
|
||||
conf_id: 配置文件的 UUID
|
||||
|
||||
Returns:
|
||||
bool: 删除是否成功
|
||||
|
||||
Raises:
|
||||
ValueError: 如果试图删除默认配置文件
|
||||
"""
|
||||
if conf_id == "default":
|
||||
raise ValueError("不能删除默认配置文件")
|
||||
|
||||
# 从映射中移除
|
||||
abconf_data = self.sp.get("abconf_mapping", {})
|
||||
if conf_id not in abconf_data:
|
||||
logger.warning(f"配置文件 {conf_id} 不存在于映射中")
|
||||
return False
|
||||
|
||||
# 获取配置文件路径
|
||||
conf_path = os.path.join(get_astrbot_config_path(), abconf_data[conf_id]["path"])
|
||||
|
||||
# 删除配置文件
|
||||
try:
|
||||
if os.path.exists(conf_path):
|
||||
os.remove(conf_path)
|
||||
logger.info(f"已删除配置文件: {conf_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"删除配置文件 {conf_path} 失败: {e}")
|
||||
return False
|
||||
|
||||
# 从内存中移除
|
||||
if conf_id in self.confs:
|
||||
del self.confs[conf_id]
|
||||
|
||||
# 从映射中移除
|
||||
del abconf_data[conf_id]
|
||||
self.sp.put("abconf_mapping", abconf_data)
|
||||
|
||||
logger.info(f"成功删除配置文件 {conf_id}")
|
||||
return True
|
||||
|
||||
def update_conf_info(self, conf_id: str, name: str = None, umo_parts: list[str] = None) -> bool:
|
||||
"""更新配置文件信息
|
||||
|
||||
Args:
|
||||
conf_id: 配置文件的 UUID
|
||||
name: 新的配置文件名称 (可选)
|
||||
umo_parts: 新的 UMO 部分列表 (可选)
|
||||
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
"""
|
||||
if conf_id == "default":
|
||||
raise ValueError("不能更新默认配置文件的信息")
|
||||
|
||||
abconf_data = self.sp.get("abconf_mapping", {})
|
||||
if conf_id not in abconf_data:
|
||||
logger.warning(f"配置文件 {conf_id} 不存在于映射中")
|
||||
return False
|
||||
|
||||
# 更新名称
|
||||
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)
|
||||
logger.info(f"成功更新配置文件 {conf_id} 的信息")
|
||||
return True
|
||||
|
||||
def g(self, umo: str = None, key: str = None, default: _VT = None) -> _VT:
|
||||
"""获取配置项。umo 为 None 时使用默认配置"""
|
||||
if umo is None:
|
||||
return self.confs["default"].get(key, default)
|
||||
conf = self.get_conf(umo)
|
||||
return conf.get(key, default)
|
||||
+484
-154
File diff suppressed because it is too large
Load Diff
@@ -27,10 +27,11 @@ from astrbot.core.provider.manager import ProviderManager
|
||||
from astrbot.core import LogBroker
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.updator import AstrBotUpdator
|
||||
from astrbot.core import logger
|
||||
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.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
|
||||
from astrbot.core.db.migration.helper import do_migration_v4
|
||||
@@ -76,11 +77,16 @@ class AstrBotCoreLifecycle:
|
||||
except Exception as e:
|
||||
logger.error(f"迁移到 v4.0.0 新版本数据格式失败: {e}")
|
||||
|
||||
# 初始化 AstrBot 配置管理器
|
||||
self.astrbot_config_mgr = AstrBotConfigManager(
|
||||
default_config=self.astrbot_config, sp=sp
|
||||
)
|
||||
|
||||
# 初始化事件队列
|
||||
self.event_queue = Queue()
|
||||
|
||||
# 初始化人格管理器
|
||||
self.persona_mgr = PersonaManager(self.db, self.astrbot_config)
|
||||
self.persona_mgr = PersonaManager(self.db, self.astrbot_config_mgr)
|
||||
await self.persona_mgr.initialize()
|
||||
|
||||
# 初始化供应商管理器
|
||||
@@ -107,6 +113,7 @@ class AstrBotCoreLifecycle:
|
||||
self.conversation_manager,
|
||||
self.platform_message_history_manager,
|
||||
self.persona_mgr,
|
||||
self.astrbot_config_mgr,
|
||||
)
|
||||
|
||||
# 初始化插件管理器
|
||||
@@ -119,17 +126,16 @@ class AstrBotCoreLifecycle:
|
||||
await self.provider_manager.initialize()
|
||||
|
||||
# 初始化消息事件流水线调度器
|
||||
self.pipeline_scheduler = PipelineScheduler(
|
||||
PipelineContext(self.astrbot_config, self.plugin_manager)
|
||||
)
|
||||
await self.pipeline_scheduler.initialize()
|
||||
self.star_context.pipeline_ctx = self.pipeline_scheduler.ctx
|
||||
|
||||
self.pipeline_scheduler_mapping = await self.load_pipeline_scheduler()
|
||||
|
||||
# 初始化更新器
|
||||
self.astrbot_updator = AstrBotUpdator()
|
||||
|
||||
# 初始化事件总线
|
||||
self.event_bus = EventBus(self.event_queue, self.pipeline_scheduler)
|
||||
self.event_bus = EventBus(
|
||||
self.event_queue, self.pipeline_scheduler_mapping, self.astrbot_config_mgr
|
||||
)
|
||||
|
||||
# 记录启动时间
|
||||
self.start_time = int(time.time())
|
||||
@@ -252,3 +258,33 @@ class AstrBotCoreLifecycle:
|
||||
)
|
||||
)
|
||||
return tasks
|
||||
|
||||
async def load_pipeline_scheduler(self) -> dict[str, PipelineScheduler]:
|
||||
"""加载消息事件流水线调度器
|
||||
|
||||
Returns:
|
||||
dict[str, PipelineScheduler]: 平台 ID 到流水线调度器的映射
|
||||
"""
|
||||
mapping = {}
|
||||
for conf_id, ab_config in self.astrbot_config_mgr.confs.items():
|
||||
scheduler = PipelineScheduler(
|
||||
PipelineContext(ab_config, self.plugin_manager, conf_id)
|
||||
)
|
||||
await scheduler.initialize()
|
||||
mapping[conf_id] = scheduler
|
||||
return mapping
|
||||
|
||||
async def reload_pipeline_scheduler(self, conf_id: str):
|
||||
"""重新加载消息事件流水线调度器
|
||||
|
||||
Returns:
|
||||
dict[str, PipelineScheduler]: 平台 ID 到流水线调度器的映射
|
||||
"""
|
||||
ab_config = self.astrbot_config_mgr.confs.get(conf_id)
|
||||
if not ab_config:
|
||||
raise ValueError(f"配置文件 {conf_id} 不存在")
|
||||
scheduler = PipelineScheduler(
|
||||
PipelineContext(ab_config, self.plugin_manager, conf_id)
|
||||
)
|
||||
await scheduler.initialize()
|
||||
self.pipeline_scheduler_mapping[conf_id] = scheduler
|
||||
|
||||
+19
-17
@@ -16,30 +16,32 @@ from asyncio import Queue
|
||||
from astrbot.core.pipeline.scheduler import PipelineScheduler
|
||||
from astrbot.core import logger
|
||||
from .platform import AstrMessageEvent
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
|
||||
|
||||
class EventBus:
|
||||
"""事件总线: 用于处理事件的分发和处理
|
||||
"""用于处理事件的分发和处理"""
|
||||
|
||||
维护一个异步队列, 来接受各种消息事件
|
||||
"""
|
||||
|
||||
def __init__(self, event_queue: Queue, pipeline_scheduler: PipelineScheduler):
|
||||
def __init__(
|
||||
self,
|
||||
event_queue: Queue,
|
||||
pipeline_scheduler_mapping: dict[str, PipelineScheduler],
|
||||
astrbot_config_mgr: AstrBotConfigManager = None,
|
||||
):
|
||||
self.event_queue = event_queue # 事件队列
|
||||
self.pipeline_scheduler = pipeline_scheduler # 管道调度器
|
||||
# abconf uuid -> scheduler
|
||||
self.pipeline_scheduler_mapping = pipeline_scheduler_mapping
|
||||
self.astrbot_config_mgr = astrbot_config_mgr
|
||||
|
||||
async def dispatch(self):
|
||||
"""无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑"""
|
||||
while True:
|
||||
event: AstrMessageEvent = (
|
||||
await self.event_queue.get()
|
||||
) # 从事件队列中获取新的事件
|
||||
self._print_event(event) # 打印日志
|
||||
asyncio.create_task(
|
||||
self.pipeline_scheduler.execute(event)
|
||||
) # 创建新的异步任务来执行管道调度器的处理逻辑
|
||||
event: AstrMessageEvent = await self.event_queue.get()
|
||||
conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin)
|
||||
self._print_event(event, conf_info["name"])
|
||||
scheduler = self.pipeline_scheduler_mapping.get(conf_info["id"])
|
||||
asyncio.create_task(scheduler.execute(event))
|
||||
|
||||
def _print_event(self, event: AstrMessageEvent):
|
||||
def _print_event(self, event: AstrMessageEvent, conf_name: str):
|
||||
"""用于记录事件信息
|
||||
|
||||
Args:
|
||||
@@ -48,10 +50,10 @@ class EventBus:
|
||||
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
||||
if event.get_sender_name():
|
||||
logger.info(
|
||||
f"[{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_name()}/{event.get_sender_id()}: {event.get_message_outline()}"
|
||||
f"[{conf_name}] [{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_name()}/{event.get_sender_id()}: {event.get_message_outline()}"
|
||||
)
|
||||
# 没有发送者名称: [平台名] 发送者ID: 消息概要
|
||||
else:
|
||||
logger.info(
|
||||
f"[{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_id()}: {event.get_message_outline()}"
|
||||
f"[{conf_name}] [{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_id()}: {event.get_message_outline()}"
|
||||
)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.po import Persona, Personality
|
||||
from astrbot.core.config import AstrBotConfig
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot import logger
|
||||
|
||||
|
||||
class PersonaManager:
|
||||
def __init__(self, db_helper: BaseDatabase, astrbot_config: AstrBotConfig):
|
||||
def __init__(self, db_helper: BaseDatabase, acm: AstrBotConfigManager):
|
||||
self.db = db_helper
|
||||
self.config = astrbot_config
|
||||
_ps: dict = astrbot_config["provider_settings"]
|
||||
self.default_persona: str = _ps.get("default_personality", "default")
|
||||
default_ps = acm.default_conf.get("provider_settings", {})
|
||||
self.default_persona: str = default_ps.get("default_personality", "default")
|
||||
self.personas: list[Persona] = []
|
||||
self.selected_default_persona: Persona | None = None
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import inspect
|
||||
import traceback
|
||||
import typing as T
|
||||
from dataclasses import dataclass
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.config import AstrBotConfig
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.star import PluginManager
|
||||
from astrbot.api import logger
|
||||
@@ -17,6 +17,7 @@ class PipelineContext:
|
||||
|
||||
astrbot_config: AstrBotConfig # AstrBot 配置对象
|
||||
plugin_manager: PluginManager # 插件管理器对象
|
||||
astrbot_config_id: str
|
||||
|
||||
async def call_event_hook(
|
||||
self,
|
||||
|
||||
@@ -197,7 +197,7 @@ class ToolLoopAgent(BaseAgentRunner):
|
||||
logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")
|
||||
executor = func_tool.execute(
|
||||
event=self.event,
|
||||
pipeline_context=self.pipeline_ctx,
|
||||
call_handler_func=self.pipeline_ctx.call_handler,
|
||||
**func_tool_args,
|
||||
)
|
||||
async for resp in executor:
|
||||
|
||||
@@ -3,7 +3,7 @@ import asyncio
|
||||
import re
|
||||
import hashlib
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing import List, Union, Optional, AsyncGenerator
|
||||
|
||||
from astrbot.core.db.po import Conversation
|
||||
@@ -23,32 +23,7 @@ from astrbot.core.provider.entities import ProviderRequest
|
||||
from astrbot.core.utils.metrics import Metric
|
||||
from .astrbot_message import AstrBotMessage, Group
|
||||
from .platform_metadata import PlatformMetadata
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageSession:
|
||||
"""描述一条消息在 AstrBot 中对应的会话的唯一标识。
|
||||
如果您需要实例化 MessageSession,请不要给 platform_id 赋值(或者同时给 platform_name 和 platform_id 赋值相同值)。它会在 __post_init__ 中自动设置为 platform_name 的值。"""
|
||||
|
||||
platform_name: str
|
||||
"""平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。"""
|
||||
message_type: MessageType
|
||||
session_id: str
|
||||
platform_id: str = None
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.platform_id}:{self.message_type.value}:{self.session_id}"
|
||||
|
||||
def __post_init__(self):
|
||||
self.platform_id = self.platform_name
|
||||
|
||||
@staticmethod
|
||||
def from_str(session_str: str):
|
||||
platform_id, message_type, session_id = session_str.split(":")
|
||||
return MessageSession(platform_id, MessageType(message_type), session_id)
|
||||
|
||||
|
||||
MessageSesion = MessageSession # back compatibility
|
||||
from .message_session import MessageSession, MessageSesion # noqa
|
||||
|
||||
|
||||
class AstrMessageEvent(abc.ABC):
|
||||
|
||||
@@ -18,6 +18,9 @@ class PlatformManager:
|
||||
|
||||
self.platforms_config = config["platform"]
|
||||
self.settings = config["platform_settings"]
|
||||
"""NOTE: 这里是 default 的配置文件,以保证最大的兼容性;
|
||||
这个配置中的 unique_session 需要特殊处理,
|
||||
约定整个项目中对 unique_session 的引用都从 default 的配置中获取"""
|
||||
self.event_queue = event_queue
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageSession:
|
||||
"""描述一条消息在 AstrBot 中对应的会话的唯一标识。
|
||||
如果您需要实例化 MessageSession,请不要给 platform_id 赋值(或者同时给 platform_name 和 platform_id 赋值相同值)。它会在 __post_init__ 中自动设置为 platform_name 的值。"""
|
||||
|
||||
platform_name: str
|
||||
"""平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。"""
|
||||
message_type: MessageType
|
||||
session_id: str
|
||||
platform_id: str = None
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.platform_id}:{self.message_type.value}:{self.session_id}"
|
||||
|
||||
def __post_init__(self):
|
||||
self.platform_id = self.platform_name
|
||||
|
||||
@staticmethod
|
||||
def from_str(session_str: str):
|
||||
platform_id, message_type, session_id = session_str.split(":")
|
||||
return MessageSession(platform_id, MessageType(message_type), session_id)
|
||||
|
||||
|
||||
MessageSesion = MessageSession # back compatibility
|
||||
@@ -5,7 +5,7 @@ from asyncio import Queue
|
||||
from .platform_metadata import PlatformMetadata
|
||||
from .astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from .astr_message_event import MessageSesion
|
||||
from .message_session import MessageSesion
|
||||
from astrbot.core.utils.metrics import Metric
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ from typing_extensions import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.pipeline.context import PipelineContext
|
||||
|
||||
|
||||
DEFAULT_MCP_CONFIG = {"mcpServers": {}}
|
||||
@@ -80,14 +79,14 @@ class FunctionTool:
|
||||
async def execute(
|
||||
self,
|
||||
event: AstrMessageEvent = None,
|
||||
pipeline_context: "PipelineContext" = None,
|
||||
call_handler_func: Awaitable = None,
|
||||
**tool_args,
|
||||
) -> AsyncGenerator[Any | mcp.types.CallToolResult, None]:
|
||||
"""执行函数调用。
|
||||
|
||||
Args:
|
||||
event (AstrMessageEvent): 事件对象, 当 origin 为 local 时必须提供。
|
||||
pipeline_context (PipelineContext): 流水线调度器上下文, 当 origin 为 local 时必须提供。
|
||||
call_handler_func (AsyncGenerator): 用于调用处理函数的异步生成器, 当 origin 为 mcp 时必须提供。
|
||||
**kwargs: 函数调用的参数。
|
||||
|
||||
Returns:
|
||||
@@ -96,7 +95,7 @@ class FunctionTool:
|
||||
if self.origin == "local":
|
||||
if not event:
|
||||
raise ValueError("Event must be provided for local function tools.")
|
||||
wrapper = pipeline_context.call_handler(
|
||||
wrapper = call_handler_func(
|
||||
event=event,
|
||||
handler=self.handler,
|
||||
**tool_args,
|
||||
@@ -207,7 +206,7 @@ class ToolSet:
|
||||
"""Get all function tools."""
|
||||
return self.get_tool(name)
|
||||
|
||||
def openai_schema(self, omit_empty_parameters: bool = False) -> List[Dict]:
|
||||
def openai_schema(self, omit_empty_parameter_field: bool = False) -> List[Dict]:
|
||||
"""Convert tools to OpenAI API function calling schema format."""
|
||||
result = []
|
||||
for tool in self.tools:
|
||||
@@ -219,7 +218,7 @@ class ToolSet:
|
||||
},
|
||||
}
|
||||
|
||||
if tool.parameters.get("properties") or not omit_empty_parameters:
|
||||
if tool.parameters.get("properties") or not omit_empty_parameter_field:
|
||||
func_def["function"]["parameters"] = tool.parameters
|
||||
|
||||
result.append(func_def)
|
||||
@@ -319,8 +318,8 @@ class ToolSet:
|
||||
return declarations
|
||||
|
||||
@deprecated(reason="Use openai_schema() instead", version="4.0.0")
|
||||
def get_func_desc_openai_style(self, omit_empty_parameters: bool = False):
|
||||
return self.openai_schema(omit_empty_parameters)
|
||||
def get_func_desc_openai_style(self, omit_empty_parameter_field: bool = False):
|
||||
return self.openai_schema(omit_empty_parameter_field)
|
||||
|
||||
@deprecated(reason="Use anthropic_schema() instead", version="4.0.0")
|
||||
def get_func_desc_anthropic_style(self):
|
||||
@@ -817,7 +816,7 @@ class FunctionToolManager:
|
||||
"""
|
||||
tools = [f for f in self.func_list if f.active]
|
||||
toolset = ToolSet(tools)
|
||||
return toolset.openai_schema(omit_empty_parameters=omit_empty_parameter_field)
|
||||
return toolset.openai_schema(omit_empty_parameter_field=omit_empty_parameter_field)
|
||||
|
||||
def get_func_desc_anthropic_style(self) -> list:
|
||||
"""
|
||||
|
||||
@@ -82,7 +82,7 @@ class ProviderManager:
|
||||
"""
|
||||
if provider_id not in self.inst_map:
|
||||
raise ValueError(f"提供商 {provider_id} 不存在,无法设置。")
|
||||
if umo and self.provider_settings["separate_provider"]:
|
||||
if umo:
|
||||
perf = sp.get("session_provider_perf", {})
|
||||
session_perf = perf.get(umo, {})
|
||||
session_perf[provider_type.value] = provider_id
|
||||
|
||||
@@ -18,6 +18,7 @@ from astrbot.core.provider.manager import ProviderManager
|
||||
from astrbot.core.platform import Platform
|
||||
from astrbot.core.platform.manager import PlatformManager
|
||||
from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryManager
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.persona_mgr import PersonaManager
|
||||
from .star import star_registry, StarMetadata, star_map
|
||||
from .star_handler import star_handlers_registry, StarHandlerMetadata, EventType
|
||||
@@ -31,11 +32,6 @@ from astrbot.core.star.filter.platform_adapter_type import (
|
||||
)
|
||||
from deprecated import deprecated
|
||||
|
||||
from typing_extensions import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from astrbot.core.pipeline.context import PipelineContext
|
||||
|
||||
|
||||
class Context:
|
||||
"""
|
||||
@@ -57,8 +53,6 @@ class Context:
|
||||
|
||||
registered_web_apis: list = []
|
||||
|
||||
pipeline_ctx: "PipelineContext" = None
|
||||
|
||||
# back compatibility
|
||||
_register_tasks: List[Awaitable] = []
|
||||
_star_manager = None
|
||||
@@ -73,6 +67,7 @@ class Context:
|
||||
conversation_manager: ConversationManager = None,
|
||||
message_history_manager: PlatformMessageHistoryManager = None,
|
||||
persona_manager: PersonaManager = None,
|
||||
astrbot_config_mgr: AstrBotConfigManager = None,
|
||||
):
|
||||
self._event_queue = event_queue
|
||||
self._config = config
|
||||
@@ -82,6 +77,7 @@ class Context:
|
||||
self.conversation_manager = conversation_manager
|
||||
self.message_history_manager = message_history_manager
|
||||
self.persona_manager = persona_manager
|
||||
self.astrbot_config_mgr = astrbot_config_mgr
|
||||
|
||||
def get_registered_star(self, star_name: str) -> StarMetadata:
|
||||
"""根据插件名获取插件的 Metadata"""
|
||||
@@ -145,7 +141,7 @@ class Context:
|
||||
Args:
|
||||
umo(str): unified_message_origin 值,如果传入并且用户启用了提供商会话隔离,则使用该会话偏好的提供商。
|
||||
"""
|
||||
if umo and self._config["provider_settings"]["separate_provider"]:
|
||||
if umo:
|
||||
perf = sp.get("session_provider_perf", {})
|
||||
prov_id = perf.get(umo, {}).get(ProviderType.CHAT_COMPLETION.value, None)
|
||||
if inst := self.provider_manager.inst_map.get(prov_id, None):
|
||||
@@ -159,7 +155,7 @@ class Context:
|
||||
Args:
|
||||
umo(str): unified_message_origin 值,如果传入,则使用该会话偏好的提供商。
|
||||
"""
|
||||
if umo and self._config["provider_settings"]["separate_provider"]:
|
||||
if umo:
|
||||
perf = sp.get("session_provider_perf", {})
|
||||
prov_id = perf.get(umo, {}).get(ProviderType.TEXT_TO_SPEECH.value, None)
|
||||
if inst := self.provider_manager.inst_map.get(prov_id, None):
|
||||
@@ -173,16 +169,20 @@ class Context:
|
||||
Args:
|
||||
umo(str): unified_message_origin 值,如果传入,则使用该会话偏好的提供商。
|
||||
"""
|
||||
if umo and self._config["provider_settings"]["separate_provider"]:
|
||||
if umo:
|
||||
perf = sp.get("session_provider_perf", {})
|
||||
prov_id = perf.get(umo, {}).get(ProviderType.SPEECH_TO_TEXT.value, None)
|
||||
if inst := self.provider_manager.inst_map.get(prov_id, None):
|
||||
return inst
|
||||
return self.provider_manager.curr_stt_provider_inst
|
||||
|
||||
def get_config(self) -> AstrBotConfig:
|
||||
def get_config(self, umo: str = None) -> AstrBotConfig:
|
||||
"""获取 AstrBot 的配置。"""
|
||||
return self._config
|
||||
if not umo:
|
||||
# using default config
|
||||
return self._config
|
||||
else:
|
||||
return self.astrbot_config_mgr.get_conf(umo)
|
||||
|
||||
def get_db(self) -> BaseDatabase:
|
||||
"""获取 AstrBot 数据库。"""
|
||||
@@ -257,9 +257,6 @@ class Context:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_pipeline_context(self) -> "PipelineContext":
|
||||
return self.pipeline_ctx
|
||||
|
||||
"""
|
||||
以下的方法已经不推荐使用。请从 AstrBot 文档查看更好的注册方式。
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,12 @@ import os
|
||||
from .route import Route, Response, RouteContext
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from quart import request
|
||||
from astrbot.core.config.default import CONFIG_METADATA_2, DEFAULT_VALUE_MAP
|
||||
from astrbot.core.config.default import (
|
||||
CONFIG_METADATA_2,
|
||||
DEFAULT_VALUE_MAP,
|
||||
CONFIG_METADATA_3,
|
||||
CONFIG_METADATA_3_SYSTEM,
|
||||
)
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
@@ -159,32 +164,119 @@ class ConfigRoute(Route):
|
||||
super().__init__(context)
|
||||
self.core_lifecycle = core_lifecycle
|
||||
self.config: AstrBotConfig = core_lifecycle.astrbot_config
|
||||
self.acm = core_lifecycle.astrbot_config_mgr
|
||||
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/get": ("GET", self.get_configs),
|
||||
"/config/astrbot/update": ("POST", self.post_astrbot_configs),
|
||||
"/config/plugin/update": ("POST", self.post_plugin_configs),
|
||||
"/config/platform/new": ("POST", self.post_new_platform),
|
||||
"/config/platform/update": ("POST", self.post_update_platform),
|
||||
"/config/platform/delete": ("POST", self.post_delete_platform),
|
||||
"/config/platform/list": ("GET", self.get_platform_list),
|
||||
"/config/provider/new": ("POST", self.post_new_provider),
|
||||
"/config/provider/update": ("POST", self.post_update_provider),
|
||||
"/config/provider/delete": ("POST", self.post_delete_provider),
|
||||
"/config/provider/check_one": ("GET", self.check_one_provider_status),
|
||||
"/config/provider/list": ("GET", self.get_provider_config_list),
|
||||
"/config/provider/model_list": ("GET", self.get_provider_model_list),
|
||||
"/config/provider/get_session_seperate": (
|
||||
"GET",
|
||||
lambda: Response()
|
||||
.ok({"enable": self.config["provider_settings"]["separate_provider"]})
|
||||
.__dict__,
|
||||
),
|
||||
"/config/provider/set_session_seperate": (
|
||||
"POST",
|
||||
self.post_session_seperate,
|
||||
),
|
||||
}
|
||||
self.register_routes()
|
||||
|
||||
async def get_abconf_list(self):
|
||||
"""获取所有 AstrBot 配置文件的列表"""
|
||||
abconf_list = self.acm.get_conf_list()
|
||||
return Response().ok({"info_list": abconf_list}).__dict__
|
||||
|
||||
async def create_abconf(self):
|
||||
"""创建新的 AstrBot 配置文件"""
|
||||
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)
|
||||
|
||||
try:
|
||||
conf_id = self.acm.create_conf(umo_parts=umo_parts, name=name)
|
||||
return Response().ok(message="创建成功", data={"conf_id": conf_id}).__dict__
|
||||
except ValueError as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def get_abconf(self):
|
||||
"""获取指定 AstrBot 配置文件"""
|
||||
abconf_id = request.args.get("id")
|
||||
system_config = request.args.get("system_config", "0").lower() == "1"
|
||||
if not abconf_id and not system_config:
|
||||
return Response().error("缺少配置文件 ID").__dict__
|
||||
|
||||
try:
|
||||
if system_config:
|
||||
abconf = self.acm.confs["default"]
|
||||
return (
|
||||
Response()
|
||||
.ok({"config": abconf, "metadata": CONFIG_METADATA_3_SYSTEM})
|
||||
.__dict__
|
||||
)
|
||||
abconf = self.acm.confs[abconf_id]
|
||||
return (
|
||||
Response()
|
||||
.ok({"config": abconf, "metadata": CONFIG_METADATA_3})
|
||||
.__dict__
|
||||
)
|
||||
except ValueError as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def delete_abconf(self):
|
||||
"""删除指定 AstrBot 配置文件"""
|
||||
post_data = await request.json
|
||||
if not post_data:
|
||||
return Response().error("缺少配置数据").__dict__
|
||||
|
||||
conf_id = post_data.get("id")
|
||||
if not conf_id:
|
||||
return Response().error("缺少配置文件 ID").__dict__
|
||||
|
||||
try:
|
||||
success = self.acm.delete_conf(conf_id)
|
||||
if success:
|
||||
return Response().ok(message="删除成功").__dict__
|
||||
else:
|
||||
return Response().error("删除失败").__dict__
|
||||
except ValueError as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"删除配置文件失败: {str(e)}").__dict__
|
||||
|
||||
async def update_abconf(self):
|
||||
"""更新指定 AstrBot 配置文件信息"""
|
||||
post_data = await request.json
|
||||
if not post_data:
|
||||
return Response().error("缺少配置数据").__dict__
|
||||
|
||||
conf_id = post_data.get("id")
|
||||
if not conf_id:
|
||||
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)
|
||||
if success:
|
||||
return Response().ok(message="更新成功").__dict__
|
||||
else:
|
||||
return Response().error("更新失败").__dict__
|
||||
except ValueError as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"更新配置文件失败: {str(e)}").__dict__
|
||||
|
||||
async def _test_single_provider(self, provider):
|
||||
"""辅助函数:测试单个 provider 的可用性"""
|
||||
meta = provider.meta()
|
||||
@@ -209,11 +301,16 @@ class ConfigRoute(Route):
|
||||
response = await asyncio.wait_for(
|
||||
provider.text_chat(prompt="REPLY `PONG` ONLY"), timeout=45.0
|
||||
)
|
||||
logger.debug(f"Received response from {status_info['name']}: {response}")
|
||||
logger.debug(
|
||||
f"Received response from {status_info['name']}: {response}"
|
||||
)
|
||||
if response is not None:
|
||||
status_info["status"] = "available"
|
||||
response_text_snippet = ""
|
||||
if hasattr(response, "completion_text") and response.completion_text:
|
||||
if (
|
||||
hasattr(response, "completion_text")
|
||||
and response.completion_text
|
||||
):
|
||||
response_text_snippet = (
|
||||
response.completion_text[:70] + "..."
|
||||
if len(response.completion_text) > 70
|
||||
@@ -232,29 +329,48 @@ class ConfigRoute(Route):
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'"
|
||||
)
|
||||
else:
|
||||
status_info["error"] = "Test call returned None, but expected an LLMResponse object."
|
||||
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.")
|
||||
status_info["error"] = (
|
||||
"Test call returned None, but expected an LLMResponse object."
|
||||
)
|
||||
logger.warning(
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None."
|
||||
)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
status_info["error"] = "Connection timed out after 45 seconds during test call."
|
||||
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.")
|
||||
status_info["error"] = (
|
||||
"Connection timed out after 45 seconds during test call."
|
||||
)
|
||||
logger.warning(
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) timed out."
|
||||
)
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
status_info["error"] = error_message
|
||||
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}")
|
||||
logger.debug(f"Traceback for {status_info['name']}:\n{traceback.format_exc()}")
|
||||
logger.warning(
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}"
|
||||
)
|
||||
logger.debug(
|
||||
f"Traceback for {status_info['name']}:\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
elif provider_capability_type == ProviderType.EMBEDDING:
|
||||
try:
|
||||
# For embedding, we can call the get_embedding method with a short prompt.
|
||||
embedding_result = await provider.get_embedding("health_check")
|
||||
if isinstance(embedding_result, list) and (not embedding_result or isinstance(embedding_result[0], float)):
|
||||
if isinstance(embedding_result, list) and (
|
||||
not embedding_result or isinstance(embedding_result[0], float)
|
||||
):
|
||||
status_info["status"] = "available"
|
||||
else:
|
||||
status_info["status"] = "unavailable"
|
||||
status_info["error"] = f"Embedding test failed: unexpected result type {type(embedding_result)}"
|
||||
status_info["error"] = (
|
||||
f"Embedding test failed: unexpected result type {type(embedding_result)}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing embedding provider {provider_name}: {e}", exc_info=True)
|
||||
logger.error(
|
||||
f"Error testing embedding provider {provider_name}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
status_info["status"] = "unavailable"
|
||||
status_info["error"] = f"Embedding test failed: {str(e)}"
|
||||
|
||||
@@ -266,41 +382,71 @@ class ConfigRoute(Route):
|
||||
status_info["status"] = "available"
|
||||
else:
|
||||
status_info["status"] = "unavailable"
|
||||
status_info["error"] = f"TTS test failed: unexpected result type {type(audio_result)}"
|
||||
status_info["error"] = (
|
||||
f"TTS test failed: unexpected result type {type(audio_result)}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing TTS provider {provider_name}: {e}", exc_info=True)
|
||||
logger.error(
|
||||
f"Error testing TTS provider {provider_name}: {e}", exc_info=True
|
||||
)
|
||||
status_info["status"] = "unavailable"
|
||||
status_info["error"] = f"TTS test failed: {str(e)}"
|
||||
elif provider_capability_type == ProviderType.SPEECH_TO_TEXT:
|
||||
try:
|
||||
logger.debug(f"Sending health check audio to provider: {status_info['name']}")
|
||||
sample_audio_path = os.path.join(get_astrbot_path(), "samples", "stt_health_check.wav")
|
||||
logger.debug(
|
||||
f"Sending health check audio to provider: {status_info['name']}"
|
||||
)
|
||||
sample_audio_path = os.path.join(
|
||||
get_astrbot_path(), "samples", "stt_health_check.wav"
|
||||
)
|
||||
if not os.path.exists(sample_audio_path):
|
||||
status_info["status"] = "unavailable"
|
||||
status_info["error"] = "STT test failed: sample audio file not found."
|
||||
logger.warning(f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}")
|
||||
status_info["error"] = (
|
||||
"STT test failed: sample audio file not found."
|
||||
)
|
||||
logger.warning(
|
||||
f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}"
|
||||
)
|
||||
else:
|
||||
text_result = await provider.get_text(sample_audio_path)
|
||||
if isinstance(text_result, str) and text_result:
|
||||
status_info["status"] = "available"
|
||||
snippet = text_result[:70] + "..." if len(text_result) > 70 else text_result
|
||||
logger.info(f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'")
|
||||
snippet = (
|
||||
text_result[:70] + "..."
|
||||
if len(text_result) > 70
|
||||
else text_result
|
||||
)
|
||||
logger.info(
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'"
|
||||
)
|
||||
else:
|
||||
status_info["status"] = "unavailable"
|
||||
status_info["error"] = f"STT test failed: unexpected result type {type(text_result)}"
|
||||
logger.warning(f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}")
|
||||
status_info["error"] = (
|
||||
f"STT test failed: unexpected result type {type(text_result)}"
|
||||
)
|
||||
logger.warning(
|
||||
f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing STT provider {provider_name}: {e}", exc_info=True)
|
||||
logger.error(
|
||||
f"Error testing STT provider {provider_name}: {e}", exc_info=True
|
||||
)
|
||||
status_info["status"] = "unavailable"
|
||||
status_info["error"] = f"STT test failed: {str(e)}"
|
||||
else:
|
||||
logger.debug(f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}")
|
||||
logger.debug(
|
||||
f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}"
|
||||
)
|
||||
status_info["status"] = "available"
|
||||
status_info["error"] = "This provider type is not tested and is assumed to be available."
|
||||
status_info["error"] = (
|
||||
"This provider type is not tested and is assumed to be available."
|
||||
)
|
||||
|
||||
return status_info
|
||||
|
||||
def _error_response(self, message: str, status_code: int = 500, log_fn=logger.error):
|
||||
def _error_response(
|
||||
self, message: str, status_code: int = 500, log_fn=logger.error
|
||||
):
|
||||
log_fn(message)
|
||||
# 记录更详细的traceback信息,但只在是严重错误时
|
||||
if status_code == 500:
|
||||
@@ -311,7 +457,9 @@ class ConfigRoute(Route):
|
||||
"""API: check a single LLM Provider's status by id"""
|
||||
provider_id = request.args.get("id")
|
||||
if not provider_id:
|
||||
return self._error_response("Missing provider_id parameter", 400, logger.warning)
|
||||
return self._error_response(
|
||||
"Missing provider_id parameter", 400, logger.warning
|
||||
)
|
||||
|
||||
logger.info(f"API call: /config/provider/check_one id={provider_id}")
|
||||
try:
|
||||
@@ -319,16 +467,21 @@ class ConfigRoute(Route):
|
||||
target = prov_mgr.inst_map.get(provider_id)
|
||||
|
||||
if not target:
|
||||
logger.warning(f"Provider with id '{provider_id}' not found in provider_manager.")
|
||||
return Response().error(f"Provider with id '{provider_id}' not found").__dict__
|
||||
logger.warning(
|
||||
f"Provider with id '{provider_id}' not found in provider_manager."
|
||||
)
|
||||
return (
|
||||
Response()
|
||||
.error(f"Provider with id '{provider_id}' not found")
|
||||
.__dict__
|
||||
)
|
||||
|
||||
result = await self._test_single_provider(target)
|
||||
return Response().ok(result).__dict__
|
||||
|
||||
except Exception as e:
|
||||
return self._error_response(
|
||||
f"Critical error checking provider {provider_id}: {e}",
|
||||
500
|
||||
f"Critical error checking provider {provider_id}: {e}", 500
|
||||
)
|
||||
|
||||
async def get_configs(self):
|
||||
@@ -339,21 +492,6 @@ class ConfigRoute(Route):
|
||||
return Response().ok(await self._get_astrbot_config()).__dict__
|
||||
return Response().ok(await self._get_plugin_config(plugin_name)).__dict__
|
||||
|
||||
async def post_session_seperate(self):
|
||||
"""设置提供商会话隔离"""
|
||||
post_config = await request.json
|
||||
enable = post_config.get("enable", None)
|
||||
if enable is None:
|
||||
return Response().error("缺少参数 enable").__dict__
|
||||
|
||||
astrbot_config = self.core_lifecycle.astrbot_config
|
||||
astrbot_config["provider_settings"]["separate_provider"] = enable
|
||||
try:
|
||||
astrbot_config.save_config()
|
||||
except Exception as e:
|
||||
return Response().error(str(e)).__dict__
|
||||
return Response().ok(None, "设置成功~").__dict__
|
||||
|
||||
async def get_provider_config_list(self):
|
||||
provider_type = request.args.get("provider_type", None)
|
||||
if not provider_type:
|
||||
@@ -387,11 +525,21 @@ class ConfigRoute(Route):
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def get_platform_list(self):
|
||||
"""获取所有平台的列表"""
|
||||
platform_list = []
|
||||
for platform in self.config["platform"]:
|
||||
platform_list.append(platform)
|
||||
return Response().ok({"platforms": platform_list}).__dict__
|
||||
|
||||
async def post_astrbot_configs(self):
|
||||
post_configs = await request.json
|
||||
data = await request.json
|
||||
config = data.get("config", None)
|
||||
conf_id = data.get("conf_id", None)
|
||||
try:
|
||||
await self._save_astrbot_configs(post_configs)
|
||||
return Response().ok(None, "保存成功~ 机器人正在重载配置。").__dict__
|
||||
await self._save_astrbot_configs(config, conf_id)
|
||||
await self.core_lifecycle.reload_pipeline_scheduler(conf_id)
|
||||
return Response().ok(None, "保存成功~").__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(str(e)).__dict__
|
||||
@@ -550,10 +698,12 @@ class ConfigRoute(Route):
|
||||
|
||||
return ret
|
||||
|
||||
async def _save_astrbot_configs(self, post_configs: dict):
|
||||
async def _save_astrbot_configs(self, post_configs: dict, conf_id: str = None):
|
||||
try:
|
||||
save_config(post_configs, self.config, is_core=True)
|
||||
await self.core_lifecycle.restart()
|
||||
if conf_id not in self.acm.confs:
|
||||
raise ValueError(f"配置文件 {conf_id} 不存在")
|
||||
astrbot_config = self.acm.confs[conf_id]
|
||||
save_config(post_configs, astrbot_config, is_core=True)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
@@ -286,14 +286,6 @@ class PluginRoute(Route):
|
||||
f"{filter.parent_command_names[0]} {filter.command_name}"
|
||||
)
|
||||
info["cmd"] = info["cmd"].strip()
|
||||
if (
|
||||
self.core_lifecycle.astrbot_config["wake_prefix"]
|
||||
and len(self.core_lifecycle.astrbot_config["wake_prefix"])
|
||||
> 0
|
||||
):
|
||||
info["cmd"] = (
|
||||
f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}"
|
||||
)
|
||||
elif isinstance(filter, CommandGroupFilter):
|
||||
info["type"] = "指令组"
|
||||
info["cmd"] = filter.get_complete_command_names()[0]
|
||||
@@ -301,14 +293,6 @@ class PluginRoute(Route):
|
||||
info["sub_command"] = filter.print_cmd_tree(
|
||||
filter.sub_command_filters
|
||||
)
|
||||
if (
|
||||
self.core_lifecycle.astrbot_config["wake_prefix"]
|
||||
and len(self.core_lifecycle.astrbot_config["wake_prefix"])
|
||||
> 0
|
||||
):
|
||||
info["cmd"] = (
|
||||
f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}"
|
||||
)
|
||||
elif isinstance(filter, RegexFilter):
|
||||
info["type"] = "正则匹配"
|
||||
info["cmd"] = filter.regex_str
|
||||
|
||||
@@ -176,7 +176,7 @@ function saveEditedContent() {
|
||||
<!-- List item -->
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
|
||||
:value="iterable[key]"
|
||||
v-model="iterable[key]"
|
||||
class="config-field"
|
||||
/>
|
||||
</div>
|
||||
@@ -287,9 +287,9 @@ function saveEditedContent() {
|
||||
></v-switch>
|
||||
|
||||
<!-- List item -->
|
||||
<ListConfigItem
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
|
||||
:value="iterable[metadataKey]"
|
||||
v-model="iterable[metadataKey]"
|
||||
class="config-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
<script setup>
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { ref, computed } from 'vue'
|
||||
import ListConfigItem from './ListConfigItem.vue'
|
||||
import ProviderSelector from './ProviderSelector.vue'
|
||||
import PersonaSelector from './PersonaSelector.vue'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
metadata: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
iterable: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
metadataKey: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const dialog = ref(false)
|
||||
const currentEditingKey = ref('')
|
||||
const currentEditingLanguage = ref('json')
|
||||
const currentEditingTheme = ref('vs-light')
|
||||
let currentEditingKeyIterable = null
|
||||
|
||||
function getValueBySelector(obj, selector) {
|
||||
const keys = selector.split('.')
|
||||
let current = obj
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
current = current[key]
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
function setValueBySelector(obj, selector, value) {
|
||||
const keys = selector.split('.')
|
||||
let current = obj
|
||||
|
||||
// 创建嵌套对象路径
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i]
|
||||
if (!current[key] || typeof current[key] !== 'object') {
|
||||
current[key] = {}
|
||||
}
|
||||
current = current[key]
|
||||
}
|
||||
|
||||
// 设置最终值
|
||||
current[keys[keys.length - 1]] = value
|
||||
}
|
||||
|
||||
// 创建一个计算属性来处理 JSON selector 的获取和设置
|
||||
function createSelectorModel(selector) {
|
||||
return computed({
|
||||
get() {
|
||||
return getValueBySelector(props.iterable, selector)
|
||||
},
|
||||
set(value) {
|
||||
setValueBySelector(props.iterable, selector, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openEditorDialog(key, value, theme, language) {
|
||||
currentEditingKey.value = key
|
||||
currentEditingLanguage.value = language || 'json'
|
||||
currentEditingTheme.value = theme || 'vs-light'
|
||||
currentEditingKeyIterable = value
|
||||
dialog.value = true
|
||||
}
|
||||
|
||||
function saveEditedContent() {
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function shouldShowItem(itemMeta, itemKey) {
|
||||
if (!itemMeta?.condition) {
|
||||
return true
|
||||
}
|
||||
for (const [conditionKey, expectedValue] of Object.entries(itemMeta.condition)) {
|
||||
const actualValue = getValueBySelector(props.iterable, conditionKey)
|
||||
if (actualValue !== expectedValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
const itemEntries = Object.entries(items)
|
||||
|
||||
// 检查当前索引之后是否还有可见的配置项
|
||||
for (let i = currentIndex + 1; i < itemEntries.length; i++) {
|
||||
const [itemKey, itemMeta] = itemEntries[i]
|
||||
if (shouldShowItem(itemMeta, itemKey)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
<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-list-item-title class="config-title">
|
||||
{{ metadata[metadataKey]?.description }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
{{ metadata[metadataKey]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Object Type Configuration with JSON Selector Support -->
|
||||
<div v-if="metadata[metadataKey]?.type === 'object'" class="object-config">
|
||||
<div v-for="(itemMeta, itemKey, index) in metadata[metadataKey].items" :key="itemKey" class="config-item">
|
||||
<!-- Check if itemKey is a JSON selector -->
|
||||
<template v-if="shouldShowItem(itemMeta, itemKey)">
|
||||
<!-- JSON Selector Property -->
|
||||
<v-row v-if="!itemMeta?.invisible" class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ itemMeta?.description || itemKey }}
|
||||
<span class="property-key">({{ itemKey }})</span>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint">‼️</span>
|
||||
{{ itemMeta?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="config-input">
|
||||
<div class="w-100" v-if="!itemMeta?._special">
|
||||
<!-- Select input for JSON selector -->
|
||||
<v-select v-if="itemMeta?.options" v-model="createSelectorModel(itemKey).value"
|
||||
:items="itemMeta?.options" :disabled="itemMeta?.readonly" density="compact" variant="outlined"
|
||||
class="config-field" hide-details></v-select>
|
||||
|
||||
<!-- Code Editor for JSON selector -->
|
||||
<div v-else-if="itemMeta?.editor_mode" class="editor-container">
|
||||
<VueMonacoEditor :theme="itemMeta?.editor_theme || 'vs-light'"
|
||||
:language="itemMeta?.editor_language || 'json'"
|
||||
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
|
||||
v-model:value="createSelectorModel(itemKey).value">
|
||||
</VueMonacoEditor>
|
||||
<v-btn icon size="small" variant="text" color="primary" class="editor-fullscreen-btn"
|
||||
@click="openEditorDialog(itemKey, iterable, itemMeta?.editor_theme, itemMeta?.editor_language)"
|
||||
:title="t('core.common.editor.fullscreen')">
|
||||
<v-icon>mdi-fullscreen</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- String input for JSON selector -->
|
||||
<v-text-field v-else-if="itemMeta?.type === 'string'" v-model="createSelectorModel(itemKey).value"
|
||||
density="compact" variant="outlined" class="config-field" hide-details></v-text-field>
|
||||
|
||||
<!-- Numeric input for JSON selector -->
|
||||
<v-text-field v-else-if="itemMeta?.type === 'int' || itemMeta?.type === 'float'"
|
||||
v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined" class="config-field"
|
||||
type="number" hide-details></v-text-field>
|
||||
|
||||
<!-- Text area for JSON selector -->
|
||||
<v-textarea v-else-if="itemMeta?.type === 'text'" v-model="createSelectorModel(itemKey).value"
|
||||
variant="outlined" rows="3" class="config-field" hide-details></v-textarea>
|
||||
|
||||
<!-- Boolean switch for JSON selector -->
|
||||
<v-switch v-else-if="itemMeta?.type === 'bool'" v-model="createSelectorModel(itemKey).value"
|
||||
color="primary" inset density="compact" hide-details style="display: flex; justify-content: end;"></v-switch>
|
||||
|
||||
<!-- List item for JSON selector -->
|
||||
<ListConfigItem
|
||||
v-else-if="itemMeta?.type === 'list'"
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
button-text="修改"
|
||||
class="config-field"
|
||||
/>
|
||||
|
||||
<!-- Fallback for JSON selector -->
|
||||
<v-text-field v-else v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined"
|
||||
class="config-field" hide-details></v-text-field>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider'">
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'chat_completion'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider_stt'">
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'speech_to_text'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider_tts'">
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'text_to_speech'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'provider_pool'">
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'chat_completion'"
|
||||
button-text="选择提供商池..."
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_persona'">
|
||||
<PersonaSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'persona_pool'">
|
||||
<PersonaSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
button-text="选择人格池..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-divider class="config-divider" v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"></v-divider>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- Full Screen Editor Dialog -->
|
||||
<v-dialog v-model="dialog" fullscreen transition="dialog-bottom-transition" scrollable>
|
||||
<v-card>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-btn icon @click="dialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>{{ t('core.common.editor.editingTitle') }} - {{ currentEditingKey }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items>
|
||||
<v-btn variant="text" @click="saveEditedContent">{{ t('core.common.save') }}</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<VueMonacoEditor :theme="currentEditingTheme" :language="currentEditingLanguage"
|
||||
style="height: calc(100vh - 64px);" v-model:value="currentEditingKeyIterable[currentEditingKey]">
|
||||
</VueMonacoEditor>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.config-section {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
/* font-weight: 600; */
|
||||
font-size: 1.3rem;
|
||||
color: var(--v-theme-primaryText);
|
||||
}
|
||||
|
||||
.config-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--v-theme-secondaryText);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.metadata-key,
|
||||
.property-key {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
font-weight: normal;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.important-hint {
|
||||
opacity: 1;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.object-config,
|
||||
.simple-config {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nested-object {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.nested-container {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.config-row {
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
padding: 10px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.config-row:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.property-info {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.property-name {
|
||||
font-size: 0.875rem;
|
||||
/* font-weight: 600; */
|
||||
color: var(--v-theme-primaryText);
|
||||
}
|
||||
|
||||
.property-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--v-theme-secondaryText);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.type-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.config-input {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.config-divider {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-fullscreen-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 10;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.editor-fullscreen-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.nested-object {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.property-info,
|
||||
.type-indicator,
|
||||
.config-input {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,135 +1,216 @@
|
||||
<template>
|
||||
<div class="list-config-item">
|
||||
<v-list dense style="background-color: transparent;max-height: 300px; overflow-y: auto;">
|
||||
<v-list-item v-for="(item, index) in items" :key="index">
|
||||
<v-list-item-content style="display: flex; justify-content: space-between;">
|
||||
<v-list-item-title v-if="editIndex !== index">
|
||||
<v-chip size="small" label color="primary">{{ item }}</v-chip>
|
||||
</v-list-item-title>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
暂无项目
|
||||
</span>
|
||||
<div v-else class="d-flex flex-wrap ga-2">
|
||||
<v-chip v-for="item in displayItems" :key="item" size="x-small" label color="primary">
|
||||
{{ item.length > 20 ? item.slice(0, 20) + '...' : item }}
|
||||
</v-chip>
|
||||
<v-chip v-if="modelValue.length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
|
||||
+{{ modelValue.length - maxDisplayItems }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
{{ buttonText }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- List Management Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
{{ dialogTitle }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<v-list v-if="localItems.length > 0" density="compact">
|
||||
<v-list-item
|
||||
v-for="(item, index) in localItems"
|
||||
:key="index"
|
||||
rounded="md"
|
||||
class="ma-1">
|
||||
<v-list-item-title v-if="editIndex !== index">
|
||||
{{ item }}
|
||||
</v-list-item-title>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="editItem"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@keyup.enter="saveEdit"
|
||||
@keyup.esc="cancelEdit"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
|
||||
<template v-slot:append>
|
||||
<div v-if="editIndex !== index" class="d-flex">
|
||||
<v-btn @click="startEdit(index, item)" variant="plain" icon size="small">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn @click="removeItem(index)" variant="plain" icon size="small">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div v-else class="d-flex">
|
||||
<v-btn @click="saveEdit" variant="plain" color="success" icon size="small">
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
<v-btn @click="cancelEdit" variant="plain" color="error" icon size="small">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-format-list-bulleted</v-icon>
|
||||
<p class="text-grey mt-4">暂无项目</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add new item section -->
|
||||
<v-card-text class="pa-4">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="editItem"
|
||||
dense
|
||||
hide-details
|
||||
v-model="newItem"
|
||||
:label="t('core.common.list.addItemPlaceholder')"
|
||||
@keyup.enter="addItem"
|
||||
clearable
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@keyup.enter="saveEdit"
|
||||
@keyup.esc="cancelEdit"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<div v-if="editIndex !== index">
|
||||
<v-btn @click="startEdit(index, item)" variant="plain" class="edit-btn" icon size="small">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn @click="removeItem(index)" variant="plain" icon size="small">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-btn @click="saveEdit" variant="plain" color="success" icon size="small">
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
<v-btn @click="cancelEdit" variant="plain" color="error" icon size="small">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<v-text-field v-model="newItem" :label="t('core.common.list.addItemPlaceholder')" @keyup.enter="addItem" clearable dense hide-details
|
||||
variant="outlined" density="compact"></v-text-field>
|
||||
<v-btn @click="addItem" text variant="tonal">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
{{ t('core.common.list.addButton') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
class="flex-grow-1">
|
||||
</v-text-field>
|
||||
<v-btn @click="addItem" variant="tonal" color="primary">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
{{ t('core.common.list.addButton') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
</div>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelDialog">取消</v-btn>
|
||||
<v-btn color="primary" @click="confirmDialog">确认</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
|
||||
export default {
|
||||
name: 'ListConfigItem',
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
return { t };
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newItem: '',
|
||||
items: this.value,
|
||||
editIndex: -1,
|
||||
editItem: '',
|
||||
};
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '修改'
|
||||
},
|
||||
watch: {
|
||||
items(newVal) {
|
||||
this.$emit('input', newVal);
|
||||
},
|
||||
dialogTitle: {
|
||||
type: String,
|
||||
default: '修改列表项'
|
||||
},
|
||||
methods: {
|
||||
addItem() {
|
||||
if (this.newItem.trim() !== '') {
|
||||
this.items.push(this.newItem.trim());
|
||||
this.newItem = '';
|
||||
}
|
||||
},
|
||||
removeItem(index) {
|
||||
this.items.splice(index, 1);
|
||||
},
|
||||
startEdit(index, item) {
|
||||
this.editIndex = index;
|
||||
this.editItem = item;
|
||||
},
|
||||
saveEdit() {
|
||||
if (this.editItem.trim() !== '') {
|
||||
this.items[this.editIndex] = this.editItem.trim();
|
||||
this.cancelEdit();
|
||||
}
|
||||
},
|
||||
cancelEdit() {
|
||||
this.editIndex = -1;
|
||||
this.editItem = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
maxDisplayItems: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialog = ref(false)
|
||||
const localItems = ref([])
|
||||
const originalItems = ref([])
|
||||
const newItem = ref('')
|
||||
const editIndex = ref(-1)
|
||||
const editItem = ref('')
|
||||
|
||||
// 计算要显示的项目
|
||||
const displayItems = computed(() => {
|
||||
return props.modelValue.slice(0, props.maxDisplayItems)
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化,同步到 localItems
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
localItems.value = [...(newValue || [])]
|
||||
}, { immediate: true })
|
||||
|
||||
function openDialog() {
|
||||
localItems.value = [...(props.modelValue || [])]
|
||||
originalItems.value = [...(props.modelValue || [])]
|
||||
dialog.value = true
|
||||
editIndex.value = -1
|
||||
editItem.value = ''
|
||||
newItem.value = ''
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
if (newItem.value.trim() !== '') {
|
||||
localItems.value.push(newItem.value.trim())
|
||||
newItem.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem(index) {
|
||||
localItems.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function startEdit(index, item) {
|
||||
editIndex.value = index
|
||||
editItem.value = item
|
||||
}
|
||||
|
||||
function saveEdit() {
|
||||
if (editItem.value.trim() !== '') {
|
||||
localItems.value[editIndex.value] = editItem.value.trim()
|
||||
cancelEdit()
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editIndex.value = -1
|
||||
editItem.value = ''
|
||||
}
|
||||
|
||||
function confirmDialog() {
|
||||
emit('update:modelValue', [...localItems.value])
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function cancelDialog() {
|
||||
localItems.value = [...originalItems.value]
|
||||
editIndex.value = -1
|
||||
editItem.value = ''
|
||||
newItem.value = ''
|
||||
dialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-config-item {
|
||||
border: 1px solid var(--v-theme-border);
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--v-theme-background);
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
padding: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.v-list-item-title {
|
||||
font-size: 14px;
|
||||
.v-list-item:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
margin-right: -8px;
|
||||
.v-chip {
|
||||
margin: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
|
||||
未选择
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ modelValue }}
|
||||
</span>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
{{ buttonText }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Persona Selection Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
选择人格
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" 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">
|
||||
<v-list-item
|
||||
v-for="persona in personaList"
|
||||
:key="persona.persona_id"
|
||||
:value="persona.persona_id"
|
||||
@click="selectPersona(persona)"
|
||||
:active="selectedPersona === persona.persona_id"
|
||||
rounded="md"
|
||||
class="ma-1">
|
||||
<v-list-item-title>{{ persona.persona_id }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ persona.system_prompt ? persona.system_prompt.substring(0, 50) + '...' : '无描述' }}
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="selectedPersona === persona.persona_id" color="primary">mdi-check-circle</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div v-else-if="!loading && personaList.length === 0" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-account-off</v-icon>
|
||||
<p class="text-grey mt-4">暂无可用的人格</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection"
|
||||
:disabled="!selectedPersona">
|
||||
确认选择
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '选择人格...'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialog = ref(false)
|
||||
const personaList = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedPersona = ref('')
|
||||
|
||||
// 监听 modelValue 变化,同步到 selectedPersona
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
selectedPersona.value = newValue || ''
|
||||
}, { immediate: true })
|
||||
|
||||
async function openDialog() {
|
||||
selectedPersona.value = props.modelValue || ''
|
||||
dialog.value = true
|
||||
await loadPersonas()
|
||||
}
|
||||
|
||||
async function loadPersonas() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/persona/list')
|
||||
if (response.data.status === 'ok') {
|
||||
personaList.value = response.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载人格列表失败:', error)
|
||||
personaList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectPersona(persona) {
|
||||
selectedPersona.value = persona.persona_id
|
||||
}
|
||||
|
||||
function confirmSelection() {
|
||||
emit('update:modelValue', selectedPersona.value)
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function cancelSelection() {
|
||||
selectedPersona.value = props.modelValue || ''
|
||||
dialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-list-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.v-list-item:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
}
|
||||
|
||||
.v-list-item.v-list-item--active {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
|
||||
未选择
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ modelValue }}
|
||||
</span>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
{{ buttonText }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Provider Selection Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
选择提供商
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
|
||||
<v-list v-if="!loading && providerList.length > 0" density="compact">
|
||||
<v-list-item
|
||||
v-for="provider in providerList"
|
||||
:key="provider.id"
|
||||
:value="provider.id"
|
||||
@click="selectProvider(provider)"
|
||||
:active="selectedProvider === provider.id"
|
||||
rounded="md"
|
||||
class="ma-1">
|
||||
<v-list-item-title>{{ provider.id }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ provider.type || provider.provider_type || '未知类型' }}
|
||||
<span v-if="provider.model_config?.model">- {{ provider.model_config.model }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="selectedProvider === provider.id" color="primary">mdi-check-circle</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div v-else-if="!loading && providerList.length === 0" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">暂无可用的提供商</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection"
|
||||
:disabled="!selectedProvider">
|
||||
确认选择
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
providerType: {
|
||||
type: String,
|
||||
default: 'chat_completion'
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '选择提供商...'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialog = ref(false)
|
||||
const providerList = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedProvider = ref('')
|
||||
|
||||
// 监听 modelValue 变化,同步到 selectedProvider
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
selectedProvider.value = newValue || ''
|
||||
}, { immediate: true })
|
||||
|
||||
async function openDialog() {
|
||||
selectedProvider.value = props.modelValue || ''
|
||||
dialog.value = true
|
||||
await loadProviders()
|
||||
}
|
||||
|
||||
async function loadProviders() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/config/provider/list', {
|
||||
params: {
|
||||
provider_type: props.providerType
|
||||
}
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
providerList.value = response.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载提供商列表失败:', error)
|
||||
providerList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectProvider(provider) {
|
||||
selectedProvider.value = provider.id
|
||||
}
|
||||
|
||||
function confirmSelection() {
|
||||
emit('update:modelValue', selectedProvider.value)
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function cancelSelection() {
|
||||
selectedProvider.value = props.modelValue || ''
|
||||
dialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-list-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.v-list-item:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
}
|
||||
|
||||
.v-list-item.v-list-item--active {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
</style>
|
||||
@@ -30,7 +30,11 @@
|
||||
"configApplyError": "配置未应用,Json 格式错误。",
|
||||
"saveSuccess": "配置保存成功",
|
||||
"saveError": "配置保存失败",
|
||||
"loadError": "配置加载失败"
|
||||
"loadError": "配置加载失败",
|
||||
"deleteSuccess": "删除成功",
|
||||
"deleteError": "删除失败",
|
||||
"updateSuccess": "更新成功",
|
||||
"updateError": "更新失败"
|
||||
},
|
||||
"sections": {
|
||||
"general": "常规设置",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, shallowRef, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { useCustomizerStore } from '../../../stores/customizer';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import sidebarItems from './sidebarItem';
|
||||
|
||||
+549
-142
@@ -1,124 +1,175 @@
|
||||
|
||||
|
||||
<template>
|
||||
<v-card style="margin-bottom: 16px;">
|
||||
<v-card-text style="padding: 0;">
|
||||
<div>
|
||||
<v-btn-group variant="outlined" divided>
|
||||
<v-btn icon="mdi-text-box-edit-outline" style="width: 80px;" :color="editorTab === 0 ? 'primary' : ''"
|
||||
@click="editorTab = 0">
|
||||
|
||||
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||
<div v-if="selectedConfigID || isSystemConfig" class="mt-4 config-panel"
|
||||
style="display: flex; flex-direction: column; align-items: start;">
|
||||
|
||||
<!-- 普通配置选择区域 -->
|
||||
<div class="d-flex flex-row pr-4"
|
||||
style="margin-bottom: 16px; align-items: center; gap: 12px; justify-content: space-between; width: 100%;">
|
||||
<div class="d-flex flex-row align-center" style="gap: 12px;" >
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<v-btn-toggle v-model="configType" mandatory color="primary" variant="outlined" density="comfortable"
|
||||
rounded="md" @update:model-value="onConfigTypeToggle">
|
||||
<v-btn value="normal" prepend-icon="mdi-cog" size="large" >
|
||||
普通
|
||||
</v-btn>
|
||||
<v-btn icon="mdi-code-json" style="width: 80px;" :color="editorTab === 1 ? 'primary' : ''"
|
||||
@click="configToString(); editorTab = 1;"></v-btn>
|
||||
</v-btn-group>
|
||||
<v-btn v-if="editorTab === 1" style="margin-left: 16px;" size="small" @click="configToString()">{{ tm('editor.revertCode') }}</v-btn>
|
||||
<v-btn v-if="editorTab === 1 && config_data_has_changed" style="margin-left: 16px;" size="small"
|
||||
@click="applyStrConfig()">{{ tm('editor.applyConfig') }}</v-btn>
|
||||
<small v-if="editorTab === 1" style="margin-left: 16px;">💡 {{ tm('editor.applyTip') }}</small>
|
||||
<v-btn value="system" prepend-icon="mdi-cog-outline" size="large">
|
||||
系统
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-progress-linear v-if="!fetched" indeterminate color="primary"></v-progress-linear>
|
||||
|
||||
<!-- 可视化编辑 -->
|
||||
<v-card v-if="editorTab === 0">
|
||||
<v-tabs v-model="tab" align-tabs="left" color="deep-purple-accent-4">
|
||||
<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">
|
||||
<v-tabs-window-item v-for="(val, key, index) in metadata" v-show="index == tab" :key="index">
|
||||
<v-container fluid>
|
||||
<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 v-for="(val2, key2, index2) in metadata[key]['metadata']">
|
||||
<!-- <h3>{{ metadata[key]['metadata'][key2]['description'] }}</h3> -->
|
||||
<div v-if="metadata[key]['metadata'][key2]?.config_template"
|
||||
v-show="key2 !== 'platform' && key2 !== 'provider'" style="border: 1px solid var(--v-theme-border); padding: 8px; margin-bottom: 16px; border-radius: 10px">
|
||||
<!-- 带有 config_template 的配置项 -->
|
||||
<v-list-item-title style="font-weight: bold;">
|
||||
{{ metadata[key]['metadata'][key2]['description'] }} ({{ key2 }})
|
||||
</v-list-item-title>
|
||||
<v-tabs style="margin-top: 16px;" align-tabs="left" color="deep-purple-accent-4"
|
||||
|
||||
v-model="config_template_tab">
|
||||
<v-tab v-if="metadata[key]['metadata'][key2]?.tmpl_display_title"
|
||||
v-for="(item, index) in config_data[key2]" :key="index" :value="index">
|
||||
{{ item[metadata[key]['metadata'][key2]?.tmpl_display_title] }}
|
||||
</v-tab>
|
||||
<v-tab v-else v-for="(item, index) in config_data[key2]" :key="index + '_'" :value="index">
|
||||
{{ item.id }}({{ item.type }})
|
||||
</v-tab>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn variant="plain" size="large" v-bind="props">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list @update:selected="addFromDefaultConfigTmpl($event, key, key2)">
|
||||
<v-list-item v-for="(item, index) in metadata[key]['metadata'][key2]?.config_template" :key="index"
|
||||
:value="index">
|
||||
<v-list-item-title>{{ index }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-tabs>
|
||||
<v-tabs-window v-model="config_template_tab">
|
||||
<v-tabs-window-item v-for="(config_item, index) in config_data[key2]"
|
||||
v-show="config_template_tab === index" :key="index" :value="index">
|
||||
<div style="padding: 16px;">
|
||||
<v-btn variant="tonal" rounded="xl" color="error" @click="deleteItem(key2, index)">
|
||||
{{ tm('actions.delete') }}
|
||||
</v-btn>
|
||||
|
||||
<AstrBotConfig :metadata="metadata[key]['metadata']" :iterable="config_item" :metadataKey="key2">
|
||||
</AstrBotConfig>
|
||||
</div>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
<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>
|
||||
|
||||
<div v-else>
|
||||
<!-- 如果配置项是一个 object,那么 iterable 需要取到这个 object 的值,否则取到整个 config_data -->
|
||||
<div v-if="metadata[key]['metadata'][key2]['type'] == 'object'" style="border: 1px solid var(--v-theme-border); padding: 8px; margin-bottom: 16px; border-radius: 10px">
|
||||
<AstrBotConfig
|
||||
:metadata="metadata[key]['metadata']" :iterable="config_data[key2]" :metadataKey="key2">
|
||||
</AstrBotConfig>
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
|
||||
<v-btn icon="mdi-content-save" size="x-large" style="position: fixed; right: 52px; bottom: 52px;"
|
||||
color="darkprimary" @click="updateConfig">
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon="mdi-code-json" size="x-large" style="position: fixed; right: 52px; bottom: 124px;" color="primary"
|
||||
@click="configToString(); codeEditorDialog = true">
|
||||
</v-btn>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Full Screen Editor Dialog -->
|
||||
<v-dialog v-model="codeEditorDialog" fullscreen transition="dialog-bottom-transition" scrollable>
|
||||
<v-card>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-btn icon @click="codeEditorDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>编辑配置文件</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-items style="display: flex; align-items: center;">
|
||||
<v-btn style="margin-left: 16px;" size="small" @click="configToString()">{{
|
||||
tm('editor.revertCode') }}</v-btn>
|
||||
<v-btn v-if="config_data_has_changed" style="margin-left: 16px;" size="small" @click="applyStrConfig()">{{
|
||||
tm('editor.applyConfig') }}</v-btn>
|
||||
<small style="margin-left: 16px;">💡 {{ tm('editor.applyTip') }}</small>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<VueMonacoEditor language="json" theme="vs-dark" style="height: calc(100vh - 64px);"
|
||||
v-model:value="config_data_str">
|
||||
</VueMonacoEditor>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Config Management Dialog -->
|
||||
<v-dialog v-model="configManageDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center justify-space-between">
|
||||
<span class="text-h4">配置文件管理</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="configManageDialog = false"></v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<small>AstrBot 支持针对不同消息平台实例分别设置配置文件。默认会使用 `default` 配置。</small>
|
||||
<div class="mt-6 mb-4">
|
||||
<v-btn prepend-icon="mdi-plus" @click="startCreateConfig" variant="tonal" color="primary">
|
||||
新建配置文件
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 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"
|
||||
@click="startEditConfig(config)"></v-btn>
|
||||
<v-btn icon="mdi-delete" size="small" variant="text" color="error"
|
||||
@click="confirmDeleteConfig(config)"></v-btn>
|
||||
</div>
|
||||
<AstrBotConfig v-else :metadata="metadata[key]['metadata']" :iterable="config_data" :metadataKey="key2">
|
||||
</AstrBotConfig>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<!-- Create/Edit Form -->
|
||||
<v-divider v-if="showConfigForm" class="my-6"></v-divider>
|
||||
|
||||
<div v-if="showConfigForm">
|
||||
<h3 class="mb-4">{{ isEditingConfig ? '编辑配置文件' : '新建配置文件' }}</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<small v-if="conflictMessage">⚠ {{ conflictMessage }}</small>
|
||||
</div>
|
||||
|
||||
<v-text-field v-model="configFormData.name" label="配置文件名称" variant="outlined" class="mb-4"
|
||||
hide-details></v-text-field>
|
||||
|
||||
<v-select v-model="configFormData.umop" :items="platformList" item-title="id" item-value="id" label="应用于平台"
|
||||
variant="outlined" hide-details multiple @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-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>
|
||||
</v-card>
|
||||
|
||||
<!-- 代码编辑 -->
|
||||
<v-card v-else style="background-color: #1e1e1e;">
|
||||
<VueMonacoEditor theme="vs-dark" language="json" height="80vh" style="padding-top: 16px; padding-bottom: 16px;"
|
||||
v-model:value="config_data_str">
|
||||
</VueMonacoEditor>
|
||||
</v-card>
|
||||
|
||||
<v-btn icon="mdi-content-save" size="x-large" style="position: fixed; right: 52px; bottom: 52px;" color="darkprimary"
|
||||
@click="updateConfig">
|
||||
</v-btn>
|
||||
<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 || !!conflictMessage">
|
||||
{{ isEditingConfig ? '更新' : '创建' }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack">
|
||||
{{ save_message }}
|
||||
@@ -130,23 +181,22 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import AstrBotConfigV4 from '@/components/shared/AstrBotConfigV4.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import config from '@/config';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'ConfigPage',
|
||||
components: {
|
||||
AstrBotConfig,
|
||||
AstrBotConfigV4,
|
||||
VueMonacoEditor,
|
||||
WaitingForRestart
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/config');
|
||||
|
||||
|
||||
return {
|
||||
t,
|
||||
tm
|
||||
@@ -154,7 +204,6 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 安全访问翻译的计算属性
|
||||
messages() {
|
||||
return {
|
||||
loadError: this.tm('messages.loadError'),
|
||||
@@ -163,7 +212,22 @@ export default {
|
||||
configApplied: this.tm('messages.configApplied'),
|
||||
configApplyError: this.tm('messages.configApplyError')
|
||||
};
|
||||
}
|
||||
},
|
||||
configInfoNameList() {
|
||||
return this.configInfoList.map(info => info.name);
|
||||
},
|
||||
selectedConfigInfo() {
|
||||
return this.configInfoList.find(info => info.id === this.selectedConfigID) || {};
|
||||
},
|
||||
configSelectItems() {
|
||||
const items = [...this.configInfoList];
|
||||
items.push({
|
||||
id: '_%manage%_',
|
||||
name: '管理配置文件...',
|
||||
umop: []
|
||||
});
|
||||
return items;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
config_data_str: function (val) {
|
||||
@@ -172,6 +236,10 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
codeEditorDialog: false,
|
||||
configManageDialog: false,
|
||||
showConfigForm: false,
|
||||
isEditingConfig: false,
|
||||
config_data_has_changed: false,
|
||||
config_data_str: "",
|
||||
config_data: {
|
||||
@@ -179,30 +247,79 @@ export default {
|
||||
},
|
||||
fetched: false,
|
||||
metadata: {},
|
||||
provider_config_tmpl: {},
|
||||
adapter_config_tmpl: {}, // 平台适配器
|
||||
save_message_snack: false,
|
||||
save_message: "",
|
||||
save_message_success: "",
|
||||
namespace: "",
|
||||
tab: 0,
|
||||
editorTab: 0, // 0: visual, 1: code
|
||||
|
||||
config_template_tab: 0,
|
||||
tab: 0, // 用于切换配置标签页
|
||||
|
||||
// 配置类型切换
|
||||
configType: 'normal', // 'normal' 或 'system'
|
||||
|
||||
// 系统配置开关
|
||||
isSystemConfig: false,
|
||||
|
||||
// 多配置文件管理
|
||||
selectedConfigID: null, // 用于存储当前选中的配置项信息
|
||||
configInfoList: [],
|
||||
platformList: [],
|
||||
configFormData: {
|
||||
name: '',
|
||||
umop: [],
|
||||
},
|
||||
editingConfigId: null,
|
||||
conflictMessage: '', // 冲突提示信息
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getConfig();
|
||||
this.getConfigInfoList("default");
|
||||
// 初始化配置类型状态
|
||||
this.configType = this.isSystemConfig ? 'system' : 'normal';
|
||||
},
|
||||
methods: {
|
||||
getConfig() {
|
||||
// 获取配置
|
||||
axios.get('/api/config/get').then((res) => {
|
||||
getConfigInfoList(abconf_id) {
|
||||
// 获取配置列表
|
||||
axios.get('/api/config/abconfs').then((res) => {
|
||||
this.configInfoList = res.data.data.info_list;
|
||||
|
||||
if (abconf_id) {
|
||||
for (let i = 0; i < this.configInfoList.length; i++) {
|
||||
if (this.configInfoList[i].id === abconf_id) {
|
||||
this.selectedConfigID = this.configInfoList[i].id
|
||||
this.getConfig(abconf_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
this.save_message = this.messages.loadError;
|
||||
this.save_message_snack = true;
|
||||
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 = {};
|
||||
|
||||
if (this.isSystemConfig) {
|
||||
params.system_config = '1';
|
||||
} else {
|
||||
params.id = abconf_id || this.selectedConfigID;
|
||||
}
|
||||
|
||||
axios.get('/api/config/abconf', {
|
||||
params: params
|
||||
}).then((res) => {
|
||||
this.config_data = res.data.data.config;
|
||||
this.fetched = true
|
||||
this.metadata = res.data.data.metadata;
|
||||
this.provider_config_tmpl = res.data.data.provider_config_tmpl;
|
||||
this.adapter_config_tmpl = res.data.data.adapter_config_tmpl;
|
||||
}).catch((err) => {
|
||||
this.save_message = this.messages.loadError;
|
||||
this.save_message_snack = true;
|
||||
@@ -211,12 +328,28 @@ export default {
|
||||
},
|
||||
updateConfig() {
|
||||
if (!this.fetched) return;
|
||||
axios.post('/api/config/astrbot/update', this.config_data).then((res) => {
|
||||
|
||||
const postData = {
|
||||
config: this.config_data
|
||||
};
|
||||
|
||||
if (this.isSystemConfig) {
|
||||
postData.conf_id = 'default';
|
||||
} else {
|
||||
postData.conf_id = this.selectedConfigID;
|
||||
}
|
||||
|
||||
axios.post('/api/config/astrbot/update', postData).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
this.save_message = res.data.message || this.messages.saveSuccess;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
this.$refs.wfr.check();
|
||||
|
||||
if (this.isSystemConfig) {
|
||||
axios.post('/api/stat/restart-core').then(() => {
|
||||
this.$refs.wfr.check();
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.save_message = res.data.message || this.messages.saveError;
|
||||
this.save_message_snack = true;
|
||||
@@ -245,27 +378,248 @@ export default {
|
||||
this.save_message_snack = true;
|
||||
}
|
||||
},
|
||||
addFromDefaultConfigTmpl(val, group_name, config_item_name) {
|
||||
console.log(val);
|
||||
createNewConfig() {
|
||||
// 修正为 umo part 形式
|
||||
// 暂时只支持 platform:: 形式
|
||||
const umo_parts = this.configFormData.umop.map(platform => platform + "::");
|
||||
|
||||
let tmpl = this.metadata[group_name]['metadata'][config_item_name]['config_template'][val];
|
||||
let new_tmpl_cfg = JSON.parse(JSON.stringify(tmpl));
|
||||
// new_tmpl_cfg.id = "new_" + val + "_" + this.config_data[config_item_name].length;
|
||||
this.config_data[config_item_name].push(new_tmpl_cfg);
|
||||
this.config_template_tab = this.config_data[config_item_name].length - 1;
|
||||
axios.post('/api/config/abconf/new', {
|
||||
umo_parts: umo_parts,
|
||||
name: this.configFormData.name
|
||||
}).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
this.save_message = res.data.message;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
this.getConfigInfoList(res.data.data.conf_id);
|
||||
this.cancelConfigForm();
|
||||
} else {
|
||||
this.save_message = res.data.message;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.save_message = "新配置文件创建失败";
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
},
|
||||
deleteItem(config_item_name, index) {
|
||||
console.log(config_item_name, index);
|
||||
let new_list = [];
|
||||
for (let i = 0; i < this.config_data[config_item_name].length; i++) {
|
||||
if (i !== index) {
|
||||
new_list.push(this.config_data[config_item_name][i]);
|
||||
checkPlatformConflict(newPlatforms) {
|
||||
const conflictConfigs = [];
|
||||
|
||||
// 遍历现有的配置文件,排除名为 "default" 的配置
|
||||
for (const config of this.configInfoList) {
|
||||
if (config.name === 'default') {
|
||||
continue; // 跳过 default 配置
|
||||
}
|
||||
|
||||
if (config.umop && config.umop.length > 0) {
|
||||
// 获取现有配置的平台列表
|
||||
const existingPlatforms = config.umop.map(umop => {
|
||||
const platformPart = umop.split(":")[0];
|
||||
return platformPart === "" ? "*" : platformPart; // 空字符串表示所有平台
|
||||
});
|
||||
|
||||
// 检查是否有重复的平台
|
||||
const hasConflict = newPlatforms.some(newPlatform => {
|
||||
return existingPlatforms.includes(newPlatform) || existingPlatforms.includes("*");
|
||||
}) || (newPlatforms.includes("*") && existingPlatforms.length > 0);
|
||||
|
||||
if (hasConflict) {
|
||||
conflictConfigs.push(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.config_data[config_item_name] = new_list;
|
||||
|
||||
if (this.config_template_tab > 0) {
|
||||
this.config_template_tab -= 1;
|
||||
return conflictConfigs;
|
||||
},
|
||||
onConfigSelect(value) {
|
||||
if (value === '_%manage%_') {
|
||||
this.configManageDialog = true;
|
||||
this.getPlatformList();
|
||||
// 重置选择到之前的值
|
||||
this.$nextTick(() => {
|
||||
this.selectedConfigID = this.selectedConfigInfo.id || 'default';
|
||||
});
|
||||
} else {
|
||||
this.getConfig(value);
|
||||
}
|
||||
},
|
||||
startCreateConfig() {
|
||||
this.showConfigForm = true;
|
||||
this.isEditingConfig = false;
|
||||
this.configFormData = {
|
||||
name: '',
|
||||
umop: [],
|
||||
};
|
||||
this.editingConfigId = null;
|
||||
this.conflictMessage = '';
|
||||
},
|
||||
startEditConfig(config) {
|
||||
this.showConfigForm = true;
|
||||
this.isEditingConfig = true;
|
||||
this.editingConfigId = config.id;
|
||||
this.configFormData = {
|
||||
name: config.name || '',
|
||||
umop: config.umop ? config.umop.map(part => part.split("::")[0]).filter(p => p) : [],
|
||||
};
|
||||
this.conflictMessage = '';
|
||||
},
|
||||
cancelConfigForm() {
|
||||
this.showConfigForm = false;
|
||||
this.isEditingConfig = false;
|
||||
this.editingConfigId = null;
|
||||
this.configFormData = {
|
||||
name: '',
|
||||
umop: [],
|
||||
};
|
||||
this.conflictMessage = '';
|
||||
},
|
||||
saveConfigForm() {
|
||||
if (!this.configFormData.name || !this.configFormData.umop.length) {
|
||||
this.save_message = "请填写配置名称和选择应用平台";
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.conflictMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEditingConfig) {
|
||||
this.updateConfigInfo();
|
||||
} else {
|
||||
this.createNewConfig();
|
||||
}
|
||||
},
|
||||
confirmDeleteConfig(config) {
|
||||
if (confirm(`确定要删除配置文件 "${config.name}" 吗?此操作不可恢复。`)) {
|
||||
this.deleteConfig(config.id);
|
||||
}
|
||||
},
|
||||
deleteConfig(configId) {
|
||||
axios.post('/api/config/abconf/delete', {
|
||||
id: configId
|
||||
}).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
this.save_message = res.data.message;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
// 删除成功后,更新配置列表
|
||||
this.getConfigInfoList("default");
|
||||
} else {
|
||||
this.save_message = res.data.message;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.save_message = "删除配置文件失败";
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
},
|
||||
checkPlatformConflictOnForm() {
|
||||
if (!this.configFormData.umop || this.configFormData.umop.length === 0) {
|
||||
this.conflictMessage = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查与其他配置文件的冲突
|
||||
let conflictConfigs = this.checkPlatformConflict(this.configFormData.umop);
|
||||
|
||||
// 如果是编辑模式,排除当前编辑的配置文件
|
||||
if (this.isEditingConfig && this.editingConfigId) {
|
||||
conflictConfigs = conflictConfigs.filter(config => config.id !== this.editingConfigId);
|
||||
}
|
||||
|
||||
if (conflictConfigs.length > 0) {
|
||||
const conflictNames = conflictConfigs.map(config => config.name).join(', ');
|
||||
this.conflictMessage = `提示:选择的平台与现有配置文件重复:${conflictNames}。AstrBot 将只会应用首个匹配的配置文件。`;
|
||||
} else {
|
||||
this.conflictMessage = '';
|
||||
}
|
||||
},
|
||||
updateConfigInfo() {
|
||||
// 修正为 umo part 形式
|
||||
// 暂时只支持 platform:: 形式
|
||||
const umo_parts = this.configFormData.umop.map(platform => platform + "::");
|
||||
|
||||
axios.post('/api/config/abconf/update', {
|
||||
id: this.editingConfigId,
|
||||
name: this.configFormData.name,
|
||||
umo_parts: umo_parts
|
||||
}).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
this.save_message = res.data.message;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
this.getConfigInfoList(this.editingConfigId);
|
||||
this.cancelConfigForm();
|
||||
} else {
|
||||
this.save_message = res.data.message;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.save_message = "更新配置文件失败";
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
});
|
||||
},
|
||||
formatUmop(umop) {
|
||||
if (!umop) {
|
||||
return
|
||||
}
|
||||
let ret = ""
|
||||
for (let i = 0; i < umop.length; i++) {
|
||||
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) {
|
||||
// 切换到系统配置
|
||||
this.getConfig();
|
||||
} else {
|
||||
// 切换回普通配置,如果有选中的配置文件则加载,否则加载default
|
||||
if (this.selectedConfigID) {
|
||||
this.getConfig(this.selectedConfigID);
|
||||
} else {
|
||||
this.getConfigInfoList("default");
|
||||
}
|
||||
}
|
||||
},
|
||||
onSystemConfigToggle() {
|
||||
// 保持向后兼容性,更新 configType
|
||||
this.configType = this.isSystemConfig ? 'system' : 'normal';
|
||||
|
||||
this.tab = 0; // 重置标签页
|
||||
this.fetched = false; // 重置加载状态
|
||||
|
||||
if (this.isSystemConfig) {
|
||||
// 切换到系统配置
|
||||
this.getConfig();
|
||||
} else {
|
||||
// 切换回普通配置,如果有选中的配置文件则加载,否则加载default
|
||||
if (this.selectedConfigID) {
|
||||
this.getConfig(this.selectedConfigID);
|
||||
} else {
|
||||
this.getConfigInfoList("default");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -277,4 +631,57 @@ export default {
|
||||
.v-tab {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
/* 按钮切换样式优化 */
|
||||
.v-btn-toggle .v-btn {
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.v-btn-toggle .v-btn:not(.v-btn--active) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.v-btn-toggle .v-btn.v-btn--active {
|
||||
opacity: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-tabs-window {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -151,8 +151,6 @@
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
|
||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||
|
||||
<!-- ID冲突确认对话框 -->
|
||||
<v-dialog v-model="showIdConflictDialog" max-width="450" persistent>
|
||||
<v-card>
|
||||
@@ -359,7 +357,6 @@ export default {
|
||||
this.loading = false;
|
||||
this.showPlatformCfg = false;
|
||||
this.getConfig();
|
||||
this.$refs.wfr.check();
|
||||
this.showSuccess(res.data.message || this.messages.updateSuccess);
|
||||
}).catch((err) => {
|
||||
this.loading = false;
|
||||
@@ -413,7 +410,6 @@ export default {
|
||||
if (confirm(`${this.messages.deleteConfirm} ${platform.id}?`)) {
|
||||
axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => {
|
||||
this.getConfig();
|
||||
this.$refs.wfr.check();
|
||||
this.showSuccess(res.data.message || this.messages.deleteSuccess);
|
||||
}).catch((err) => {
|
||||
this.showError(err.response?.data?.message || err.message);
|
||||
@@ -429,7 +425,6 @@ export default {
|
||||
config: platform
|
||||
}).then((res) => {
|
||||
this.getConfig();
|
||||
this.$refs.wfr.check();
|
||||
this.showSuccess(res.data.message || this.messages.statusUpdateSuccess);
|
||||
}).catch((err) => {
|
||||
platform.enable = !platform.enable; // 发生错误时回滚状态
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<v-btn color="success" prepend-icon="mdi-cog" variant="tonal" class="me-2" @click="showSettingsDialog = true" rounded="xl" size="x-large">
|
||||
{{ tm('providers.settings') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddProviderDialog = true" rounded="xl" size="x-large">
|
||||
{{ tm('providers.addProvider') }}
|
||||
</v-btn>
|
||||
@@ -250,49 +247,6 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 设置对话框 -->
|
||||
<v-dialog v-model="showSettingsDialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
|
||||
<v-icon color="white" class="me-2">mdi-cog</v-icon>
|
||||
<span>{{ tm('dialogs.settings.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon variant="text" color="white" @click="showSettingsDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch
|
||||
style="padding: 12px;"
|
||||
v-model="sessionSeparationEnabled"
|
||||
color="primary"
|
||||
:loading="sessionSettingLoading"
|
||||
@change="updateSessionSeparation"
|
||||
hide-details
|
||||
>
|
||||
<template v-slot:label>
|
||||
<div>
|
||||
<div class="text-subtitle-1">{{ tm('dialogs.settings.sessionSeparation.title') }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ tm('dialogs.settings.sessionSeparation.description') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showSettingsDialog = false">
|
||||
{{ tm('dialogs.settings.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
|
||||
location="top">
|
||||
@@ -365,11 +319,6 @@ export default {
|
||||
metadata: {},
|
||||
showProviderCfg: false,
|
||||
|
||||
// 设置对话框相关
|
||||
showSettingsDialog: false,
|
||||
sessionSeparationEnabled: false,
|
||||
sessionSettingLoading: false,
|
||||
|
||||
// ID冲突确认对话框
|
||||
showIdConflictDialog: false,
|
||||
conflictId: '',
|
||||
@@ -464,10 +413,8 @@ export default {
|
||||
add: this.tm('messages.success.add'),
|
||||
delete: this.tm('messages.success.delete'),
|
||||
statusUpdate: this.tm('messages.success.statusUpdate'),
|
||||
sessionSeparation: this.tm('messages.success.sessionSeparation')
|
||||
},
|
||||
error: {
|
||||
sessionSeparation: this.tm('messages.error.sessionSeparation'),
|
||||
fetchStatus: this.tm('messages.error.fetchStatus')
|
||||
},
|
||||
confirm: {
|
||||
@@ -502,7 +449,6 @@ export default {
|
||||
|
||||
mounted() {
|
||||
this.getConfig();
|
||||
this.getSessionSeparationStatus();
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -722,32 +668,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 获取会话隔离配置状态
|
||||
getSessionSeparationStatus() {
|
||||
axios.get('/api/config/provider/get_session_seperate').then((res) => {
|
||||
if (res.data && res.data.status === 'ok') {
|
||||
this.sessionSeparationEnabled = res.data.data.enable;
|
||||
}
|
||||
}).catch((err) => {
|
||||
this.showError(err.response?.data?.message || this.messages.error.sessionSeparation);
|
||||
});
|
||||
},
|
||||
|
||||
// 更新会话隔离配置
|
||||
updateSessionSeparation() {
|
||||
this.sessionSettingLoading = true;
|
||||
axios.post('/api/config/provider/set_session_seperate', {
|
||||
enable: this.sessionSeparationEnabled
|
||||
}).then((res) => {
|
||||
this.showSuccess(res.data.message || this.messages.success.sessionSeparation);
|
||||
this.sessionSettingLoading = false;
|
||||
}).catch((err) => {
|
||||
this.sessionSeparationEnabled = !this.sessionSeparationEnabled; // 发生错误时回滚状态
|
||||
this.showError(err.response?.data?.message || err.message);
|
||||
this.sessionSettingLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
showSuccess(message) {
|
||||
this.save_message = message;
|
||||
this.save_message_success = "success";
|
||||
|
||||
@@ -8,6 +8,7 @@ from astrbot.api.provider import ProviderRequest
|
||||
from astrbot.api.message_components import Plain, Image
|
||||
from astrbot import logger
|
||||
from collections import defaultdict
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
|
||||
"""
|
||||
聊天记忆增强
|
||||
@@ -15,28 +16,40 @@ from collections import defaultdict
|
||||
|
||||
|
||||
class LongTermMemory:
|
||||
def __init__(self, config: dict, context: star.Context):
|
||||
self.config = config
|
||||
def __init__(self, acm: AstrBotConfigManager, context: star.Context):
|
||||
self.acm = acm
|
||||
self.context = context
|
||||
self.session_chats = defaultdict(list)
|
||||
"""记录群成员的群聊记录"""
|
||||
|
||||
def cfg(self, event: AstrMessageEvent):
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
try:
|
||||
self.max_cnt = int(self.config["group_message_max_cnt"])
|
||||
max_cnt = int(cfg["group_message_max_cnt"])
|
||||
except BaseException as e:
|
||||
logger.error(e)
|
||||
self.max_cnt = 300
|
||||
self.image_caption = self.config["image_caption"]
|
||||
self.image_caption_prompt = self.config["image_caption_prompt"]
|
||||
self.image_caption_provider_id = self.config["image_caption_provider_id"]
|
||||
|
||||
self.active_reply = self.config["active_reply"]
|
||||
self.enable_active_reply = self.active_reply.get("enable", False)
|
||||
self.ar_method = self.active_reply["method"]
|
||||
self.ar_possibility = self.active_reply["possibility_reply"]
|
||||
self.ar_prompt = self.active_reply.get("prompt", "")
|
||||
self.ar_whitelist = self.active_reply.get("whitelist", [])
|
||||
|
||||
# self.put_history_to_prompt = self.config["put_history_to_prompt"]
|
||||
max_cnt = 300
|
||||
image_caption = cfg["image_caption"]
|
||||
image_caption_prompt = cfg["image_caption_prompt"] # TODO: 去掉这个配置项
|
||||
image_caption_provider_id = cfg["image_caption_provider_id"] # TODO: 去掉这个配置项
|
||||
active_reply = cfg["active_reply"]
|
||||
enable_active_reply = active_reply.get("enable", False)
|
||||
ar_method = active_reply["method"]
|
||||
ar_possibility = active_reply["possibility_reply"]
|
||||
ar_prompt = active_reply.get("prompt", "")
|
||||
ar_whitelist = active_reply.get("whitelist", [])
|
||||
ret = {
|
||||
"max_cnt": max_cnt,
|
||||
"image_caption": image_caption,
|
||||
"image_caption_prompt": image_caption_prompt,
|
||||
"image_caption_provider_id": image_caption_provider_id,
|
||||
"enable_active_reply": enable_active_reply,
|
||||
"ar_method": ar_method,
|
||||
"ar_possibility": ar_possibility,
|
||||
"ar_prompt": ar_prompt,
|
||||
"ar_whitelist": ar_whitelist,
|
||||
}
|
||||
return ret
|
||||
|
||||
async def remove_session(self, event: AstrMessageEvent) -> int:
|
||||
cnt = 0
|
||||
@@ -45,17 +58,17 @@ class LongTermMemory:
|
||||
del self.session_chats[event.unified_msg_origin]
|
||||
return cnt
|
||||
|
||||
async def get_image_caption(self, image_url: str) -> str:
|
||||
if not self.image_caption_provider_id:
|
||||
async def get_image_caption(
|
||||
self, image_url: str, image_caption_provider_id: str, image_caption_prompt: str
|
||||
) -> str:
|
||||
if not image_caption_provider_id:
|
||||
provider = self.context.get_using_provider()
|
||||
else:
|
||||
provider = self.context.get_provider_by_id(self.image_caption_provider_id)
|
||||
provider = self.context.get_provider_by_id(image_caption_provider_id)
|
||||
if not provider:
|
||||
raise Exception(
|
||||
f"没有找到 ID 为 {self.image_caption_provider_id} 的提供商"
|
||||
)
|
||||
raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商")
|
||||
response = await provider.text_chat(
|
||||
prompt=self.image_caption_prompt,
|
||||
prompt=image_caption_prompt,
|
||||
session_id=uuid.uuid4().hex,
|
||||
image_urls=[image_url],
|
||||
persist=False,
|
||||
@@ -63,7 +76,8 @@ class LongTermMemory:
|
||||
return response.completion_text
|
||||
|
||||
async def need_active_reply(self, event: AstrMessageEvent) -> bool:
|
||||
if not self.enable_active_reply:
|
||||
cfg = self.cfg(event)
|
||||
if not cfg["enable_active_reply"]:
|
||||
return False
|
||||
if event.get_message_type() != MessageType.GROUP_MESSAGE:
|
||||
return False
|
||||
@@ -72,15 +86,15 @@ class LongTermMemory:
|
||||
# if the message is a command, let it pass
|
||||
return False
|
||||
|
||||
if self.ar_whitelist and (
|
||||
event.unified_msg_origin not in self.ar_whitelist
|
||||
and (event.get_group_id() and event.get_group_id() not in self.ar_whitelist)
|
||||
if cfg["ar_whitelist"] and (
|
||||
event.unified_msg_origin not in cfg["ar_whitelist"]
|
||||
and (event.get_group_id() and event.get_group_id() not in cfg["ar_whitelist"])
|
||||
):
|
||||
return False
|
||||
|
||||
match self.ar_method:
|
||||
match cfg["ar_method"]:
|
||||
case "possibility_reply":
|
||||
trig = random.random() < self.ar_possibility
|
||||
trig = random.random() < cfg["ar_possibility"]
|
||||
return trig
|
||||
|
||||
return False
|
||||
@@ -92,15 +106,19 @@ class LongTermMemory:
|
||||
|
||||
final_message = f"[{event.message_obj.sender.nickname}/{datetime_str}]: "
|
||||
|
||||
cfg = self.cfg(event)
|
||||
|
||||
for comp in event.get_messages():
|
||||
if isinstance(comp, Plain):
|
||||
final_message += f" {comp.text}"
|
||||
elif isinstance(comp, Image):
|
||||
# image_urls.append(comp.url if comp.url else comp.file)
|
||||
if self.image_caption:
|
||||
cfg = self.cfg(event)
|
||||
if cfg["image_caption"]:
|
||||
try:
|
||||
caption = await self.get_image_caption(
|
||||
comp.url if comp.url else comp.file
|
||||
comp.url if comp.url else comp.file,
|
||||
cfg["image_caption_provider_id"],
|
||||
cfg["image_caption_prompt"],
|
||||
)
|
||||
final_message += f" [Image: {caption}]"
|
||||
except Exception as e:
|
||||
@@ -109,7 +127,7 @@ class LongTermMemory:
|
||||
final_message += " [Image]"
|
||||
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
|
||||
self.session_chats[event.unified_msg_origin].append(final_message)
|
||||
if len(self.session_chats[event.unified_msg_origin]) > self.max_cnt:
|
||||
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
||||
self.session_chats[event.unified_msg_origin].pop(0)
|
||||
|
||||
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
@@ -119,7 +137,8 @@ class LongTermMemory:
|
||||
|
||||
chats_str = "\n---\n".join(self.session_chats[event.unified_msg_origin])
|
||||
|
||||
if self.enable_active_reply:
|
||||
cfg = self.cfg(event)
|
||||
if cfg["enable_active_reply"]:
|
||||
prompt = req.prompt
|
||||
req.prompt = f"You are now in a chatroom. The chat history is as follows:\n{chats_str}"
|
||||
req.prompt += f"\nNow, a new message is coming: `{prompt}`. Please react to it. Only output your response and do not output any other information."
|
||||
@@ -138,5 +157,6 @@ class LongTermMemory:
|
||||
final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {event.get_result().get_plain_text()}"
|
||||
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
|
||||
self.session_chats[event.unified_msg_origin].append(final_message)
|
||||
if len(self.session_chats[event.unified_msg_origin]) > self.max_cnt:
|
||||
cfg = self.cfg(event)
|
||||
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
||||
self.session_chats[event.unified_msg_origin].pop(0)
|
||||
|
||||
+28
-32
@@ -60,9 +60,6 @@ class Main(star.Star):
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
self.context = context
|
||||
cfg = context.get_config()
|
||||
self.prompt_prefix = cfg["provider_settings"]["prompt_prefix"]
|
||||
self.identifier = cfg["provider_settings"]["identifier"]
|
||||
self.enable_datetime = cfg["provider_settings"]["datetime_system_prompt"]
|
||||
self.timezone = cfg.get("timezone")
|
||||
if not self.timezone:
|
||||
# 系统默认时区
|
||||
@@ -70,18 +67,10 @@ class Main(star.Star):
|
||||
else:
|
||||
logger.info(f"Timezone set to: {self.timezone}")
|
||||
self.ltm = None
|
||||
if (
|
||||
self.context.get_config()["provider_ltm_settings"]["group_icl_enable"]
|
||||
or self.context.get_config()["provider_ltm_settings"]["active_reply"][
|
||||
"enable"
|
||||
]
|
||||
):
|
||||
try:
|
||||
self.ltm = LongTermMemory(
|
||||
self.context.get_config()["provider_ltm_settings"], self.context
|
||||
)
|
||||
except BaseException as e:
|
||||
logger.error(f"聊天增强 err: {e}")
|
||||
try:
|
||||
self.ltm = LongTermMemory(self.context.astrbot_config_mgr, self.context)
|
||||
except BaseException as e:
|
||||
logger.error(f"聊天增强 err: {e}")
|
||||
|
||||
async def _query_astrbot_notice(self):
|
||||
try:
|
||||
@@ -93,6 +82,12 @@ class Main(star.Star):
|
||||
except BaseException:
|
||||
return ""
|
||||
|
||||
def ltm_enabled(self, event: AstrMessageEvent):
|
||||
ltmse = self.context.get_config(umo=event.unified_msg_origin)[
|
||||
"provider_ltm_settings"
|
||||
]
|
||||
return ltmse["group_icl_enable"] or ltmse["active_reply"]["enable"]
|
||||
|
||||
@filter.command("help")
|
||||
async def help(self, event: AstrMessageEvent):
|
||||
"""查看帮助"""
|
||||
@@ -139,7 +134,7 @@ class Main(star.Star):
|
||||
@filter.command("llm")
|
||||
async def llm(self, event: AstrMessageEvent):
|
||||
"""开启/关闭 LLM"""
|
||||
cfg = self.context.get_config()
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
enable = cfg["provider_settings"]["enable"]
|
||||
if enable:
|
||||
cfg["provider_settings"]["enable"] = False
|
||||
@@ -294,7 +289,7 @@ class Main(star.Star):
|
||||
@filter.command("t2i")
|
||||
async def t2i(self, event: AstrMessageEvent):
|
||||
"""开关文本转图片"""
|
||||
config = self.context.get_config()
|
||||
config = self.context.get_config(umo=event.unified_msg_origin)
|
||||
if config["t2i"]:
|
||||
config["t2i"] = False
|
||||
config.save_config()
|
||||
@@ -376,8 +371,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
"使用方法: /wl <id> 添加白名单;/dwl <id> 删除白名单。可通过 /sid 获取 ID。"
|
||||
)
|
||||
)
|
||||
self.context.get_config()["platform_settings"]["id_whitelist"].append(str(sid))
|
||||
self.context.get_config().save_config()
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
cfg["platform_settings"]["id_whitelist"].append(str(sid))
|
||||
cfg.save_config()
|
||||
event.set_result(MessageEventResult().message("添加白名单成功。"))
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@@ -385,10 +381,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
async def dwl(self, event: AstrMessageEvent, sid: str):
|
||||
"""删除白名单。dwl <sid>"""
|
||||
try:
|
||||
self.context.get_config()["platform_settings"]["id_whitelist"].remove(
|
||||
str(sid)
|
||||
)
|
||||
self.context.get_config().save_config()
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
cfg["platform_settings"]["id_whitelist"].remove(str(sid))
|
||||
cfg.save_config()
|
||||
event.set_result(MessageEventResult().message("删除白名单成功。"))
|
||||
except ValueError:
|
||||
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
||||
@@ -551,7 +546,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
)
|
||||
|
||||
ret = "清除会话 LLM 聊天历史成功。"
|
||||
if self.ltm:
|
||||
if self.ltm and self.ltm_enabled(message):
|
||||
cnt = await self.ltm.remove_session(event=message)
|
||||
ret += f"\n聊天增强: 已清除 {cnt} 条聊天记录。"
|
||||
|
||||
@@ -769,7 +764,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
)
|
||||
|
||||
# 长期记忆
|
||||
if self.ltm:
|
||||
if self.ltm and self.ltm_enabled(message):
|
||||
try:
|
||||
await self.ltm.remove_session(event=message)
|
||||
except Exception as e:
|
||||
@@ -1137,7 +1132,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
has_image_or_plain = True
|
||||
break
|
||||
|
||||
if self.ltm and has_image_or_plain:
|
||||
if self.ltm_enabled(event) and self.ltm and has_image_or_plain:
|
||||
need_active = await self.ltm.need_active_reply(event)
|
||||
|
||||
group_icl_enable = self.context.get_config()["provider_ltm_settings"][
|
||||
@@ -1205,8 +1200,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
@filter.on_llm_request()
|
||||
async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
||||
if self.prompt_prefix:
|
||||
req.prompt = self.prompt_prefix + req.prompt
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)["provider_settings"]
|
||||
if prefix := cfg.get("prompt_prefix"):
|
||||
req.prompt = prefix + req.prompt
|
||||
|
||||
# 解析引用内容
|
||||
quote = None
|
||||
@@ -1215,14 +1211,14 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
quote = comp
|
||||
break
|
||||
|
||||
if self.identifier:
|
||||
if cfg.get("identifier"):
|
||||
user_id = event.message_obj.sender.user_id
|
||||
user_nickname = event.message_obj.sender.nickname
|
||||
user_info = f"\n[User ID: {user_id}, Nickname: {user_nickname}]\n"
|
||||
req.prompt = user_info + req.prompt
|
||||
|
||||
# 启用附加时间戳
|
||||
if self.enable_datetime:
|
||||
if cfg.get("datetime_system_prompt"):
|
||||
current_time = None
|
||||
if self.timezone:
|
||||
# 启用时区
|
||||
@@ -1300,7 +1296,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
except BaseException as e:
|
||||
logger.error(f"处理引用图片失败: {e}")
|
||||
|
||||
if self.ltm:
|
||||
if self.ltm and self.ltm_enabled(event):
|
||||
try:
|
||||
await self.ltm.on_req_llm(event, req)
|
||||
except BaseException as e:
|
||||
@@ -1309,7 +1305,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
@filter.after_message_sent()
|
||||
async def after_llm_req(self, event: AstrMessageEvent):
|
||||
"""在 LLM 请求后记录对话"""
|
||||
if self.ltm:
|
||||
if self.ltm and self.ltm_enabled(event):
|
||||
try:
|
||||
await self.ltm.after_req_llm(event)
|
||||
except BaseException as e:
|
||||
|
||||
@@ -19,9 +19,6 @@ class Waiter(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
self.p_settings: dict = self.context.get_config()["platform_settings"]
|
||||
self.wake_prefix = self.context.get_config()["wake_prefix"]
|
||||
|
||||
@filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize)
|
||||
async def handle_session_control_agent(self, event: AstrMessageEvent):
|
||||
"""会话控制代理"""
|
||||
@@ -36,16 +33,19 @@ class Waiter(Star):
|
||||
"""实现了对只有一个 @ 的消息内容的处理"""
|
||||
try:
|
||||
messages = event.get_messages()
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
p_settings = cfg["platform_settings"]
|
||||
wake_prefix = cfg.get("wake_prefix", [])
|
||||
if len(messages) == 1:
|
||||
if (
|
||||
isinstance(messages[0], Comp.At)
|
||||
and str(messages[0].qq) == str(event.get_self_id())
|
||||
and self.p_settings.get("empty_mention_waiting", True)
|
||||
and p_settings.get("empty_mention_waiting", True)
|
||||
) or (
|
||||
isinstance(messages[0], Comp.Plain)
|
||||
and messages[0].text.strip() in self.wake_prefix
|
||||
and messages[0].text.strip() in wake_prefix
|
||||
):
|
||||
if self.p_settings.get("empty_mention_waiting_need_reply", True):
|
||||
if p_settings.get("empty_mention_waiting_need_reply", True):
|
||||
try:
|
||||
# 尝试使用 LLM 生成更生动的回复
|
||||
func_tools_mgr = self.context.get_llm_tool_manager()
|
||||
@@ -63,7 +63,8 @@ class Waiter(Star):
|
||||
else:
|
||||
# 创建新对话
|
||||
curr_cid = await self.context.conversation_manager.new_conversation(
|
||||
event.unified_msg_origin, platform_id=event.get_platform_id()
|
||||
event.unified_msg_origin,
|
||||
platform_id=event.get_platform_id(),
|
||||
)
|
||||
|
||||
# 使用 LLM 生成回复
|
||||
|
||||
@@ -8,15 +8,13 @@ from openai.types.chat.chat_completion import ChatCompletion
|
||||
class R1Filter(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
self.display_reasoning_text = (
|
||||
self.context.get_config()
|
||||
.get("provider_settings", {})
|
||||
.get("display_reasoning_text", False)
|
||||
)
|
||||
|
||||
@filter.on_llm_response()
|
||||
async def resp(self, event: AstrMessageEvent, response: LLMResponse):
|
||||
if self.display_reasoning_text:
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin).get(
|
||||
"provider_settings", {}
|
||||
)
|
||||
if cfg.get("display_reasoning_text", False):
|
||||
# 显示推理内容的处理逻辑
|
||||
if (
|
||||
response
|
||||
|
||||
@@ -22,19 +22,6 @@ class Main(star.Star):
|
||||
self.sogo_search = Sogo()
|
||||
self.google = Google()
|
||||
|
||||
self.websearch_link = self.context.get_config()["provider_settings"].get(
|
||||
"web_search_link", False
|
||||
)
|
||||
|
||||
async def initialize(self):
|
||||
websearch = self.context.get_config()["provider_settings"]["web_search"]
|
||||
if websearch:
|
||||
self.context.activate_llm_tool("web_search")
|
||||
self.context.activate_llm_tool("fetch_url")
|
||||
else:
|
||||
self.context.deactivate_llm_tool("web_search")
|
||||
self.context.deactivate_llm_tool("fetch_url")
|
||||
|
||||
async def _tidy_text(self, text: str) -> str:
|
||||
"""清理文本,去除空格、换行符等"""
|
||||
return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ")
|
||||
@@ -54,34 +41,7 @@ class Main(star.Star):
|
||||
|
||||
@filter.command("websearch")
|
||||
async def websearch(self, event: AstrMessageEvent, oper: str = None) -> str:
|
||||
websearch = self.context.get_config()["provider_settings"]["web_search"]
|
||||
if oper is None:
|
||||
status = "开启" if websearch else "关闭"
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
"当前网页搜索功能状态:"
|
||||
+ status
|
||||
+ "。使用 /websearch on 或者 off 启用或者关闭。"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if oper == "on":
|
||||
self.context.get_config()["provider_settings"]["web_search"] = True
|
||||
self.context.get_config().save_config()
|
||||
self.context.activate_llm_tool("web_search")
|
||||
self.context.activate_llm_tool("fetch_url")
|
||||
event.set_result(MessageEventResult().message("已开启网页搜索功能"))
|
||||
elif oper == "off":
|
||||
self.context.get_config()["provider_settings"]["web_search"] = False
|
||||
self.context.get_config().save_config()
|
||||
self.context.deactivate_llm_tool("web_search")
|
||||
self.context.deactivate_llm_tool("fetch_url")
|
||||
event.set_result(MessageEventResult().message("已关闭网页搜索功能"))
|
||||
else:
|
||||
event.set_result(
|
||||
MessageEventResult().message("操作参数错误,应为 on 或 off")
|
||||
)
|
||||
event.set_result(MessageEventResult().message("此指令已经被废弃,请在 WebUI 中开启或关闭网页搜索功能。"))
|
||||
|
||||
@llm_tool("web_search")
|
||||
async def search_from_search_engine(
|
||||
@@ -93,6 +53,9 @@ class Main(star.Star):
|
||||
query(string): 和用户的问题最相关的搜索关键词,用于在 Google 上搜索。
|
||||
"""
|
||||
logger.info("web_searcher - search_from_search_engine: " + query)
|
||||
websearch_link = self.context.get_config(umo=event.unified_msg_origin)[
|
||||
"provider_settings"
|
||||
].get("web_search_link", False)
|
||||
results = []
|
||||
RESULT_NUM = 5
|
||||
try:
|
||||
@@ -128,13 +91,13 @@ class Main(star.Star):
|
||||
|
||||
header = f"{idx}. {i.title} "
|
||||
|
||||
if self.websearch_link and i.url:
|
||||
if websearch_link and i.url:
|
||||
header += i.url
|
||||
|
||||
ret += f"{header}\n{i.snippet}\n{site_result}\n\n"
|
||||
idx += 1
|
||||
|
||||
if self.websearch_link:
|
||||
if websearch_link:
|
||||
ret += "针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。"
|
||||
|
||||
return ret
|
||||
|
||||
Reference in New Issue
Block a user