Compare commits
3 Commits
v4.7.1
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf5a6aeaff | ||
|
|
3989a6669c | ||
|
|
0b53b8f96a |
4
.github/workflows/auto_release.yml
vendored
4
.github/workflows/auto_release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Dashboard Build
|
||||
run: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
needs: build-and-publish-to-github-release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
2
.github/workflows/code-format.yml
vendored
2
.github/workflows/code-format.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
2
.github/workflows/coverage_test.yml
vendored
2
.github/workflows/coverage_test.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/dashboard_ci.yml
vendored
2
.github/workflows/dashboard_ci.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tag: true
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tag: true
|
||||
|
||||
@@ -345,6 +345,9 @@ class MCPClient:
|
||||
|
||||
async def cleanup(self):
|
||||
"""Clean up resources including old exit stacks from reconnections"""
|
||||
# Set running_event first to unblock any waiting tasks
|
||||
self.running_event.set()
|
||||
|
||||
# Close current exit stack
|
||||
try:
|
||||
await self.exit_stack.aclose()
|
||||
@@ -356,9 +359,6 @@ class MCPClient:
|
||||
# Just clear the list to release references
|
||||
self._old_exit_stacks.clear()
|
||||
|
||||
# Set running_event first to unblock any waiting tasks
|
||||
self.running_event.set()
|
||||
|
||||
|
||||
class MCPTool(FunctionTool, Generic[TContext]):
|
||||
"""A function tool that calls an MCP service."""
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.7.1"
|
||||
VERSION = "4.6.1"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -647,7 +647,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"words_count_threshold": {
|
||||
"type": "int",
|
||||
"hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150",
|
||||
"hint": "超过这个字数的消息不会被分段回复。默认为 150",
|
||||
},
|
||||
"regex": {
|
||||
"type": "string",
|
||||
|
||||
@@ -161,21 +161,11 @@ class ResultDecorateStage(Stage):
|
||||
# 不分段回复
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
try:
|
||||
split_response = re.findall(
|
||||
self.regex,
|
||||
comp.text,
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
except re.error:
|
||||
logger.error(
|
||||
f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}",
|
||||
)
|
||||
split_response = re.findall(
|
||||
r".*?[。?!~…]+|.+$",
|
||||
comp.text,
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
if not split_response:
|
||||
new_chain.append(comp)
|
||||
continue
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
|
||||
from astrbot.core import astrbot_config, logger, sp
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
from astrbot.core.db import BaseDatabase
|
||||
|
||||
@@ -24,7 +24,6 @@ class ProviderManager:
|
||||
db_helper: BaseDatabase,
|
||||
persona_mgr: PersonaManager,
|
||||
):
|
||||
self.reload_lock = asyncio.Lock()
|
||||
self.persona_mgr = persona_mgr
|
||||
self.acm = acm
|
||||
config = acm.confs["default"]
|
||||
@@ -227,7 +226,6 @@ class ProviderManager:
|
||||
|
||||
async def load_provider(self, provider_config: dict):
|
||||
if not provider_config["enable"]:
|
||||
logger.info(f"Provider {provider_config['id']} is disabled, skipping")
|
||||
return
|
||||
if provider_config.get("provider_type", "") == "agent_runner":
|
||||
return
|
||||
@@ -436,15 +434,13 @@ class ProviderManager:
|
||||
)
|
||||
|
||||
async def reload(self, provider_config: dict):
|
||||
async with self.reload_lock:
|
||||
await self.terminate_provider(provider_config["id"])
|
||||
if provider_config["enable"]:
|
||||
await self.load_provider(provider_config)
|
||||
|
||||
# 和配置文件保持同步
|
||||
self.providers_config = astrbot_config["provider"]
|
||||
config_ids = [provider["id"] for provider in self.providers_config]
|
||||
logger.info(f"providers in user's config: {config_ids}")
|
||||
logger.debug(f"providers in user's config: {config_ids}")
|
||||
for key in list(self.inst_map.keys()):
|
||||
if key not in config_ids:
|
||||
await self.terminate_provider(key)
|
||||
@@ -459,9 +455,7 @@ class ProviderManager:
|
||||
|
||||
if len(self.stt_provider_insts) == 0:
|
||||
self.curr_stt_provider_inst = None
|
||||
elif (
|
||||
self.curr_stt_provider_inst is None and len(self.stt_provider_insts) > 0
|
||||
):
|
||||
elif self.curr_stt_provider_inst is None and len(self.stt_provider_insts) > 0:
|
||||
self.curr_stt_provider_inst = self.stt_provider_insts[0]
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_stt_provider_inst.meta().id} 作为当前语音转文本提供商适配器。",
|
||||
@@ -469,9 +463,7 @@ class ProviderManager:
|
||||
|
||||
if len(self.tts_provider_insts) == 0:
|
||||
self.curr_tts_provider_inst = None
|
||||
elif (
|
||||
self.curr_tts_provider_inst is None and len(self.tts_provider_insts) > 0
|
||||
):
|
||||
elif self.curr_tts_provider_inst is None and len(self.tts_provider_insts) > 0:
|
||||
self.curr_tts_provider_inst = self.tts_provider_insts[0]
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_tts_provider_inst.meta().id} 作为当前文本转语音提供商适配器。",
|
||||
|
||||
@@ -171,3 +171,110 @@ class SessionServiceManager:
|
||||
|
||||
# 如果没有配置,默认为启用(兼容性考虑)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def set_session_status(session_id: str, enabled: bool) -> None:
|
||||
"""设置会话的整体启停状态
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
enabled: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
session_config = (
|
||||
sp.get("session_service_config", {}, scope="umo", scope_id=session_id) or {}
|
||||
)
|
||||
session_config["session_enabled"] = enabled
|
||||
sp.put(
|
||||
"session_service_config",
|
||||
session_config,
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"会话 {session_id} 的整体状态已更新为: {'启用' if enabled else '禁用'}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def should_process_session_request(event: AstrMessageEvent) -> bool:
|
||||
"""检查是否应该处理会话请求(会话整体启停检查)
|
||||
|
||||
Args:
|
||||
event: 消息事件
|
||||
|
||||
Returns:
|
||||
bool: True表示应该处理,False表示跳过
|
||||
|
||||
"""
|
||||
session_id = event.unified_msg_origin
|
||||
return SessionServiceManager.is_session_enabled(session_id)
|
||||
|
||||
# =============================================================================
|
||||
# 会话命名相关方法
|
||||
# =============================================================================
|
||||
|
||||
@staticmethod
|
||||
def get_session_custom_name(session_id: str) -> str | None:
|
||||
"""获取会话的自定义名称
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
str: 自定义名称,如果没有设置则返回None
|
||||
|
||||
"""
|
||||
session_services = sp.get(
|
||||
"session_service_config",
|
||||
{},
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
return session_services.get("custom_name")
|
||||
|
||||
@staticmethod
|
||||
def set_session_custom_name(session_id: str, custom_name: str) -> None:
|
||||
"""设置会话的自定义名称
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
custom_name: 自定义名称,可以为空字符串来清除名称
|
||||
|
||||
"""
|
||||
session_config = (
|
||||
sp.get("session_service_config", {}, scope="umo", scope_id=session_id) or {}
|
||||
)
|
||||
if custom_name and custom_name.strip():
|
||||
session_config["custom_name"] = custom_name.strip()
|
||||
else:
|
||||
# 如果传入空名称,则删除自定义名称
|
||||
session_config.pop("custom_name", None)
|
||||
sp.put(
|
||||
"session_service_config",
|
||||
session_config,
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"会话 {session_id} 的自定义名称已更新为: {custom_name.strip() if custom_name and custom_name.strip() else '已清除'}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_session_display_name(session_id: str) -> str:
|
||||
"""获取会话的显示名称(优先显示自定义名称,否则显示原始session_id的最后一段)
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
str: 显示名称
|
||||
|
||||
"""
|
||||
custom_name = SessionServiceManager.get_session_custom_name(session_id)
|
||||
if custom_name:
|
||||
return custom_name
|
||||
|
||||
# 如果没有自定义名称,返回session_id的最后一段
|
||||
return session_id.split(":")[2] if session_id.count(":") >= 2 else session_id
|
||||
|
||||
@@ -42,6 +42,87 @@ class SessionPluginManager:
|
||||
# 如果都没有配置,默认为启用(兼容性考虑)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def set_plugin_status_for_session(
|
||||
session_id: str,
|
||||
plugin_name: str,
|
||||
enabled: bool,
|
||||
) -> None:
|
||||
"""设置插件在指定会话中的启停状态
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
plugin_name: 插件名称
|
||||
enabled: True表示启用,False表示禁用
|
||||
|
||||
"""
|
||||
# 获取当前配置
|
||||
session_plugin_config = sp.get(
|
||||
"session_plugin_config",
|
||||
{},
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
if session_id not in session_plugin_config:
|
||||
session_plugin_config[session_id] = {
|
||||
"enabled_plugins": [],
|
||||
"disabled_plugins": [],
|
||||
}
|
||||
|
||||
session_config = session_plugin_config[session_id]
|
||||
enabled_plugins = session_config.get("enabled_plugins", [])
|
||||
disabled_plugins = session_config.get("disabled_plugins", [])
|
||||
|
||||
if enabled:
|
||||
# 启用插件
|
||||
if plugin_name in disabled_plugins:
|
||||
disabled_plugins.remove(plugin_name)
|
||||
if plugin_name not in enabled_plugins:
|
||||
enabled_plugins.append(plugin_name)
|
||||
else:
|
||||
# 禁用插件
|
||||
if plugin_name in enabled_plugins:
|
||||
enabled_plugins.remove(plugin_name)
|
||||
if plugin_name not in disabled_plugins:
|
||||
disabled_plugins.append(plugin_name)
|
||||
|
||||
# 保存配置
|
||||
session_config["enabled_plugins"] = enabled_plugins
|
||||
session_config["disabled_plugins"] = disabled_plugins
|
||||
session_plugin_config[session_id] = session_config
|
||||
sp.put(
|
||||
"session_plugin_config",
|
||||
session_plugin_config,
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"会话 {session_id} 的插件 {plugin_name} 状态已更新为: {'启用' if enabled else '禁用'}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_session_plugin_config(session_id: str) -> dict[str, list[str]]:
|
||||
"""获取指定会话的插件配置
|
||||
|
||||
Args:
|
||||
session_id: 会话ID (unified_msg_origin)
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: 包含enabled_plugins和disabled_plugins的字典
|
||||
|
||||
"""
|
||||
session_plugin_config = sp.get(
|
||||
"session_plugin_config",
|
||||
{},
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
return session_plugin_config.get(
|
||||
session_id,
|
||||
{"enabled_plugins": [], "disabled_plugins": []},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def filter_handlers_by_session(event: AstrMessageEvent, handlers: list) -> list:
|
||||
"""根据会话配置过滤处理器列表
|
||||
|
||||
@@ -60,6 +60,10 @@ class KnowledgeBaseRoute(Route):
|
||||
# "/kb/media/delete": ("POST", self.delete_media),
|
||||
# 检索
|
||||
"/kb/retrieve": ("POST", self.retrieve),
|
||||
# 会话知识库配置
|
||||
"/kb/session/config/get": ("GET", self.get_session_kb_config),
|
||||
"/kb/session/config/set": ("POST", self.set_session_kb_config),
|
||||
"/kb/session/config/delete": ("POST", self.delete_session_kb_config),
|
||||
}
|
||||
self.register_routes()
|
||||
|
||||
@@ -916,6 +920,158 @@ class KnowledgeBaseRoute(Route):
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"检索失败: {e!s}").__dict__
|
||||
|
||||
# ===== 会话知识库配置 API =====
|
||||
|
||||
async def get_session_kb_config(self):
|
||||
"""获取会话的知识库配置
|
||||
|
||||
Query 参数:
|
||||
- session_id: 会话 ID (必填)
|
||||
|
||||
返回:
|
||||
- kb_ids: 知识库 ID 列表
|
||||
- top_k: 返回结果数量
|
||||
- enable_rerank: 是否启用重排序
|
||||
"""
|
||||
try:
|
||||
from astrbot.core import sp
|
||||
|
||||
session_id = request.args.get("session_id")
|
||||
|
||||
if not session_id:
|
||||
return Response().error("缺少参数 session_id").__dict__
|
||||
|
||||
# 从 SharedPreferences 获取配置
|
||||
config = await sp.session_get(session_id, "kb_config", default={})
|
||||
|
||||
logger.debug(f"[KB配置] 读取到配置: session_id={session_id}")
|
||||
|
||||
# 如果没有配置,返回默认值
|
||||
if not config:
|
||||
config = {"kb_ids": [], "top_k": 5, "enable_rerank": True}
|
||||
|
||||
return Response().ok(config).__dict__
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KB配置] 获取配置时出错: {e}", exc_info=True)
|
||||
return Response().error(f"获取会话知识库配置失败: {e!s}").__dict__
|
||||
|
||||
async def set_session_kb_config(self):
|
||||
"""设置会话的知识库配置
|
||||
|
||||
Body:
|
||||
- scope: 配置范围 (目前只支持 "session")
|
||||
- scope_id: 会话 ID (必填)
|
||||
- kb_ids: 知识库 ID 列表 (必填)
|
||||
- top_k: 返回结果数量 (可选, 默认 5)
|
||||
- enable_rerank: 是否启用重排序 (可选, 默认 true)
|
||||
"""
|
||||
try:
|
||||
from astrbot.core import sp
|
||||
|
||||
data = await request.json
|
||||
|
||||
scope = data.get("scope")
|
||||
scope_id = data.get("scope_id")
|
||||
kb_ids = data.get("kb_ids", [])
|
||||
top_k = data.get("top_k", 5)
|
||||
enable_rerank = data.get("enable_rerank", True)
|
||||
|
||||
# 验证参数
|
||||
if scope != "session":
|
||||
return Response().error("目前仅支持 session 范围的配置").__dict__
|
||||
|
||||
if not scope_id:
|
||||
return Response().error("缺少参数 scope_id").__dict__
|
||||
|
||||
if not isinstance(kb_ids, list):
|
||||
return Response().error("kb_ids 必须是列表").__dict__
|
||||
|
||||
# 验证知识库是否存在
|
||||
kb_mgr = self._get_kb_manager()
|
||||
invalid_ids = []
|
||||
valid_ids = []
|
||||
for kb_id in kb_ids:
|
||||
kb_helper = await kb_mgr.get_kb(kb_id)
|
||||
if kb_helper:
|
||||
valid_ids.append(kb_id)
|
||||
else:
|
||||
invalid_ids.append(kb_id)
|
||||
logger.warning(f"[KB配置] 知识库不存在: {kb_id}")
|
||||
|
||||
if invalid_ids:
|
||||
logger.warning(f"[KB配置] 以下知识库ID无效: {invalid_ids}")
|
||||
|
||||
# 允许保存空列表,表示明确不使用任何知识库
|
||||
if kb_ids and not valid_ids:
|
||||
# 只有当用户提供了 kb_ids 但全部无效时才报错
|
||||
return Response().error(f"所有提供的知识库ID都无效: {kb_ids}").__dict__
|
||||
|
||||
# 如果 kb_ids 为空列表,表示用户想清空配置
|
||||
if not kb_ids:
|
||||
valid_ids = []
|
||||
|
||||
# 构建配置对象(只保存有效的ID)
|
||||
config = {
|
||||
"kb_ids": valid_ids,
|
||||
"top_k": top_k,
|
||||
"enable_rerank": enable_rerank,
|
||||
}
|
||||
|
||||
# 保存到 SharedPreferences
|
||||
await sp.session_put(scope_id, "kb_config", config)
|
||||
|
||||
# 立即验证是否保存成功
|
||||
verify_config = await sp.session_get(scope_id, "kb_config", default={})
|
||||
|
||||
if verify_config == config:
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{"valid_ids": valid_ids, "invalid_ids": invalid_ids},
|
||||
"保存知识库配置成功",
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
logger.error("[KB配置] 配置保存失败,验证不匹配")
|
||||
return Response().error("配置保存失败").__dict__
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[KB配置] 设置配置时出错: {e}", exc_info=True)
|
||||
return Response().error(f"设置会话知识库配置失败: {e!s}").__dict__
|
||||
|
||||
async def delete_session_kb_config(self):
|
||||
"""删除会话的知识库配置
|
||||
|
||||
Body:
|
||||
- scope: 配置范围 (目前只支持 "session")
|
||||
- scope_id: 会话 ID (必填)
|
||||
"""
|
||||
try:
|
||||
from astrbot.core import sp
|
||||
|
||||
data = await request.json
|
||||
|
||||
scope = data.get("scope")
|
||||
scope_id = data.get("scope_id")
|
||||
|
||||
# 验证参数
|
||||
if scope != "session":
|
||||
return Response().error("目前仅支持 session 范围的配置").__dict__
|
||||
|
||||
if not scope_id:
|
||||
return Response().error("缺少参数 scope_id").__dict__
|
||||
|
||||
# 从 SharedPreferences 删除配置
|
||||
await sp.session_remove(scope_id, "kb_config")
|
||||
|
||||
return Response().ok(message="删除知识库配置成功").__dict__
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除会话知识库配置失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"删除会话知识库配置失败: {e!s}").__dict__
|
||||
|
||||
async def upload_document_from_url(self):
|
||||
"""从 URL 上传文档
|
||||
|
||||
|
||||
@@ -74,9 +74,6 @@ class SessionManagementRoute(Route):
|
||||
umo_id = pref.scope_id
|
||||
if umo_id not in umo_rules:
|
||||
umo_rules[umo_id] = {}
|
||||
if pref.key == "session_plugin_config" and umo_id in pref.value["val"]:
|
||||
umo_rules[umo_id][pref.key] = pref.value["val"][umo_id]
|
||||
else:
|
||||
umo_rules[umo_id][pref.key] = pref.value["val"]
|
||||
|
||||
# 搜索过滤
|
||||
@@ -188,35 +185,6 @@ class SessionManagementRoute(Route):
|
||||
for p in provider_manager.tts_provider_insts
|
||||
]
|
||||
|
||||
# 获取可用的插件列表(排除 reserved 的系统插件)
|
||||
plugin_manager = self.core_lifecycle.plugin_manager
|
||||
available_plugins = [
|
||||
{
|
||||
"name": p.name,
|
||||
"display_name": p.display_name or p.name,
|
||||
"desc": p.desc,
|
||||
}
|
||||
for p in plugin_manager.context.get_all_stars()
|
||||
if not p.reserved and p.name
|
||||
]
|
||||
|
||||
# 获取可用的知识库列表
|
||||
available_kbs = []
|
||||
kb_manager = self.core_lifecycle.kb_manager
|
||||
if kb_manager:
|
||||
try:
|
||||
kbs = await kb_manager.list_kbs()
|
||||
available_kbs = [
|
||||
{
|
||||
"kb_id": kb.kb_id,
|
||||
"kb_name": kb.kb_name,
|
||||
"emoji": kb.emoji,
|
||||
}
|
||||
for kb in kbs
|
||||
]
|
||||
except Exception as e:
|
||||
logger.warning(f"获取知识库列表失败: {e!s}")
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
@@ -229,8 +197,6 @@ class SessionManagementRoute(Route):
|
||||
"available_chat_providers": available_chat_providers,
|
||||
"available_stt_providers": available_stt_providers,
|
||||
"available_tts_providers": available_tts_providers,
|
||||
"available_plugins": available_plugins,
|
||||
"available_kbs": available_kbs,
|
||||
"available_rule_keys": AVAILABLE_SESSION_RULE_KEYS,
|
||||
}
|
||||
)
|
||||
@@ -263,11 +229,6 @@ class SessionManagementRoute(Route):
|
||||
if rule_key not in AVAILABLE_SESSION_RULE_KEYS:
|
||||
return Response().error(f"不支持的规则键: {rule_key}").__dict__
|
||||
|
||||
if rule_key == "session_plugin_config":
|
||||
rule_value = {
|
||||
umo: rule_value,
|
||||
}
|
||||
|
||||
# 使用 shared preferences 更新规则
|
||||
await sp.session_put(umo, rule_key, rule_value)
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
重构:
|
||||
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界
|
||||
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
|
||||
|
||||
优化:
|
||||
- Dify、阿里云百炼应用支持流式输出
|
||||
- 防止分段回复正则表达式解析错误导致消息不发送
|
||||
- 群聊上下文感知记录 At 信息
|
||||
- 优化模型提供商页面的测试提供商功能
|
||||
|
||||
新增:
|
||||
- 支持在配置文件页面快速测试对话
|
||||
- 为配置文件配置项内容添加国际化支持
|
||||
|
||||
修复:
|
||||
- 在更新 MCP Server 配置后,MCP 无法正常重启的问题
|
||||
@@ -1,22 +0,0 @@
|
||||
## What's Changed
|
||||
|
||||
### 修复了自定义规则页面无法设置插件和知识库的规则的问题
|
||||
|
||||
---
|
||||
|
||||
重构:
|
||||
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界。详见:[Agent 执行器](https://docs.astrbot.app/use/agent-runner.html)
|
||||
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
|
||||
|
||||
优化:
|
||||
- Dify、阿里云百炼应用支持流式输出
|
||||
- 防止分段回复正则表达式解析错误导致消息不发送
|
||||
- 群聊上下文感知记录 At 信息
|
||||
- 优化模型提供商页面的测试提供商功能
|
||||
|
||||
新增:
|
||||
- 支持在配置文件页面快速测试对话
|
||||
- 为配置文件配置项内容添加国际化支持
|
||||
|
||||
修复:
|
||||
- 在更新 MCP Server 配置后,MCP 无法正常重启的问题
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
@@ -84,7 +84,7 @@
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:disabled="isStreaming"
|
||||
:disabled="isStreaming || isConvRunning"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
:session-id="sessionId || null"
|
||||
:platform-id="sessionPlatformId"
|
||||
:is-group="sessionIsGroup"
|
||||
:initial-config-id="props.configId"
|
||||
@config-changed="handleConfigChange"
|
||||
/>
|
||||
<ProviderModelSelector v-if="showProviderSelector" ref="providerModelSelectorRef" />
|
||||
@@ -80,13 +79,11 @@ interface Props {
|
||||
isRecording: boolean;
|
||||
sessionId?: string | null;
|
||||
currentSession?: Session | null;
|
||||
configId?: string | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
sessionId: null,
|
||||
currentSession: null,
|
||||
configId: null
|
||||
currentSession: null
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -90,12 +90,10 @@ const props = withDefaults(defineProps<{
|
||||
sessionId?: string | null;
|
||||
platformId?: string;
|
||||
isGroup?: boolean;
|
||||
initialConfigId?: string | null;
|
||||
}>(), {
|
||||
sessionId: null,
|
||||
platformId: 'webchat',
|
||||
isGroup: false,
|
||||
initialConfigId: null
|
||||
isGroup: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ 'config-changed': [ConfigChangedPayload] }>();
|
||||
@@ -293,7 +291,7 @@ watch(
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchConfigList();
|
||||
const stored = props.initialConfigId || localStorage.getItem(STORAGE_KEY) || 'default';
|
||||
const stored = localStorage.getItem(STORAGE_KEY) || 'default';
|
||||
selectedConfigId.value = stored;
|
||||
await setSelection(stored);
|
||||
await syncSelectionForSession();
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
<template>
|
||||
<v-card class="standalone-chat-card" elevation="0" rounded="0">
|
||||
<v-card-text class="standalone-chat-container">
|
||||
<div class="chat-layout">
|
||||
<!-- 聊天内容区域 -->
|
||||
<div class="chat-content-panel">
|
||||
<MessageList v-if="messages && messages.length > 0" :messages="messages" :isDark="isDark"
|
||||
:isStreaming="isStreaming || isConvRunning" @openImagePreview="openImagePreview"
|
||||
ref="messageList" />
|
||||
<div class="welcome-container fade-in" v-else>
|
||||
<div class="welcome-title">
|
||||
<span>Hello, I'm</span>
|
||||
<span class="bot-name">AstrBot ⭐</span>
|
||||
</div>
|
||||
<p class="text-caption text-medium-emphasis mt-2">
|
||||
测试配置: {{ configId || 'default' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<ChatInput
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:disabled="isStreaming"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:config-id="configId"
|
||||
@send="handleSendMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
|
||||
<v-card class="image-preview-card" elevation="8">
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||
<span>{{ t('core.common.imagePreview') }}</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" />
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center pa-4">
|
||||
<img :src="previewImageUrl" class="preview-image-large" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { useTheme } from 'vuetify';
|
||||
import MessageList from '@/components/chat/MessageList.vue';
|
||||
import ChatInput from '@/components/chat/ChatInput.vue';
|
||||
import { useMessages } from '@/composables/useMessages';
|
||||
import { useMediaHandling } from '@/composables/useMediaHandling';
|
||||
import { useRecording } from '@/composables/useRecording';
|
||||
import { useToast } from '@/utils/toast';
|
||||
|
||||
interface Props {
|
||||
configId?: string | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
configId: null
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { error: showError } = useToast();
|
||||
|
||||
// UI 状态
|
||||
const imagePreviewDialog = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
|
||||
// 会话管理(不使用 useSessions 避免路由跳转)
|
||||
const currSessionId = ref('');
|
||||
const getCurrentSession = computed(() => null); // 独立测试模式不需要会话信息
|
||||
|
||||
async function newSession() {
|
||||
try {
|
||||
const response = await axios.get('/api/chat/new_session');
|
||||
const sessionId = response.data.data.session_id;
|
||||
currSessionId.value = sessionId;
|
||||
return sessionId;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSessionTitle(sessionId: string, title: string) {
|
||||
// 独立模式不需要更新会话标题
|
||||
}
|
||||
|
||||
function getSessions() {
|
||||
// 独立模式不需要加载会话列表
|
||||
}
|
||||
|
||||
const {
|
||||
stagedImagesName,
|
||||
stagedImagesUrl,
|
||||
stagedAudioUrl,
|
||||
getMediaFile,
|
||||
processAndUploadImage,
|
||||
handlePaste,
|
||||
removeImage,
|
||||
removeAudio,
|
||||
clearStaged,
|
||||
cleanupMediaCache
|
||||
} = useMediaHandling();
|
||||
|
||||
const { isRecording, startRecording: startRec, stopRecording: stopRec } = useRecording();
|
||||
|
||||
const {
|
||||
messages,
|
||||
isStreaming,
|
||||
isConvRunning,
|
||||
enableStreaming,
|
||||
getSessionMessages: getSessionMsg,
|
||||
sendMessage: sendMsg,
|
||||
toggleStreaming
|
||||
} = useMessages(currSessionId, getMediaFile, updateSessionTitle, getSessions);
|
||||
|
||||
// 组件引用
|
||||
const messageList = ref<InstanceType<typeof MessageList> | null>(null);
|
||||
const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
|
||||
|
||||
// 输入状态
|
||||
const prompt = ref('');
|
||||
|
||||
const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark');
|
||||
|
||||
function openImagePreview(imageUrl: string) {
|
||||
previewImageUrl.value = imageUrl;
|
||||
imagePreviewDialog.value = true;
|
||||
}
|
||||
|
||||
async function handleStartRecording() {
|
||||
await startRec();
|
||||
}
|
||||
|
||||
async function handleStopRecording() {
|
||||
const audioFilename = await stopRec();
|
||||
stagedAudioUrl.value = audioFilename;
|
||||
}
|
||||
|
||||
async function handleFileSelect(files: FileList) {
|
||||
for (const file of files) {
|
||||
await processAndUploadImage(file);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendMessage() {
|
||||
if (!prompt.value.trim() && stagedImagesName.value.length === 0 && !stagedAudioUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!currSessionId.value) {
|
||||
await newSession();
|
||||
}
|
||||
|
||||
const promptToSend = prompt.value.trim();
|
||||
const imageNamesToSend = [...stagedImagesName.value];
|
||||
const audioNameToSend = stagedAudioUrl.value;
|
||||
|
||||
// 清空输入和附件
|
||||
prompt.value = '';
|
||||
clearStaged();
|
||||
|
||||
// 获取选择的提供商和模型
|
||||
const selection = chatInputRef.value?.getCurrentSelection();
|
||||
const selectedProviderId = selection?.providerId || '';
|
||||
const selectedModelName = selection?.modelName || '';
|
||||
|
||||
await sendMsg(
|
||||
promptToSend,
|
||||
imageNamesToSend,
|
||||
audioNameToSend,
|
||||
selectedProviderId,
|
||||
selectedModelName
|
||||
);
|
||||
|
||||
// 滚动到底部
|
||||
nextTick(() => {
|
||||
messageList.value?.scrollToBottom();
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to send message:', err);
|
||||
showError(t('features.chat.errors.sendMessageFailed'));
|
||||
// 恢复输入内容,让用户可以重试
|
||||
// 注意:附件已经上传到服务器,所以不恢复附件
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 独立模式在挂载时创建新会话
|
||||
try {
|
||||
await newSession();
|
||||
} catch (err) {
|
||||
console.error('Failed to create initial session:', err);
|
||||
showError(t('features.chat.errors.createSessionFailed'));
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupMediaCache();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.standalone-chat-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.standalone-chat-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-layout {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-content-panel {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.conversation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
padding-left: 16px;
|
||||
border-bottom: 1px solid var(--v-theme-border);
|
||||
width: 100%;
|
||||
padding-right: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conversation-header-info h4 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.conversation-header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.welcome-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bot-name {
|
||||
font-weight: 700;
|
||||
margin-left: 8px;
|
||||
color: var(--v-theme-secondary);
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.preview-image-large {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -85,9 +85,5 @@
|
||||
"reconnected": "Chat connection re-established",
|
||||
"failed": "Connection failed, please refresh the page"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"sendMessageFailed": "Failed to send message, please try again",
|
||||
"createSessionFailed": "Failed to create session, please refresh the page"
|
||||
}
|
||||
}
|
||||
@@ -36,16 +36,13 @@
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"umoInfo": "Unified Message Origin",
|
||||
"umoInfo": "Session Info",
|
||||
"rulesOverview": "Rules Overview",
|
||||
"actions": "Actions"
|
||||
}
|
||||
},
|
||||
"persona": {
|
||||
"none": "Follow Config"
|
||||
},
|
||||
"provider": {
|
||||
"followConfig": "Follow Config"
|
||||
"none": "No Persona"
|
||||
},
|
||||
"addRule": {
|
||||
"title": "Add Custom Rule",
|
||||
@@ -73,17 +70,6 @@
|
||||
"title": "Persona Configuration",
|
||||
"selectPersona": "Select Persona",
|
||||
"hint": "Persona settings affect the conversation style and behavior of the LLM"
|
||||
},
|
||||
"pluginConfig": {
|
||||
"title": "Plugin Configuration",
|
||||
"disabledPlugins": "Disabled Plugins",
|
||||
"hint": "Select plugins to disable for this session. Unselected plugins will remain enabled."
|
||||
},
|
||||
"kbConfig": {
|
||||
"title": "Knowledge Base Configuration",
|
||||
"selectKbs": "Select Knowledge Bases",
|
||||
"topK": "Top K Results",
|
||||
"enableRerank": "Enable Reranking"
|
||||
}
|
||||
},
|
||||
"deleteConfirm": {
|
||||
|
||||
@@ -85,9 +85,5 @@
|
||||
"reconnected": "聊天连接已重新建立",
|
||||
"failed": "连接失败,请刷新页面重试"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"sendMessageFailed": "发送消息失败,请重试",
|
||||
"createSessionFailed": "创建会话失败,请刷新页面重试"
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,7 @@
|
||||
}
|
||||
},
|
||||
"persona": {
|
||||
"none": "跟随配置文件"
|
||||
},
|
||||
"provider": {
|
||||
"followConfig": "跟随配置文件"
|
||||
"none": "无人格"
|
||||
},
|
||||
"addRule": {
|
||||
"title": "添加自定义规则",
|
||||
@@ -72,18 +69,7 @@
|
||||
"personaConfig": {
|
||||
"title": "人格配置",
|
||||
"selectPersona": "选择人格",
|
||||
"hint": "应用人格配置后,将会强制该来源的所有对话使用该人格。"
|
||||
},
|
||||
"pluginConfig": {
|
||||
"title": "插件配置",
|
||||
"disabledPlugins": "禁用的插件",
|
||||
"hint": "选择要在此会话中禁用的插件。未选择的插件将保持启用状态。"
|
||||
},
|
||||
"kbConfig": {
|
||||
"title": "知识库配置",
|
||||
"selectKbs": "选择知识库",
|
||||
"topK": "返回结果数量 (Top K)",
|
||||
"enableRerank": "启用重排序"
|
||||
"hint": "人格配置会影响 LLM 的对话风格和行为"
|
||||
}
|
||||
},
|
||||
"deleteConfirm": {
|
||||
|
||||
@@ -69,7 +69,7 @@ const sidebarItem: menu[] = [
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.sessionManagement',
|
||||
icon: 'mdi-pencil-ruler',
|
||||
icon: 'mdi-account-group',
|
||||
to: '/session-management'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -45,15 +45,6 @@
|
||||
@click="configToString(); codeEditorDialog = true">
|
||||
</v-btn>
|
||||
|
||||
<v-tooltip text="测试当前配置" location="left" v-if="!isSystemConfig">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-chat-processing" size="x-large"
|
||||
style="position: fixed; right: 52px; bottom: 196px;" color="secondary"
|
||||
@click="openTestChat">
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
|
||||
@@ -144,34 +135,6 @@
|
||||
</v-snackbar>
|
||||
|
||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||
|
||||
<!-- 测试聊天抽屉 -->
|
||||
<v-overlay
|
||||
v-model="testChatDrawer"
|
||||
class="test-chat-overlay"
|
||||
location="right"
|
||||
transition="slide-x-reverse-transition"
|
||||
:scrim="true"
|
||||
@click:outside="closeTestChat"
|
||||
>
|
||||
<v-card class="test-chat-card" elevation="12">
|
||||
<div class="test-chat-header">
|
||||
<div>
|
||||
<span class="text-h6">测试配置</span>
|
||||
<div v-if="selectedConfigInfo.name" class="text-caption text-grey">
|
||||
{{ selectedConfigInfo.name }} ({{ testConfigId }})
|
||||
</div>
|
||||
</div>
|
||||
<v-btn icon variant="text" @click="closeTestChat">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
<div class="test-chat-content">
|
||||
<StandaloneChat v-if="testChatDrawer" :configId="testConfigId" />
|
||||
</div>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -179,7 +142,6 @@
|
||||
import axios from 'axios';
|
||||
import AstrBotCoreConfigWrapper from '@/components/config/AstrBotCoreConfigWrapper.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import StandaloneChat from '@/components/chat/StandaloneChat.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
@@ -188,8 +150,7 @@ export default {
|
||||
components: {
|
||||
AstrBotCoreConfigWrapper,
|
||||
VueMonacoEditor,
|
||||
WaitingForRestart,
|
||||
StandaloneChat
|
||||
WaitingForRestart
|
||||
},
|
||||
props: {
|
||||
initialConfigId: {
|
||||
@@ -277,10 +238,6 @@ export default {
|
||||
name: '',
|
||||
},
|
||||
editingConfigId: null,
|
||||
|
||||
// 测试聊天
|
||||
testChatDrawer: false,
|
||||
testConfigId: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -549,20 +506,6 @@ export default {
|
||||
this.getConfigInfoList("default");
|
||||
}
|
||||
}
|
||||
},
|
||||
openTestChat() {
|
||||
if (!this.selectedConfigID) {
|
||||
this.save_message = "请先选择一个配置文件";
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "warning";
|
||||
return;
|
||||
}
|
||||
this.testConfigId = this.selectedConfigID;
|
||||
this.testChatDrawer = true;
|
||||
},
|
||||
closeTestChat() {
|
||||
this.testChatDrawer = false;
|
||||
this.testConfigId = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -622,32 +565,4 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 测试聊天抽屉样式 */
|
||||
.test-chat-overlay {
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.test-chat-card {
|
||||
width: clamp(320px, 50vw, 720px);
|
||||
height: calc(100vh - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.test-chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 12px 20px;
|
||||
}
|
||||
|
||||
.test-chat-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border-radius: 0 0 16px 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,6 @@ import axios from 'axios';
|
||||
import { pinyin } from 'pinyin-pro';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import defaultPluginIcon from '@/assets/images/plugin_icon.png';
|
||||
|
||||
import { ref, computed, onMounted, reactive, inject, watch } from 'vue';
|
||||
|
||||
@@ -940,7 +939,7 @@ watch(marketSearch, (newVal) => {
|
||||
|
||||
<v-row style="min-height: 26rem;">
|
||||
<v-col v-for="plugin in paginatedPlugins" :key="plugin.name" cols="12" md="6" lg="4">
|
||||
<v-card class="rounded-lg d-flex flex-column plugin-card" elevation="0"
|
||||
<v-card class="rounded-lg d-flex flex-column" elevation="0"
|
||||
style=" height: 12rem; position: relative;">
|
||||
|
||||
<!-- 推荐标记 -->
|
||||
@@ -951,8 +950,8 @@ watch(marketSearch, (newVal) => {
|
||||
|
||||
<v-card-text
|
||||
style="padding: 12px; padding-bottom: 8px; display: flex; gap: 12px; width: 100%; flex: 1; overflow: hidden;">
|
||||
<div style="flex-shrink: 0;">
|
||||
<img :src="plugin?.logo || defaultPluginIcon" :alt="plugin.name"
|
||||
<div v-if="plugin?.logo" style="flex-shrink: 0;">
|
||||
<img :src="plugin.logo" :alt="plugin.name"
|
||||
style="height: 75px; width: 75px; border-radius: 8px; object-fit: cover;" />
|
||||
</div>
|
||||
|
||||
@@ -987,7 +986,8 @@ watch(marketSearch, (newVal) => {
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="text-caption plugin-description">
|
||||
<div class="text-caption"
|
||||
style="overflow: scroll; color: rgba(var(--v-theme-on-surface), 0.6); line-height: 1.3; margin-bottom: 6px; flex: 1;">
|
||||
{{ plugin.desc }}
|
||||
</div>
|
||||
|
||||
@@ -1246,36 +1246,4 @@ watch(marketSearch, (newVal) => {
|
||||
border-radius: 5px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.plugin-description {
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
line-height: 1.3;
|
||||
margin-bottom: 6px;
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.plugin-card:hover .plugin-description {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(var(--v-theme-primary-rgb), 0.4);
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(var(--v-theme-primary-rgb), 0.6);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -69,25 +69,6 @@
|
||||
:loading="isProviderTesting(provider.id)" @toggle-enabled="providerStatusChange"
|
||||
:bglogo="getProviderIcon(provider.provider)" @delete="deleteProvider" @edit="configExistingProvider"
|
||||
@copy="copyProvider" :show-copy-button="true">
|
||||
<template #item-details="{ item }">
|
||||
<!-- 测试状态 chip -->
|
||||
<v-tooltip v-if="getProviderStatus(item.id)" location="top" max-width="300">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip v-bind="props" :color="getStatusColor(getProviderStatus(item.id).status)" size="small">
|
||||
<v-icon start size="small">
|
||||
{{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
|
||||
getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
|
||||
'mdi-clock-outline' }}
|
||||
</v-icon>
|
||||
{{ getStatusText(getProviderStatus(item.id).status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<span v-if="getProviderStatus(item.id).status === 'unavailable'">
|
||||
{{ getProviderStatus(item.id).error }}
|
||||
</span>
|
||||
<span v-else>{{ getStatusText(getProviderStatus(item.id).status) }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<v-btn style="z-index: 100000;" variant="tonal" color="info" rounded="xl" size="small"
|
||||
:loading="isProviderTesting(item.id)" @click="testSingleProvider(item)">
|
||||
@@ -115,38 +96,73 @@
|
||||
:loading="isProviderTesting(provider.id)" @toggle-enabled="providerStatusChange"
|
||||
:bglogo="getProviderIcon(provider.provider)" @delete="deleteProvider" @edit="configExistingProvider"
|
||||
@copy="copyProvider" :show-copy-button="true">
|
||||
|
||||
<template #item-details="{ item }">
|
||||
<!-- 测试状态 chip -->
|
||||
<v-tooltip v-if="getProviderStatus(item.id)" location="top" max-width="300">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip v-bind="props" :color="getStatusColor(getProviderStatus(item.id).status)" size="small">
|
||||
<v-icon start size="small">
|
||||
{{ getProviderStatus(item.id).status === 'available' ? 'mdi-check-circle' :
|
||||
getProviderStatus(item.id).status === 'unavailable' ? 'mdi-alert-circle' :
|
||||
'mdi-clock-outline' }}
|
||||
</v-icon>
|
||||
{{ getStatusText(getProviderStatus(item.id).status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<span v-if="getProviderStatus(item.id).status === 'unavailable'">
|
||||
{{ getProviderStatus(item.id).error }}
|
||||
</span>
|
||||
<span v-else>{{ getStatusText(getProviderStatus(item.id).status) }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<v-btn style="z-index: 100000;" variant="tonal" color="info" rounded="xl" size="small"
|
||||
:loading="isProviderTesting(item.id)" @click="testSingleProvider(item)">
|
||||
{{ tm('availability.test') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:details="{ item }">
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 供应商状态部分 -->
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<v-icon class="me-2">mdi-heart-pulse</v-icon>
|
||||
<span class="text-h4">{{ tm('availability.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="tonal" :loading="testingProviders.length > 0" @click="fetchProviderStatus">
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
{{ tm('availability.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn variant="text" color="primary" @click="showStatus = !showStatus" style="margin-left: 8px;">
|
||||
{{ showStatus ? tm('logs.collapse') : tm('logs.expand') }}
|
||||
<v-icon>{{ showStatus ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-0" v-if="showStatus">
|
||||
<v-card-text class="px-4 py-3">
|
||||
<v-alert v-if="providerStatuses.length === 0" type="info" variant="tonal">
|
||||
{{ tm('availability.noData') }}
|
||||
</v-alert>
|
||||
|
||||
<v-container v-else class="pa-0">
|
||||
<v-row>
|
||||
<v-col v-for="status in providerStatuses" :key="status.id" cols="12" sm="6" md="4">
|
||||
<v-card variant="outlined" class="status-card" :class="`status-${status.status}`">
|
||||
<v-card-item>
|
||||
<v-icon v-if="status.status === 'available'" color="success"
|
||||
class="me-2">mdi-check-circle</v-icon>
|
||||
<v-icon v-else-if="status.status === 'unavailable'" color="error"
|
||||
class="me-2">mdi-alert-circle</v-icon>
|
||||
<v-progress-circular v-else-if="status.status === 'pending'" indeterminate color="primary"
|
||||
size="20" width="2" class="me-2"></v-progress-circular>
|
||||
|
||||
<span class="font-weight-bold">{{ status.id }}</span>
|
||||
|
||||
<v-chip :color="getStatusColor(status.status)" size="small" class="ml-2">
|
||||
{{ getStatusText(status.status) }}
|
||||
</v-chip>
|
||||
</v-card-item>
|
||||
<v-card-text v-if="status.status === 'unavailable'" class="text-caption text-medium-emphasis">
|
||||
<span class="font-weight-bold">{{ tm('availability.errorMessage') }}:</span> {{ status.error }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card-text>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
|
||||
<!-- 日志部分 -->
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
@@ -735,14 +751,11 @@ export default {
|
||||
return this.testingProviders.includes(providerId);
|
||||
},
|
||||
|
||||
getProviderStatus(providerId) {
|
||||
return this.providerStatuses.find(s => s.id === providerId);
|
||||
},
|
||||
|
||||
async testSingleProvider(provider) {
|
||||
if (this.isProviderTesting(provider.id)) return;
|
||||
|
||||
this.testingProviders.push(provider.id);
|
||||
this.showStatus = true; // 自动展开状态部分
|
||||
|
||||
// 更新UI为pending状态
|
||||
const statusIndex = this.providerStatuses.findIndex(s => s.id === provider.id);
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
<v-card flat>
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
<span class="text-h4">{{ tm('customRules.title') }}</span>
|
||||
<v-btn icon="mdi-information-outline" size="small" variant="text" href="https://astrbot.app/use/custom-rules.html" target="_blank"></v-btn>
|
||||
<v-chip size="small" class="ml-1">{{ totalItems }} {{ tm('customRules.rulesCount') }}</v-chip>
|
||||
<v-chip size="small" class="ml-2">{{ totalItems }} {{ tm('customRules.rulesCount') }}</v-chip>
|
||||
<v-row class="me-4 ms-4" dense>
|
||||
<v-text-field v-model="searchQuery" prepend-inner-icon="mdi-magnify" :label="tm('search.placeholder')"
|
||||
hide-details clearable variant="solo-filled" flat class="me-4" density="compact"></v-text-field>
|
||||
@@ -29,8 +28,8 @@
|
||||
<v-card-text class="pa-0">
|
||||
<v-data-table-server :headers="headers" :items="filteredRulesList" :loading="loading"
|
||||
:items-length="totalItems" v-model:items-per-page="itemsPerPage" v-model:page="currentPage"
|
||||
@update:options="onTableOptionsUpdate" class="elevation-0" style="font-size: 12px;" v-model="selectedItems"
|
||||
show-select item-value="umo" return-object>
|
||||
@update:options="onTableOptionsUpdate" class="elevation-0" style="font-size: 12px;"
|
||||
v-model="selectedItems" show-select item-value="umo" return-object>
|
||||
|
||||
<!-- UMO 信息 -->
|
||||
<template v-slot:item.umo_info="{ item }">
|
||||
@@ -41,8 +40,7 @@
|
||||
</v-chip>
|
||||
<span class="text-truncate" style="max-width: 300px;">{{ item.umo }}</span>
|
||||
<div class="d-flex align-center" v-if="item.rules?.session_service_config?.custom_name || true">
|
||||
<span class="ml-2" style="color: gray; font-size: 10px;"
|
||||
v-if="item.rules?.session_service_config?.custom_name">
|
||||
<span class="ml-2" style="color: gray; font-size: 10px;" v-if="item.rules?.session_service_config?.custom_name">
|
||||
({{ item.rules?.session_service_config?.custom_name }})
|
||||
</span>
|
||||
<v-btn icon size="x-small" variant="text" class="ml-1" @click.stop="openQuickEditName(item)">
|
||||
@@ -143,11 +141,11 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 规则编辑对话框 -->
|
||||
<v-dialog v-model="ruleDialog" max-width="550" scrollable>
|
||||
<v-dialog v-model="ruleDialog" max-width="700" scrollable>
|
||||
<v-card v-if="selectedUmo" class="d-flex flex-column" height="600">
|
||||
<v-card-title class="py-3 px-6 d-flex align-center border-b">
|
||||
<span>{{ tm('ruleEditor.title') }}</span>
|
||||
<v-chip size="x-small" class="ml-2 font-weight-regular" variant="outlined">
|
||||
<v-chip size="small" class="ml-4 font-weight-regular" variant="outlined">
|
||||
{{ selectedUmo.umo }}
|
||||
</v-chip>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -181,6 +179,9 @@
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn variant="text" color="error" size="small" @click="clearServiceConfig" class="mr-2">
|
||||
{{ tm('buttons.clear') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="saveServiceConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
@@ -196,21 +197,24 @@
|
||||
<v-col cols="12">
|
||||
<v-select v-model="providerConfig.chat_completion" :items="chatProviderOptions" item-title="label"
|
||||
item-value="value" :label="tm('ruleEditor.providerConfig.chatProvider')" variant="outlined"
|
||||
hide-details class="mb-2" />
|
||||
hide-details clearable class="mb-2" />
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-select v-model="providerConfig.speech_to_text" :items="sttProviderOptions" item-title="label"
|
||||
item-value="value" :label="tm('ruleEditor.providerConfig.sttProvider')" variant="outlined"
|
||||
hide-details :disabled="availableSttProviders.length === 0" class="mb-2" />
|
||||
hide-details clearable :disabled="sttProviderOptions.length === 0" class="mb-2" />
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-select v-model="providerConfig.text_to_speech" :items="ttsProviderOptions" item-title="label"
|
||||
item-value="value" :label="tm('ruleEditor.providerConfig.ttsProvider')" variant="outlined"
|
||||
hide-details :disabled="availableTtsProviders.length === 0" />
|
||||
hide-details clearable :disabled="ttsProviderOptions.length === 0" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn variant="text" color="error" size="small" @click="clearProviderConfig" class="mr-2">
|
||||
{{ tm('buttons.clear') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="saveProviderConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
@@ -241,59 +245,6 @@
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Config Section -->
|
||||
<div class="d-flex align-center mb-4 mt-4">
|
||||
<h3 class="font-weight-bold mb-0">{{ tm('ruleEditor.pluginConfig.title') }}</h3>
|
||||
</div>
|
||||
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<v-select v-model="pluginConfig.disabled_plugins" :items="pluginOptions" item-title="label"
|
||||
item-value="value" :label="tm('ruleEditor.pluginConfig.disabledPlugins')" variant="outlined"
|
||||
hide-details multiple chips closable-chips clearable />
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-alert type="info" variant="tonal" class="mt-2" icon="mdi-information-outline">
|
||||
{{ tm('ruleEditor.pluginConfig.hint') }}
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="savePluginConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- KB Config Section -->
|
||||
<div class="d-flex align-center mb-4 mt-4">
|
||||
<h3 class="font-weight-bold mb-0">{{ tm('ruleEditor.kbConfig.title') }}</h3>
|
||||
</div>
|
||||
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<v-select v-model="kbConfig.kb_ids" :items="kbOptions" item-title="label" item-value="value" :disabled="availableKbs.length === 0"
|
||||
:label="tm('ruleEditor.kbConfig.selectKbs')" variant="outlined" hide-details multiple chips
|
||||
closable-chips clearable />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field v-model.number="kbConfig.top_k" :label="tm('ruleEditor.kbConfig.topK')"
|
||||
variant="outlined" hide-details type="number" min="1" max="20" class="mt-3"/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-checkbox v-model="kbConfig.enable_rerank" :label="tm('ruleEditor.kbConfig.enableRerank')"
|
||||
color="primary" hide-details class="mt-3"/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn color="primary" variant="tonal" size="small" @click="saveKbConfig" :loading="saving"
|
||||
prepend-icon="mdi-content-save">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -400,8 +351,6 @@ export default {
|
||||
availableChatProviders: [],
|
||||
availableSttProviders: [],
|
||||
availableTtsProviders: [],
|
||||
availablePlugins: [],
|
||||
availableKbs: [],
|
||||
|
||||
// 添加规则
|
||||
addRuleDialog: false,
|
||||
@@ -429,19 +378,6 @@ export default {
|
||||
text_to_speech: null,
|
||||
},
|
||||
|
||||
// 插件配置
|
||||
pluginConfig: {
|
||||
enabled_plugins: [],
|
||||
disabled_plugins: [],
|
||||
},
|
||||
|
||||
// 知识库配置
|
||||
kbConfig: {
|
||||
kb_ids: [],
|
||||
top_k: 5,
|
||||
enable_rerank: true,
|
||||
},
|
||||
|
||||
// 删除确认
|
||||
deleteDialog: false,
|
||||
deleteTarget: null,
|
||||
@@ -487,47 +423,24 @@ export default {
|
||||
},
|
||||
|
||||
chatProviderOptions() {
|
||||
return [
|
||||
{ label: this.tm('provider.followConfig'), value: null },
|
||||
...this.availableChatProviders.map(p => ({
|
||||
return this.availableChatProviders.map(p => ({
|
||||
label: `${p.name} (${p.model})`,
|
||||
value: p.id
|
||||
}))
|
||||
]
|
||||
},
|
||||
|
||||
sttProviderOptions() {
|
||||
return [
|
||||
{ label: this.tm('provider.followConfig'), value: null },
|
||||
...this.availableSttProviders.map(p => ({
|
||||
return this.availableSttProviders.map(p => ({
|
||||
label: `${p.name} (${p.model})`,
|
||||
value: p.id
|
||||
}))
|
||||
]
|
||||
},
|
||||
|
||||
ttsProviderOptions() {
|
||||
return [
|
||||
{ label: this.tm('provider.followConfig'), value: null },
|
||||
...this.availableTtsProviders.map(p => ({
|
||||
return this.availableTtsProviders.map(p => ({
|
||||
label: `${p.name} (${p.model})`,
|
||||
value: p.id
|
||||
}))
|
||||
]
|
||||
},
|
||||
|
||||
pluginOptions() {
|
||||
return this.availablePlugins.map(p => ({
|
||||
label: p.display_name || p.name,
|
||||
value: p.name
|
||||
}))
|
||||
},
|
||||
|
||||
kbOptions() {
|
||||
return this.availableKbs.map(kb => ({
|
||||
label: `${kb.emoji || '📚'} ${kb.kb_name}`,
|
||||
value: kb.kb_id
|
||||
}))
|
||||
},
|
||||
},
|
||||
|
||||
@@ -574,8 +487,6 @@ export default {
|
||||
this.availableChatProviders = data.available_chat_providers
|
||||
this.availableSttProviders = data.available_stt_providers
|
||||
this.availableTtsProviders = data.available_tts_providers
|
||||
this.availablePlugins = data.available_plugins || []
|
||||
this.availableKbs = data.available_kbs || []
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.loadError'))
|
||||
}
|
||||
@@ -673,21 +584,6 @@ export default {
|
||||
text_to_speech: this.editingRules['provider_perf_text_to_speech'] || null,
|
||||
}
|
||||
|
||||
// 初始化插件配置
|
||||
const pluginCfg = this.editingRules.session_plugin_config || {}
|
||||
this.pluginConfig = {
|
||||
enabled_plugins: pluginCfg.enabled_plugins || [],
|
||||
disabled_plugins: pluginCfg.disabled_plugins || [],
|
||||
}
|
||||
|
||||
// 初始化知识库配置
|
||||
const kbCfg = this.editingRules.kb_config || {}
|
||||
this.kbConfig = {
|
||||
kb_ids: kbCfg.kb_ids || [],
|
||||
top_k: kbCfg.top_k ?? 5,
|
||||
enable_rerank: kbCfg.enable_rerank !== false,
|
||||
}
|
||||
|
||||
this.ruleDialog = true
|
||||
},
|
||||
|
||||
@@ -740,40 +636,68 @@ export default {
|
||||
this.saving = false
|
||||
},
|
||||
|
||||
async clearServiceConfig() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
this.saving = true
|
||||
try {
|
||||
const response = await axios.post('/api/session/delete-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
rule_key: 'session_service_config'
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(this.tm('messages.clearSuccess'))
|
||||
delete this.editingRules.session_service_config
|
||||
this.serviceConfig = {
|
||||
session_enabled: true,
|
||||
llm_enabled: true,
|
||||
tts_enabled: true,
|
||||
custom_name: '',
|
||||
persona_id: null,
|
||||
}
|
||||
// 更新列表中的数据
|
||||
const item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
if (item) {
|
||||
delete item.rules.session_service_config
|
||||
// 如果没有任何规则了,从列表中移除
|
||||
if (Object.keys(item.rules).length === 0) {
|
||||
const index = this.rulesList.findIndex(u => u.umo === this.selectedUmo.umo)
|
||||
if (index > -1) this.rulesList.splice(index, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.clearError'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.clearError'))
|
||||
}
|
||||
this.saving = false
|
||||
},
|
||||
|
||||
async saveProviderConfig() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
this.saving = true
|
||||
try {
|
||||
const updateTasks = []
|
||||
const deleteTasks = []
|
||||
const tasks = []
|
||||
const providerTypes = ['chat_completion', 'speech_to_text', 'text_to_speech']
|
||||
|
||||
for (const type of providerTypes) {
|
||||
const value = this.providerConfig[type]
|
||||
if (value) {
|
||||
// 有值时更新
|
||||
updateTasks.push(
|
||||
tasks.push(
|
||||
axios.post('/api/session/update-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
rule_key: `provider_perf_${type}`,
|
||||
rule_value: value
|
||||
})
|
||||
)
|
||||
} else if (this.editingRules[`provider_perf_${type}`]) {
|
||||
// 选择了"跟随配置文件"(null)且之前有配置,则删除
|
||||
deleteTasks.push(
|
||||
axios.post('/api/session/delete-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
rule_key: `provider_perf_${type}`
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const allTasks = [...updateTasks, ...deleteTasks]
|
||||
if (allTasks.length > 0) {
|
||||
await Promise.all(allTasks)
|
||||
if (tasks.length > 0) {
|
||||
await Promise.all(tasks)
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
|
||||
// 更新或添加到列表
|
||||
@@ -792,10 +716,6 @@ export default {
|
||||
if (this.providerConfig[type]) {
|
||||
item.rules[`provider_perf_${type}`] = this.providerConfig[type]
|
||||
this.editingRules[`provider_perf_${type}`] = this.providerConfig[type]
|
||||
} else {
|
||||
// 删除本地数据
|
||||
delete item.rules[`provider_perf_${type}`]
|
||||
delete this.editingRules[`provider_perf_${type}`]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -807,113 +727,42 @@ export default {
|
||||
this.saving = false
|
||||
},
|
||||
|
||||
async savePluginConfig() {
|
||||
async clearProviderConfig() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
this.saving = true
|
||||
try {
|
||||
const config = {
|
||||
enabled_plugins: this.pluginConfig.enabled_plugins,
|
||||
disabled_plugins: this.pluginConfig.disabled_plugins,
|
||||
}
|
||||
|
||||
// 如果两个列表都为空,删除配置
|
||||
if (config.enabled_plugins.length === 0 && config.disabled_plugins.length === 0) {
|
||||
if (this.editingRules.session_plugin_config) {
|
||||
await axios.post('/api/session/delete-rule', {
|
||||
const providerTypes = ['chat_completion', 'speech_to_text', 'text_to_speech']
|
||||
const tasks = providerTypes.map(type =>
|
||||
axios.post('/api/session/delete-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
rule_key: 'session_plugin_config'
|
||||
rule_key: `provider_perf_${type}`
|
||||
})
|
||||
delete this.editingRules.session_plugin_config
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
if (item) delete item.rules.session_plugin_config
|
||||
)
|
||||
|
||||
await Promise.all(tasks)
|
||||
this.showSuccess(this.tm('messages.clearSuccess'))
|
||||
|
||||
// 更新本地数据
|
||||
this.providerConfig = {
|
||||
chat_completion: null,
|
||||
speech_to_text: null,
|
||||
text_to_speech: null,
|
||||
}
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
} else {
|
||||
const response = await axios.post('/api/session/update-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
rule_key: 'session_plugin_config',
|
||||
rule_value: config
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
this.editingRules.session_plugin_config = config
|
||||
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
const item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
if (item) {
|
||||
item.rules.session_plugin_config = config
|
||||
} else {
|
||||
this.rulesList.push({
|
||||
umo: this.selectedUmo.umo,
|
||||
platform: this.selectedUmo.platform,
|
||||
message_type: this.selectedUmo.message_type,
|
||||
session_id: this.selectedUmo.session_id,
|
||||
rules: { session_plugin_config: config }
|
||||
})
|
||||
for (const type of providerTypes) {
|
||||
delete item.rules[`provider_perf_${type}`]
|
||||
delete this.editingRules[`provider_perf_${type}`]
|
||||
}
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.saveError'))
|
||||
// 如果没有任何规则了,从列表中移除
|
||||
if (Object.keys(item.rules).length === 0) {
|
||||
const index = this.rulesList.findIndex(u => u.umo === this.selectedUmo.umo)
|
||||
if (index > -1) this.rulesList.splice(index, 1)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
|
||||
}
|
||||
this.saving = false
|
||||
},
|
||||
|
||||
async saveKbConfig() {
|
||||
if (!this.selectedUmo) return
|
||||
|
||||
this.saving = true
|
||||
try {
|
||||
const config = {
|
||||
kb_ids: this.kbConfig.kb_ids,
|
||||
top_k: this.kbConfig.top_k,
|
||||
enable_rerank: this.kbConfig.enable_rerank,
|
||||
}
|
||||
|
||||
// 如果 kb_ids 为空,删除配置
|
||||
if (config.kb_ids.length === 0) {
|
||||
if (this.editingRules.kb_config) {
|
||||
await axios.post('/api/session/delete-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
rule_key: 'kb_config'
|
||||
})
|
||||
delete this.editingRules.kb_config
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
if (item) delete item.rules.kb_config
|
||||
}
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
} else {
|
||||
const response = await axios.post('/api/session/update-rule', {
|
||||
umo: this.selectedUmo.umo,
|
||||
rule_key: 'kb_config',
|
||||
rule_value: config
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(this.tm('messages.saveSuccess'))
|
||||
this.editingRules.kb_config = config
|
||||
|
||||
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
|
||||
if (item) {
|
||||
item.rules.kb_config = config
|
||||
} else {
|
||||
this.rulesList.push({
|
||||
umo: this.selectedUmo.umo,
|
||||
platform: this.selectedUmo.platform,
|
||||
message_type: this.selectedUmo.message_type,
|
||||
session_id: this.selectedUmo.session_id,
|
||||
rules: { kb_config: config }
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('messages.saveError'))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
|
||||
this.showError(error.response?.data?.message || this.tm('messages.clearError'))
|
||||
}
|
||||
this.saving = false
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import defaultdict
|
||||
from astrbot import logger
|
||||
from astrbot.api import star
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
from astrbot.api.message_components import At, Image, Plain
|
||||
from astrbot.api.message_components import Image, Plain
|
||||
from astrbot.api.platform import MessageType
|
||||
from astrbot.api.provider import Provider, ProviderRequest
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
@@ -142,8 +142,6 @@ class LongTermMemory:
|
||||
logger.error(f"获取图片描述失败: {e}")
|
||||
else:
|
||||
parts.append(" [Image]")
|
||||
elif isinstance(comp, At):
|
||||
parts.append(f" [At: {comp.name}]")
|
||||
|
||||
final_message = "".join(parts)
|
||||
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
|
||||
|
||||
@@ -41,6 +41,7 @@ class ProcessLLMRequest:
|
||||
if default_persona:
|
||||
persona_id = default_persona["name"]
|
||||
|
||||
|
||||
persona = next(
|
||||
builtins.filter(
|
||||
lambda persona: persona["name"] == persona_id,
|
||||
|
||||
@@ -14,7 +14,7 @@ from astrbot.core.utils.session_waiter import (
|
||||
)
|
||||
|
||||
|
||||
class Main(Star):
|
||||
class Waiter(Star):
|
||||
"""会话控制"""
|
||||
|
||||
def __init__(self, context: Context):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.7.1"
|
||||
version = "4.6.1"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -21,17 +21,7 @@ async def core_lifecycle_td(tmp_path_factory):
|
||||
log_broker = LogBroker()
|
||||
core_lifecycle = AstrBotCoreLifecycle(log_broker, db)
|
||||
await core_lifecycle.initialize()
|
||||
try:
|
||||
yield core_lifecycle
|
||||
finally:
|
||||
# 优先停止核心生命周期以释放资源(包括关闭 MCP 等后台任务)
|
||||
try:
|
||||
_stop_res = core_lifecycle.stop()
|
||||
if asyncio.iscoroutine(_stop_res):
|
||||
await _stop_res
|
||||
except Exception:
|
||||
# 停止过程中如有异常,不影响后续清理
|
||||
pass
|
||||
return core_lifecycle
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -39,7 +39,6 @@ def plugin_manager_pm(tmp_path):
|
||||
message_history_manager = MagicMock()
|
||||
persona_manager = MagicMock()
|
||||
astrbot_config_mgr = MagicMock()
|
||||
knowledge_base_manager = MagicMock()
|
||||
|
||||
star_context = Context(
|
||||
event_queue,
|
||||
@@ -51,7 +50,6 @@ def plugin_manager_pm(tmp_path):
|
||||
message_history_manager,
|
||||
persona_manager,
|
||||
astrbot_config_mgr,
|
||||
knowledge_base_manager=knowledge_base_manager,
|
||||
)
|
||||
|
||||
# Create the PluginManager instance
|
||||
|
||||
Reference in New Issue
Block a user