Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16ec462abd | |||
| ca55465d3c | |||
| 7098c98dde | |||
| f56355da89 | |||
| 422160debd | |||
| 8062cf406a | |||
| 0e802232ec | |||
| f650a9205d | |||
| c85dbb2347 | |||
| a6a79128c8 | |||
| 42839627e8 | |||
| 267e68a894 | |||
| b32b444438 | |||
| 522d0f8313 | |||
| 5715e5de67 | |||
| cc6b05e8b3 | |||
| 417747d5d0 | |||
| a34f439226 | |||
| b7ca014fd0 | |||
| fa098d585a | |||
| c35a14e3ec | |||
| 60651736a5 | |||
| 581f9b7bd3 | |||
| 124eb04807 | |||
| 1d561da7fb | |||
| 16e3cd0784 | |||
| a6d91933dc | |||
| 445c40f758 | |||
| 725a841a3b | |||
| f77c453843 | |||
| ba6718d5bc | |||
| cdb7a1b3fa | |||
| a03c79b89d | |||
| 98800d3426 | |||
| a616adaac4 | |||
| ffb5605c99 | |||
| 621b556856 | |||
| a3ffecbb2a | |||
| e79487dd5f | |||
| 7fe1c1ec89 | |||
| 33b64ddf39 | |||
| 9713f96401 | |||
| 98d2e9bd27 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /AstrBot
|
||||
|
||||
COPY . /AstrBot/
|
||||
|
||||
@@ -83,29 +83,61 @@ class AstrBotConfig(dict):
|
||||
return conf
|
||||
|
||||
def check_config_integrity(self, refer_conf: Dict, conf: Dict, path=""):
|
||||
"""检查配置完整性,如果有新的配置项则返回 True"""
|
||||
"""检查配置完整性,如果有新的配置项或顺序不一致则返回 True"""
|
||||
has_new = False
|
||||
|
||||
# 创建一个新的有序字典以保持参考配置的顺序
|
||||
new_conf = {}
|
||||
|
||||
# 先按照参考配置的顺序添加配置项
|
||||
for key, value in refer_conf.items():
|
||||
if key not in conf:
|
||||
# logger.info(f"检查到配置项 {path + "." + key if path else key} 不存在,已插入默认值 {value}")
|
||||
# 配置项不存在,插入默认值
|
||||
path_ = path + "." + key if path else key
|
||||
logger.info(f"检查到配置项 {path_} 不存在,已插入默认值 {value}")
|
||||
conf[key] = value
|
||||
new_conf[key] = value
|
||||
has_new = True
|
||||
else:
|
||||
if conf[key] is None:
|
||||
conf[key] = value
|
||||
# 配置项为 None,使用默认值
|
||||
new_conf[key] = value
|
||||
has_new = True
|
||||
elif isinstance(value, dict):
|
||||
has_new |= self.check_config_integrity(
|
||||
value, conf[key], path + "." + key if path else key
|
||||
)
|
||||
# 递归检查子配置项
|
||||
if not isinstance(conf[key], dict):
|
||||
# 类型不匹配,使用默认值
|
||||
new_conf[key] = value
|
||||
has_new = True
|
||||
else:
|
||||
# 递归检查并同步顺序
|
||||
child_has_new = self.check_config_integrity(
|
||||
value, conf[key], path + "." + key if path else key
|
||||
)
|
||||
new_conf[key] = conf[key]
|
||||
has_new |= child_has_new
|
||||
else:
|
||||
# 直接使用现有配置
|
||||
new_conf[key] = conf[key]
|
||||
|
||||
# 检查是否存在参考配置中没有的配置项
|
||||
for key in list(conf.keys()):
|
||||
if key not in refer_conf:
|
||||
path_ = path + "." + key if path else key
|
||||
logger.info(f"检查到配置项 {path_} 不存在,将从当前配置中删除")
|
||||
del conf[key]
|
||||
has_new = True
|
||||
|
||||
# 顺序不一致也算作变更
|
||||
if list(conf.keys()) != list(new_conf.keys()):
|
||||
if path:
|
||||
logger.info(f"检查到配置项 {path} 的子项顺序不一致,已重新排序")
|
||||
else:
|
||||
logger.info("检查到配置项顺序不一致,已重新排序")
|
||||
has_new = True
|
||||
|
||||
# 更新原始配置
|
||||
conf.clear()
|
||||
conf.update(new_conf)
|
||||
|
||||
return has_new
|
||||
|
||||
def save_config(self, replace_config: Dict = None):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import os
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "3.5.14"
|
||||
VERSION = "3.5.15"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -40,12 +40,15 @@ DEFAULT_CONFIG = {
|
||||
},
|
||||
"no_permission_reply": True,
|
||||
"empty_mention_waiting": True,
|
||||
"empty_mention_waiting_need_reply": True,
|
||||
"friend_message_needs_wake_prefix": False,
|
||||
"ignore_bot_self_message": False,
|
||||
"ignore_at_all": False,
|
||||
},
|
||||
"provider": [],
|
||||
"provider_settings": {
|
||||
"enable": True,
|
||||
"default_provider_id": "",
|
||||
"wake_prefix": "",
|
||||
"web_search": False,
|
||||
"web_search_link": False,
|
||||
@@ -57,6 +60,7 @@ DEFAULT_CONFIG = {
|
||||
"dequeue_context_length": 1,
|
||||
"streaming_response": False,
|
||||
"streaming_segmented": False,
|
||||
"separate_provider": False,
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"enable": False,
|
||||
@@ -355,9 +359,14 @@ CONFIG_METADATA_2 = {
|
||||
"hint": "启用后,当用户没有权限执行某个操作时,机器人会回复一条消息。",
|
||||
},
|
||||
"empty_mention_waiting": {
|
||||
"description": "只 @ 机器人是否触发等待回复",
|
||||
"description": "只 @ 机器人是否触发等待",
|
||||
"type": "bool",
|
||||
"hint": "启用后,当消息内容只有 @ 机器人时,会触发等待回复,在 60 秒内的该用户的任意一条消息均会唤醒机器人。这在某些平台不支持 @ 和语音/图片等消息同时发送时特别有用。",
|
||||
"hint": "启用后,当消息内容只有 @ 机器人时,会触发等待,在 60 秒内的该用户的任意一条消息均会唤醒机器人。这在某些平台不支持 @ 和语音/图片等消息同时发送时特别有用。",
|
||||
},
|
||||
"empty_mention_waiting_need_reply": {
|
||||
"description": "只 @ 机器人触发等待时是否需要回复提醒",
|
||||
"type": "bool",
|
||||
"hint": "在上面一个配置项中,如果启用了触发等待,启用此项后,机器人会使用 LLM 生成一条回复。否则,将不回复而只是等待。",
|
||||
},
|
||||
"friend_message_needs_wake_prefix": {
|
||||
"description": "私聊消息是否需要唤醒前缀",
|
||||
@@ -369,6 +378,11 @@ CONFIG_METADATA_2 = {
|
||||
"type": "bool",
|
||||
"hint": "某些平台如 gewechat 会将自身账号在其他 APP 端发送的消息也当做消息事件下发导致给自己发消息时唤醒机器人",
|
||||
},
|
||||
"ignore_at_all": {
|
||||
"description": "是否忽略 @ 全体成员",
|
||||
"type": "bool",
|
||||
"hint": "启用后,机器人会忽略 @ 全体成员 的消息事件。",
|
||||
},
|
||||
"segmented_reply": {
|
||||
"description": "分段回复",
|
||||
"type": "object",
|
||||
@@ -620,6 +634,7 @@ CONFIG_METADATA_2 = {
|
||||
"gm_resp_image_modal": False,
|
||||
"gm_native_search": False,
|
||||
"gm_native_coderunner": False,
|
||||
"gm_url_context": False,
|
||||
"gm_safety_settings": {
|
||||
"harassment": "BLOCK_MEDIUM_AND_ABOVE",
|
||||
"hate_speech": "BLOCK_MEDIUM_AND_ABOVE",
|
||||
@@ -1024,6 +1039,12 @@ CONFIG_METADATA_2 = {
|
||||
"hint": "启用后所有函数工具将全部失效",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"gm_url_context": {
|
||||
"description": "启用URL上下文功能",
|
||||
"type": "bool",
|
||||
"hint": "启用后所有函数工具将全部失效",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"gm_safety_settings": {
|
||||
"description": "安全过滤器",
|
||||
"type": "object",
|
||||
@@ -1379,9 +1400,19 @@ CONFIG_METADATA_2 = {
|
||||
"enable": {
|
||||
"description": "启用大语言模型聊天",
|
||||
"type": "bool",
|
||||
"hint": "如需切换大语言模型提供商,请使用 `/provider` 命令。",
|
||||
"hint": "如需切换大语言模型提供商,请使用 /provider 命令。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"separate_provider": {
|
||||
"description": "提供商会话隔离",
|
||||
"type": "bool",
|
||||
"hint": "启用后,每个会话支持独立选择文本生成、STT、TTS 等提供商。如果会话在使用 /provider 指令时提示无权限,可以将会话加入管理员名单或者使用 /alter_cmd provider member 将指令设为非管理员指令。",
|
||||
},
|
||||
"default_provider_id": {
|
||||
"description": "默认模型提供商 ID",
|
||||
"type": "string",
|
||||
"hint": "可选。每个聊天会话的默认提供商 ID。",
|
||||
},
|
||||
"wake_prefix": {
|
||||
"description": "LLM 聊天额外唤醒前缀",
|
||||
"type": "string",
|
||||
@@ -1494,7 +1525,7 @@ CONFIG_METADATA_2 = {
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "提供商 ID,不填则默认第一个STT提供商",
|
||||
"description": "提供商 ID",
|
||||
"type": "string",
|
||||
"hint": "语音转文本提供商 ID。如果不填写将使用载入的第一个提供商。",
|
||||
},
|
||||
@@ -1511,7 +1542,7 @@ CONFIG_METADATA_2 = {
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "提供商 ID,不填则默认第一个TTS提供商",
|
||||
"description": "提供商 ID",
|
||||
"type": "string",
|
||||
"hint": "文本转语音提供商 ID。如果不填写将使用载入的第一个提供商。",
|
||||
},
|
||||
|
||||
@@ -11,7 +11,9 @@ class SQLiteDatabase(BaseDatabase):
|
||||
super().__init__()
|
||||
self.db_path = db_path
|
||||
|
||||
with open(os.path.dirname(__file__) + "/sqlite_init.sql", "r") as f:
|
||||
with open(
|
||||
os.path.dirname(__file__) + "/sqlite_init.sql", "r", encoding="utf-8"
|
||||
) as f:
|
||||
sql = f.read()
|
||||
|
||||
# 初始化数据库
|
||||
|
||||
@@ -43,9 +43,8 @@ class PreProcessStage(Stage):
|
||||
# STT
|
||||
if self.stt_settings.get("enable", False):
|
||||
# TODO: 独立
|
||||
stt_provider = (
|
||||
self.plugin_manager.context.provider_manager.curr_stt_provider_inst
|
||||
)
|
||||
ctx = self.plugin_manager.context
|
||||
stt_provider = ctx.get_using_stt_provider(event.unified_msg_origin)
|
||||
if not stt_provider:
|
||||
return
|
||||
message_chain = event.get_messages()
|
||||
|
||||
@@ -33,6 +33,7 @@ from mcp.types import (
|
||||
TextResourceContents,
|
||||
BlobResourceContents,
|
||||
)
|
||||
from astrbot.core import web_chat_back_queue
|
||||
|
||||
|
||||
class LLMRequestSubStage(Stage):
|
||||
@@ -70,8 +71,8 @@ class LLMRequestSubStage(Stage):
|
||||
if not self.ctx.astrbot_config["provider_settings"]["enable"]:
|
||||
logger.debug("未启用 LLM 能力,跳过处理。")
|
||||
return
|
||||
|
||||
provider = self.ctx.plugin_manager.context.get_using_provider()
|
||||
umo = event.unified_msg_origin
|
||||
provider = self.ctx.plugin_manager.context.get_using_provider(umo=umo)
|
||||
if provider is None:
|
||||
return
|
||||
|
||||
@@ -287,7 +288,66 @@ class LLMRequestSubStage(Stage):
|
||||
if img_b64 := event.get_extra("tool_call_img_respond"):
|
||||
await event.send(MessageChain(chain=[Image.fromBase64(img_b64)]))
|
||||
event.set_extra("tool_call_img_respond", None)
|
||||
yield
|
||||
|
||||
if event.get_platform_name() == "webchat":
|
||||
# 异步处理 WebChat 特殊情况
|
||||
asyncio.create_task(self._handle_webchat(event, req))
|
||||
|
||||
async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
"""处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title"""
|
||||
conversation = await self.conv_manager.get_conversation(
|
||||
event.unified_msg_origin, req.conversation.cid
|
||||
)
|
||||
if conversation and not req.conversation.title:
|
||||
messages = json.loads(conversation.history)
|
||||
latest_pair = messages[-2:]
|
||||
if not latest_pair:
|
||||
return
|
||||
provider = self.ctx.plugin_manager.context.get_using_provider()
|
||||
cleaned_text = "User: " + latest_pair[0].get("content", "").strip()
|
||||
# if len(latest_pair) > 1:
|
||||
# cleaned_text += (
|
||||
# "\nAssistant: " + latest_pair[1].get("content", "").strip()
|
||||
# )
|
||||
logger.debug(f"WebChat 对话标题生成请求,清理后的文本: {cleaned_text}")
|
||||
llm_resp = await provider.text_chat(
|
||||
system_prompt="You are expert in summarizing user's query.",
|
||||
prompt=(
|
||||
f"Please summarize the following query of user:\n"
|
||||
f"{cleaned_text}\n"
|
||||
"Only output the summary within 10 words, DO NOT INCLUDE any other text."
|
||||
"You must use the same language as the user."
|
||||
"If you think the dialog is too short to summarize, only output a special mark: `None`"
|
||||
),
|
||||
)
|
||||
if llm_resp and llm_resp.completion_text:
|
||||
logger.debug(
|
||||
f"WebChat 对话标题生成响应: {llm_resp.completion_text.strip()}"
|
||||
)
|
||||
title = llm_resp.completion_text.strip()
|
||||
if not title or "None" == title:
|
||||
return
|
||||
await self.conv_manager.update_conversation_title(
|
||||
event.unified_msg_origin, title=title
|
||||
)
|
||||
# 由于 WebChat 平台特殊性,其有两个对话,因此我们要更新两个对话的标题
|
||||
# webchat adapter 中,session_id 的格式是 f"webchat!{username}!{cid}"
|
||||
# TODO: 优化 WebChat 适配器的对话管理
|
||||
if event.session_id:
|
||||
username, cid = event.session_id.split("!")[1:3]
|
||||
db_helper = self.ctx.plugin_manager.context._db
|
||||
db_helper.update_conversation_title(
|
||||
user_id=username,
|
||||
cid=cid,
|
||||
title=title,
|
||||
)
|
||||
web_chat_back_queue.put_nowait(
|
||||
{
|
||||
"type": "update_title",
|
||||
"cid": cid,
|
||||
"data": title,
|
||||
}
|
||||
)
|
||||
|
||||
async def _handle_llm_response(
|
||||
self,
|
||||
|
||||
@@ -191,6 +191,7 @@ class RespondStage(Stage):
|
||||
await asyncio.sleep(i)
|
||||
try:
|
||||
await event.send(MessageChain([*decorated_comps, comp]))
|
||||
decorated_comps = [] # 清空已发送的装饰组件
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息失败: {e} chain: {result.chain}")
|
||||
break
|
||||
|
||||
@@ -169,8 +169,8 @@ class ResultDecorateStage(Stage):
|
||||
result.chain = new_chain
|
||||
|
||||
# TTS
|
||||
tts_provider = (
|
||||
self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst
|
||||
tts_provider = self.ctx.plugin_manager.context.get_using_tts_provider(
|
||||
event.unified_msg_origin
|
||||
)
|
||||
if (
|
||||
self.ctx.astrbot_config["provider_tts_settings"]["enable"]
|
||||
|
||||
@@ -4,7 +4,7 @@ from astrbot import logger
|
||||
from typing import Union, AsyncGenerator
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
from astrbot.core.message.message_event_result import MessageEventResult, MessageChain
|
||||
from astrbot.core.message.components import At
|
||||
from astrbot.core.message.components import At, AtAll
|
||||
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
||||
from astrbot.core.star.star import star_map
|
||||
from astrbot.core.star.filter.permission import PermissionTypeFilter
|
||||
@@ -39,6 +39,9 @@ class WakingCheckStage(Stage):
|
||||
self.ignore_bot_self_message = self.ctx.astrbot_config["platform_settings"].get(
|
||||
"ignore_bot_self_message", False
|
||||
)
|
||||
self.ignore_at_all = self.ctx.astrbot_config["platform_settings"].get(
|
||||
"ignore_at_all", False
|
||||
)
|
||||
|
||||
async def process(
|
||||
self, event: AstrMessageEvent
|
||||
@@ -79,10 +82,9 @@ class WakingCheckStage(Stage):
|
||||
if not is_wake:
|
||||
# 检查是否有 at 消息
|
||||
for message in messages:
|
||||
if isinstance(message, At) and (
|
||||
if (isinstance(message, At) and (
|
||||
str(message.qq) == str(event.get_self_id())
|
||||
or str(message.qq) == "all"
|
||||
):
|
||||
)) or (isinstance(message, AtAll) and not self.ignore_at_all):
|
||||
is_wake = True
|
||||
event.is_wake = True
|
||||
wake_prefix = ""
|
||||
|
||||
@@ -221,6 +221,9 @@ class AiocqhttpAdapter(Platform):
|
||||
a = None
|
||||
if t == "text":
|
||||
current_text = "".join(m["data"]["text"] for m in m_group).strip()
|
||||
if not current_text:
|
||||
# 如果文本段为空,则跳过
|
||||
continue
|
||||
message_str += current_text
|
||||
a = ComponentTypes[t](text=current_text) # noqa: F405
|
||||
abm.message.append(a)
|
||||
|
||||
@@ -18,13 +18,6 @@ class ProviderManager:
|
||||
self.persona_configs: list = config.get("persona", [])
|
||||
self.astrbot_config = config
|
||||
|
||||
self.selected_provider_id = sp.get("curr_provider")
|
||||
self.selected_stt_provider_id = self.provider_stt_settings.get("provider_id")
|
||||
self.selected_tts_provider_id = self.provider_settings.get("provider_id")
|
||||
# self.provider_enabled = self.provider_settings.get("enable", False)
|
||||
# self.stt_enabled = self.provider_stt_settings.get("enable", False)
|
||||
# self.tts_enabled = self.provider_tts_settings.get("enable", False)
|
||||
|
||||
# 人格情景管理
|
||||
# 目前没有拆成独立的模块
|
||||
self.default_persona_name = self.provider_settings.get(
|
||||
@@ -103,14 +96,13 @@ class ProviderManager:
|
||||
self.inst_map = {}
|
||||
"""Provider 实例映射. key: provider_id, value: Provider 实例"""
|
||||
self.llm_tools = llm_tools
|
||||
self.default_provider_inst: Provider = None
|
||||
"""默认的 Provider 实例。第 0 个或者用户以前指定的 Provider 实例"""
|
||||
|
||||
self.curr_provider_inst: Provider = None
|
||||
"""当前使用的 Provider 实例"""
|
||||
"""默认的 Provider 实例"""
|
||||
self.curr_stt_provider_inst: STTProvider = None
|
||||
"""当前使用的 Speech To Text Provider 实例"""
|
||||
"""默认的 Speech To Text Provider 实例"""
|
||||
self.curr_tts_provider_inst: TTSProvider = None
|
||||
"""当前使用的 Text To Speech Provider 实例"""
|
||||
"""默认的 Text To Speech Provider 实例"""
|
||||
self.db_helper = db_helper
|
||||
|
||||
# kdb(experimental)
|
||||
@@ -119,13 +111,57 @@ class ProviderManager:
|
||||
if kdb_cfg and len(kdb_cfg):
|
||||
self.curr_kdb_name = list(kdb_cfg.keys())[0]
|
||||
|
||||
async def set_provider(
|
||||
self, provider_id: str, provider_type: ProviderType, umo: str = None
|
||||
):
|
||||
"""设置提供商。
|
||||
|
||||
Args:
|
||||
provider_id (str): 提供商 ID。
|
||||
provider_type (ProviderType): 提供商类型。
|
||||
umo (str, optional): 用户会话 ID,用于提供商会话隔离。当用户启用了提供商会话隔离时此参数才生效。
|
||||
"""
|
||||
if provider_id not in self.inst_map:
|
||||
raise ValueError(f"提供商 {provider_id} 不存在,无法设置。")
|
||||
if umo and self.provider_settings["separate_provider"]:
|
||||
perf = sp.get("session_provider_perf", {})
|
||||
session_perf = perf.get(umo, {})
|
||||
session_perf[provider_type.value] = provider_id
|
||||
perf[umo] = session_perf
|
||||
sp.put("session_provider_perf", perf)
|
||||
return
|
||||
# 不启用提供商会话隔离模式的情况
|
||||
self.curr_provider_inst = self.inst_map[provider_id]
|
||||
if provider_type == ProviderType.TEXT_TO_SPEECH:
|
||||
sp.put("curr_provider_tts", provider_id)
|
||||
elif provider_type == ProviderType.SPEECH_TO_TEXT:
|
||||
sp.put("curr_provider_stt", provider_id)
|
||||
elif provider_type == ProviderType.CHAT_COMPLETION:
|
||||
sp.put("curr_provider", provider_id)
|
||||
|
||||
async def initialize(self):
|
||||
# 逐个初始化提供商
|
||||
for provider_config in self.providers_config:
|
||||
await self.load_provider(provider_config)
|
||||
|
||||
self.default_provider_inst = self.inst_map.get(self.selected_provider_id)
|
||||
if not self.default_provider_inst and self.provider_insts:
|
||||
self.default_provider_inst = self.provider_insts[0]
|
||||
# 设置默认提供商
|
||||
self.curr_provider_inst = self.inst_map.get(
|
||||
self.provider_settings.get("default_provider_id")
|
||||
)
|
||||
if not self.curr_provider_inst and self.provider_insts:
|
||||
self.curr_provider_inst = self.provider_insts[0]
|
||||
|
||||
self.curr_stt_provider_inst = self.inst_map.get(
|
||||
self.provider_stt_settings.get("provider_id")
|
||||
)
|
||||
if not self.curr_stt_provider_inst and self.stt_provider_insts:
|
||||
self.curr_stt_provider_inst = self.stt_provider_insts[0]
|
||||
|
||||
self.curr_tts_provider_inst = self.inst_map.get(
|
||||
self.provider_tts_settings.get("provider_id")
|
||||
)
|
||||
if not self.curr_tts_provider_inst and self.tts_provider_insts:
|
||||
self.curr_tts_provider_inst = self.tts_provider_insts[0]
|
||||
|
||||
# 初始化 MCP Client 连接
|
||||
asyncio.create_task(
|
||||
@@ -217,6 +253,10 @@ class ProviderManager:
|
||||
from .sources.openai_embedding_source import (
|
||||
OpenAIEmbeddingProvider as OpenAIEmbeddingProvider,
|
||||
)
|
||||
case "gemini_embedding":
|
||||
from .sources.gemini_embedding_source import (
|
||||
GeminiEmbeddingProvider as GeminiEmbeddingProvider,
|
||||
)
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.critical(
|
||||
f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。"
|
||||
@@ -248,7 +288,10 @@ class ProviderManager:
|
||||
await inst.initialize()
|
||||
|
||||
self.stt_provider_insts.append(inst)
|
||||
if self.selected_stt_provider_id == provider_config["id"]:
|
||||
if (
|
||||
self.provider_stt_settings.get("provider_id")
|
||||
== provider_config["id"]
|
||||
):
|
||||
self.curr_stt_provider_inst = inst
|
||||
logger.info(
|
||||
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前语音转文本提供商适配器。"
|
||||
@@ -266,7 +309,7 @@ class ProviderManager:
|
||||
await inst.initialize()
|
||||
|
||||
self.tts_provider_insts.append(inst)
|
||||
if self.selected_tts_provider_id == provider_config["id"]:
|
||||
if self.provider_settings.get("provider_id") == provider_config["id"]:
|
||||
self.curr_tts_provider_inst = inst
|
||||
logger.info(
|
||||
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前文本转语音提供商适配器。"
|
||||
@@ -288,7 +331,10 @@ class ProviderManager:
|
||||
await inst.initialize()
|
||||
|
||||
self.provider_insts.append(inst)
|
||||
if self.selected_provider_id == provider_config["id"]:
|
||||
if (
|
||||
self.provider_settings.get("default_provider_id")
|
||||
== provider_config["id"]
|
||||
):
|
||||
self.curr_provider_inst = inst
|
||||
logger.info(
|
||||
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前提供商适配器。"
|
||||
@@ -326,7 +372,6 @@ class ProviderManager:
|
||||
self.curr_provider_inst = None
|
||||
elif self.curr_provider_inst is None and len(self.provider_insts) > 0:
|
||||
self.curr_provider_inst = self.provider_insts[0]
|
||||
self.selected_provider_id = self.curr_provider_inst.meta().id
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_provider_inst.meta().id} 作为当前提供商适配器。"
|
||||
)
|
||||
@@ -335,7 +380,6 @@ class ProviderManager:
|
||||
self.curr_stt_provider_inst = None
|
||||
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]
|
||||
self.selected_stt_provider_id = self.curr_stt_provider_inst.meta().id
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_stt_provider_inst.meta().id} 作为当前语音转文本提供商适配器。"
|
||||
)
|
||||
@@ -344,7 +388,6 @@ class ProviderManager:
|
||||
self.curr_tts_provider_inst = None
|
||||
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]
|
||||
self.selected_tts_provider_id = self.curr_tts_provider_inst.meta().id
|
||||
logger.info(
|
||||
f"自动选择 {self.curr_tts_provider_inst.meta().id} 作为当前文本转语音提供商适配器。"
|
||||
)
|
||||
|
||||
@@ -141,24 +141,66 @@ class ProviderGoogleGenAI(Provider):
|
||||
logger.warning("流式输出不支持图片模态,已自动降级为文本模态")
|
||||
modalities = ["Text"]
|
||||
|
||||
tool_list = None
|
||||
tool_list = []
|
||||
model_name = self.get_model()
|
||||
native_coderunner = self.provider_config.get("gm_native_coderunner", False)
|
||||
native_search = self.provider_config.get("gm_native_search", False)
|
||||
url_context = self.provider_config.get("gm_url_context", False)
|
||||
|
||||
if native_coderunner:
|
||||
tool_list = [types.Tool(code_execution=types.ToolCodeExecution())]
|
||||
if native_search:
|
||||
logger.warning("已启用代码执行工具,搜索工具将被忽略")
|
||||
if tools:
|
||||
logger.warning("已启用代码执行工具,函数工具将被忽略")
|
||||
elif native_search:
|
||||
tool_list = [types.Tool(google_search=types.GoogleSearch())]
|
||||
if tools:
|
||||
logger.warning("已启用搜索工具,函数工具将被忽略")
|
||||
if "gemini-2.5" in model_name:
|
||||
if native_coderunner:
|
||||
tool_list.append(types.Tool(code_execution=types.ToolCodeExecution()))
|
||||
if native_search:
|
||||
logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具")
|
||||
if url_context:
|
||||
logger.warning(
|
||||
"代码执行工具与URL上下文工具互斥,已忽略URL上下文工具"
|
||||
)
|
||||
else:
|
||||
if native_search:
|
||||
tool_list.append(types.Tool(google_search=types.GoogleSearch()))
|
||||
|
||||
if url_context:
|
||||
if hasattr(types, "UrlContext"):
|
||||
tool_list.append(types.Tool(url_context=types.UrlContext()))
|
||||
else:
|
||||
logger.warning(
|
||||
"当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包"
|
||||
)
|
||||
|
||||
elif "gemini-2.0-lite" in model_name:
|
||||
if native_coderunner or native_search or url_context:
|
||||
logger.warning(
|
||||
"gemini-2.0-lite 不支持代码执行、搜索工具和URL上下文,将忽略这些设置"
|
||||
)
|
||||
tool_list = None
|
||||
|
||||
else:
|
||||
if native_coderunner:
|
||||
tool_list.append(types.Tool(code_execution=types.ToolCodeExecution()))
|
||||
if native_search:
|
||||
logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具")
|
||||
elif native_search:
|
||||
tool_list.append(types.Tool(google_search=types.GoogleSearch()))
|
||||
|
||||
if url_context and not native_coderunner:
|
||||
if hasattr(types, "UrlContext"):
|
||||
tool_list.append(types.Tool(url_context=types.UrlContext()))
|
||||
else:
|
||||
logger.warning(
|
||||
"当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包"
|
||||
)
|
||||
|
||||
if not tool_list:
|
||||
tool_list = None
|
||||
|
||||
if tools and tool_list:
|
||||
logger.warning("已启用原生工具,函数工具将被忽略")
|
||||
elif tools and (func_desc := tools.get_func_desc_google_genai_style()):
|
||||
tool_list = [
|
||||
types.Tool(function_declarations=func_desc["function_declarations"])
|
||||
]
|
||||
|
||||
return types.GenerateContentConfig(
|
||||
system_instruction=system_instruction,
|
||||
temperature=temperature,
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import List, Union
|
||||
|
||||
from astrbot.core import sp
|
||||
from astrbot.core.provider.provider import Provider, TTSProvider, STTProvider
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.provider.func_tool_manager import FuncCall
|
||||
@@ -140,24 +141,46 @@ class Context:
|
||||
"""获取所有用于 STT 任务的 Provider。"""
|
||||
return self.provider_manager.stt_provider_insts
|
||||
|
||||
def get_using_provider(self) -> Provider:
|
||||
def get_using_provider(self, umo: str = None) -> Provider:
|
||||
"""
|
||||
获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。
|
||||
获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。通过 /provider 指令切换。
|
||||
|
||||
通过 /provider 指令切换。
|
||||
Args:
|
||||
umo(str): unified_message_origin 值,如果传入并且用户启用了提供商会话隔离,则使用该会话偏好的提供商。
|
||||
"""
|
||||
if umo and self._config["provider_settings"]["separate_provider"]:
|
||||
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):
|
||||
return inst
|
||||
return self.provider_manager.curr_provider_inst
|
||||
|
||||
def get_using_tts_provider(self) -> TTSProvider:
|
||||
def get_using_tts_provider(self, umo: str = None) -> TTSProvider:
|
||||
"""
|
||||
获取当前使用的用于 TTS 任务的 Provider。
|
||||
|
||||
Args:
|
||||
umo(str): unified_message_origin 值,如果传入,则使用该会话偏好的提供商。
|
||||
"""
|
||||
if umo and self._config["provider_settings"]["separate_provider"]:
|
||||
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):
|
||||
return inst
|
||||
return self.provider_manager.curr_tts_provider_inst
|
||||
|
||||
def get_using_stt_provider(self) -> STTProvider:
|
||||
def get_using_stt_provider(self, umo: str = None) -> STTProvider:
|
||||
"""
|
||||
获取当前使用的用于 STT 任务的 Provider。
|
||||
|
||||
Args:
|
||||
umo(str): unified_message_origin 值,如果传入,则使用该会话偏好的提供商。
|
||||
"""
|
||||
if umo and self._config["provider_settings"]["separate_provider"]:
|
||||
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:
|
||||
|
||||
@@ -7,6 +7,9 @@ from astrbot.core.config import AstrBotConfig
|
||||
from .custom_filter import CustomFilter
|
||||
from ..star_handler import StarHandlerMetadata
|
||||
|
||||
class GreedyStr(str):
|
||||
"""标记指令完成其他参数接收后的所有剩余文本。"""
|
||||
pass
|
||||
|
||||
# 标准指令受到 wake_prefix 的制约。
|
||||
class CommandFilter(HandlerFilter):
|
||||
@@ -68,7 +71,22 @@ class CommandFilter(HandlerFilter):
|
||||
) -> Dict[str, Any]:
|
||||
"""将参数列表 params 根据 param_type 转换为参数字典。"""
|
||||
result = {}
|
||||
for i, (param_name, param_type_or_default_val) in enumerate(param_type.items()):
|
||||
param_items = list(param_type.items())
|
||||
for i, (param_name, param_type_or_default_val) in enumerate(param_items):
|
||||
is_greedy = param_type_or_default_val is GreedyStr
|
||||
|
||||
if is_greedy:
|
||||
# GreedyStr 必须是最后一个参数
|
||||
if i != len(param_items) - 1:
|
||||
raise ValueError(
|
||||
f"参数 '{param_name}' (GreedyStr) 必须是最后一个参数。"
|
||||
)
|
||||
|
||||
# 将剩余的所有部分合并成一个字符串
|
||||
remaining_params = params[i:]
|
||||
result[param_name] = " ".join(remaining_params)
|
||||
break
|
||||
# 没有 GreedyStr 的情况
|
||||
if i >= len(params):
|
||||
if (
|
||||
isinstance(param_type_or_default_val, Type)
|
||||
|
||||
@@ -32,10 +32,6 @@ class PluginUpdator(RepoZipUpdator):
|
||||
if not repo_url:
|
||||
raise Exception(f"插件 {plugin.name} 没有指定仓库地址。")
|
||||
|
||||
if proxy:
|
||||
proxy = proxy.removesuffix("/")
|
||||
repo_url = f"{proxy}/{repo_url}"
|
||||
|
||||
plugin_path = os.path.join(self.plugin_store_path, plugin.root_dir_name)
|
||||
|
||||
logger.info(f"正在更新插件,路径: {plugin_path},仓库地址: {repo_url}")
|
||||
|
||||
@@ -148,6 +148,7 @@ class RepoZipUpdator:
|
||||
release_url = releases[0]["zipball_url"]
|
||||
|
||||
if proxy:
|
||||
proxy = proxy.rstrip("/")
|
||||
release_url = f"{proxy}/{release_url}"
|
||||
logger.info(
|
||||
f"检查到设置了镜像站,将使用镜像站下载 {author}/{repo} 仓库源码: {release_url}"
|
||||
|
||||
@@ -26,6 +26,7 @@ class ChatRoute(Route):
|
||||
"/chat/conversations": ("GET", self.get_conversations),
|
||||
"/chat/get_conversation": ("GET", self.get_conversation),
|
||||
"/chat/delete_conversation": ("GET", self.delete_conversation),
|
||||
"/chat/rename_conversation": ("POST", self.rename_conversation),
|
||||
"/chat/get_file": ("GET", self.get_file),
|
||||
"/chat/post_image": ("POST", self.post_image),
|
||||
"/chat/post_file": ("POST", self.post_file),
|
||||
@@ -100,7 +101,6 @@ class ChatRoute(Route):
|
||||
|
||||
file = post_data["file"]
|
||||
filename = f"{str(uuid.uuid4())}"
|
||||
print(file)
|
||||
# 通过文件格式判断文件类型
|
||||
if file.content_type.startswith("audio"):
|
||||
filename += ".wav"
|
||||
@@ -135,22 +135,24 @@ class ChatRoute(Route):
|
||||
|
||||
self.curr_user_cid[username] = conversation_id
|
||||
|
||||
await web_chat_queue.put((
|
||||
username,
|
||||
conversation_id,
|
||||
{
|
||||
"message": message,
|
||||
"image_url": image_url, # list
|
||||
"audio_url": audio_url,
|
||||
},
|
||||
))
|
||||
await web_chat_queue.put(
|
||||
(
|
||||
username,
|
||||
conversation_id,
|
||||
{
|
||||
"message": message,
|
||||
"image_url": image_url, # list
|
||||
"audio_url": audio_url,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# 持久化
|
||||
conversation = self.db.get_conversation_by_user_id(username, conversation_id)
|
||||
try:
|
||||
history = json.loads(conversation.history)
|
||||
except BaseException as e:
|
||||
print(e)
|
||||
logger.error(f"Failed to parse conversation history: {e}")
|
||||
history = []
|
||||
new_his = {"type": "user", "message": message}
|
||||
if image_url:
|
||||
@@ -204,6 +206,9 @@ class ChatRoute(Route):
|
||||
if streaming and type != "end":
|
||||
continue
|
||||
|
||||
if type == "update_title":
|
||||
continue
|
||||
|
||||
if result_text:
|
||||
conversation = self.db.get_conversation_by_user_id(
|
||||
username, cid
|
||||
@@ -211,7 +216,7 @@ class ChatRoute(Route):
|
||||
try:
|
||||
history = json.loads(conversation.history)
|
||||
except BaseException as e:
|
||||
print(e)
|
||||
logger.error(f"Failed to parse conversation history: {e}")
|
||||
history = []
|
||||
history.append({"type": "bot", "message": result_text})
|
||||
self.db.update_conversation(
|
||||
@@ -249,6 +254,18 @@ class ChatRoute(Route):
|
||||
self.db.new_conversation(username, conversation_id)
|
||||
return Response().ok(data={"conversation_id": conversation_id}).__dict__
|
||||
|
||||
async def rename_conversation(self):
|
||||
username = g.get("username", "guest")
|
||||
post_data = await request.json
|
||||
if "conversation_id" not in post_data or "title" not in post_data:
|
||||
return Response().error("Missing key: conversation_id or title").__dict__
|
||||
|
||||
conversation_id = post_data["conversation_id"]
|
||||
title = post_data["title"]
|
||||
|
||||
self.db.update_conversation_title(username, conversation_id, title=title)
|
||||
return Response().ok(message="重命名成功!").__dict__
|
||||
|
||||
async def get_conversations(self):
|
||||
username = g.get("username", "guest")
|
||||
conversations = self.db.get_conversations(username)
|
||||
|
||||
@@ -154,6 +154,7 @@ class ConfigRoute(Route):
|
||||
) -> None:
|
||||
super().__init__(context)
|
||||
self.core_lifecycle = core_lifecycle
|
||||
self.config: AstrBotConfig = core_lifecycle.astrbot_config
|
||||
self.routes = {
|
||||
"/config/get": ("GET", self.get_configs),
|
||||
"/config/astrbot/update": ("POST", self.post_astrbot_configs),
|
||||
@@ -165,58 +166,94 @@ class ConfigRoute(Route):
|
||||
"/config/provider/update": ("POST", self.post_update_provider),
|
||||
"/config/provider/delete": ("POST", self.post_delete_provider),
|
||||
"/config/llmtools": ("GET", self.get_llm_tools),
|
||||
"/config/provider/check_status": ("GET", self.check_all_providers_status),
|
||||
"/config/provider/check_status": ("GET", self.check_all_providers_status),
|
||||
"/config/provider/list": ("GET", self.get_provider_config_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 _test_single_provider(self, provider):
|
||||
async def _test_single_provider(self, provider):
|
||||
"""辅助函数:测试单个 provider 的可用性"""
|
||||
meta = provider.meta()
|
||||
provider_name = provider.provider_config.get("id", "Unknown Provider")
|
||||
logger.debug(f"Got provider meta: {meta}")
|
||||
if not provider_name and meta:
|
||||
if not provider_name and meta:
|
||||
provider_name = meta.id
|
||||
elif not provider_name:
|
||||
elif not provider_name:
|
||||
provider_name = "Unknown Provider"
|
||||
status_info = {
|
||||
"id": getattr(meta, 'id', 'Unknown ID'),
|
||||
"model": getattr(meta, 'model', 'Unknown Model'),
|
||||
"type": getattr(meta, 'type', 'Unknown Type'),
|
||||
"id": getattr(meta, "id", "Unknown ID"),
|
||||
"model": getattr(meta, "model", "Unknown Model"),
|
||||
"type": getattr(meta, "type", "Unknown Type"),
|
||||
"name": provider_name,
|
||||
"status": "unavailable", # 默认为不可用
|
||||
"status": "unavailable", # 默认为不可用
|
||||
"error": None,
|
||||
}
|
||||
logger.debug(f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})")
|
||||
logger.debug(
|
||||
f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})"
|
||||
)
|
||||
try:
|
||||
logger.debug(f"Sending 'Ping' to provider: {status_info['name']}")
|
||||
response = await asyncio.wait_for(provider.text_chat(prompt="REPLY `PONG` ONLY"), timeout=45.0)
|
||||
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}")
|
||||
# 只要 text_chat 调用成功返回一个 LLMResponse 对象 (即 response 不为 None),就认为可用
|
||||
if response is not None:
|
||||
status_info["status"] = "available"
|
||||
response_text_snippet = ""
|
||||
if hasattr(response, 'completion_text') and response.completion_text:
|
||||
response_text_snippet = response.completion_text[:70] + "..." if len(response.completion_text) > 70 else response.completion_text
|
||||
elif hasattr(response, 'result_chain') and response.result_chain:
|
||||
if hasattr(response, "completion_text") and response.completion_text:
|
||||
response_text_snippet = (
|
||||
response.completion_text[:70] + "..."
|
||||
if len(response.completion_text) > 70
|
||||
else response.completion_text
|
||||
)
|
||||
elif hasattr(response, "result_chain") and response.result_chain:
|
||||
try:
|
||||
response_text_snippet = response.result_chain.get_plain_text()[:70] + "..." if len(response.result_chain.get_plain_text()) > 70 else response.result_chain.get_plain_text()
|
||||
except:
|
||||
response_text_snippet = (
|
||||
response.result_chain.get_plain_text()[:70] + "..."
|
||||
if len(response.result_chain.get_plain_text()) > 70
|
||||
else response.result_chain.get_plain_text()
|
||||
)
|
||||
except Exception as _:
|
||||
pass
|
||||
logger.info(f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'")
|
||||
logger.info(
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'"
|
||||
)
|
||||
else:
|
||||
# 这个分支理论上不应该被走到,除非 text_chat 实现可能返回 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.")
|
||||
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()}"
|
||||
)
|
||||
return status_info
|
||||
|
||||
async def check_all_providers_status(self):
|
||||
@@ -225,7 +262,9 @@ class ConfigRoute(Route):
|
||||
"""
|
||||
logger.info("API call received: /config/provider/check_status")
|
||||
try:
|
||||
all_providers: typing.List = self.core_lifecycle.star_context.get_all_providers()
|
||||
all_providers: typing.List = (
|
||||
self.core_lifecycle.star_context.get_all_providers()
|
||||
)
|
||||
logger.debug(f"Found {len(all_providers)} providers to check.")
|
||||
|
||||
if not all_providers:
|
||||
@@ -234,15 +273,17 @@ class ConfigRoute(Route):
|
||||
|
||||
tasks = [self._test_single_provider(p) for p in all_providers]
|
||||
logger.debug(f"Created {len(tasks)} tasks for concurrent provider checks.")
|
||||
|
||||
|
||||
results = await asyncio.gather(*tasks)
|
||||
logger.info(f"Provider status check completed. Results: {results}")
|
||||
|
||||
return Response().ok(results).__dict__
|
||||
|
||||
return Response().ok(results).__dict__
|
||||
except Exception as e:
|
||||
logger.error(f"Critical error in check_all_providers_status: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"检查 Provider 状态时发生严重错误: {str(e)}").__dict__
|
||||
return (
|
||||
Response().error(f"检查 Provider 状态时发生严重错误: {str(e)}").__dict__
|
||||
)
|
||||
|
||||
async def get_configs(self):
|
||||
# plugin_name 为空时返回 AstrBot 配置
|
||||
@@ -252,6 +293,21 @@ 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:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# What's Changed
|
||||
|
||||
1. 修复:如果设置了 GitHub 加速地址,更新插件会报错
|
||||
2. 修复:部分场景下,`只@触发等待` 配置项功能无效的问题
|
||||
3. 新增:增加 `只@触发等待时是否回复` 配置项
|
||||
4. 新增:**支持模型提供商使用时会话隔离(需要手动开启配置项:提供商会话隔离)**
|
||||
5. 新增:Google Gemini 提供商支持 URL 上下文功能
|
||||
6. 新增:优化 WebChat 的 UI 显示,WebChat 支持修改标题和自动生成标题,支持 WebChatBox
|
||||
7. 新增:支持可配置是否忽略 @ 全体成员
|
||||
8. 优化:WebUI 顶栏移动端显示
|
||||
9. 优化:插件/AstrBot 配置项完整性检查的同时也保证**配置项相对顺序一致性**
|
||||
10. 优化:perf: 分段回复时,仅在输出的第一句话带上回复/引用
|
||||
11. 修复: Windows 下部署项目时可能出现的 UnicodeDecodeError。
|
||||
@@ -254,7 +254,7 @@ commonStore.getStartTime();
|
||||
variant="flat" @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="hidden-lg-and-up text-secondary ms-3" color="lightsecondary" icon rounded="sm" variant="flat"
|
||||
<v-btn v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="hidden-lg-and-up ms-3" color="lightsecondary" icon rounded="sm" variant="flat"
|
||||
@click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
@@ -263,15 +263,15 @@ commonStore.getStartTime();
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<div style="margin-left: 16px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style=" font-size: 24px; font-weight: 1000;">Astr<span style="font-weight: normal;">Bot</span>
|
||||
</span>
|
||||
<span style="font-size: 12px; color: var(--v-theme-secondaryText);">{{ botCurrVersion }}</span>
|
||||
<div class="logo-container" :class="{'mobile-logo': $vuetify.display.xs}">
|
||||
<span class="logo-text">Astr<span class="logo-text-light">Bot</span></span>
|
||||
<span class="version-text hidden-xs">{{ botCurrVersion }}</span>
|
||||
</div>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<div class="mr-4">
|
||||
<!-- 版本提示信息 - 在手机上隐藏 -->
|
||||
<div class="mr-4 hidden-xs">
|
||||
<small v-if="hasNewVersion">
|
||||
AstrBot 有新版本!
|
||||
</small>
|
||||
@@ -280,24 +280,28 @@ commonStore.getStartTime();
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<v-btn size="small" @click="toggleDarkMode();" class="text-primary mr-2" color="var(--v-theme-surface)"
|
||||
variant="flat" rounded="sm">
|
||||
<!-- 明暗主题切换按钮 -->
|
||||
<!-- 主题切换按钮 -->
|
||||
<v-btn size="small" @click="toggleDarkMode();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm">
|
||||
<v-icon v-if="useCustomizerStore().uiTheme === 'PurpleThemeDark'">mdi-weather-night</v-icon>
|
||||
<v-icon v-else>mdi-white-balance-sunny</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="updateStatusDialog" width="1000">
|
||||
<!-- 更新对话框 -->
|
||||
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1000'" :fullscreen="$vuetify.display.xs">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="text-primary mr-2"
|
||||
color="var(--v-theme-surface)"
|
||||
variant="flat" rounded="sm" v-bind="props">
|
||||
更新
|
||||
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
|
||||
<v-icon class="hidden-sm-and-up">mdi-update</v-icon>
|
||||
<span class="hidden-xs">更新</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-card-title class="mobile-card-title">
|
||||
<span class="text-h5">更新 AstrBot</span>
|
||||
<v-btn v-if="$vuetify.display.xs" icon @click="updateStatusDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
@@ -308,10 +312,9 @@ commonStore.getStartTime();
|
||||
<small style="margin-left: 4px;">{{ updateStatus }}</small>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<div v-if="releaseMessage"
|
||||
style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;"
|
||||
v-html="marked(releaseMessage)" class="markdown-content">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-4">
|
||||
@@ -422,11 +425,12 @@ commonStore.getStartTime();
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="dialog" persistent max-width="500">
|
||||
<!-- 账户对话框 -->
|
||||
<v-dialog v-model="dialog" persistent :max-width="$vuetify.display.xs ? '90%' : '500'">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" class="text-primary mr-4" color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
|
||||
<v-icon class="mr-1">mdi-account</v-icon>
|
||||
账户
|
||||
<v-btn size="small" class="action-btn mr-4" color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
|
||||
<v-icon>mdi-account</v-icon>
|
||||
<span class="hidden-xs ml-1">账户</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card class="account-dialog">
|
||||
@@ -582,4 +586,68 @@ commonStore.getStartTime();
|
||||
.account-dialog .v-avatar:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 响应式布局样式 */
|
||||
.logo-container {
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-logo {
|
||||
margin-left: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 24px;
|
||||
font-weight: 1000;
|
||||
}
|
||||
|
||||
.logo-text-light {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 12px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* 移动端对话框标题样式 */
|
||||
.mobile-card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 移动端样式优化 */
|
||||
@media (max-width: 600px) {
|
||||
.logo-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-right: 4px;
|
||||
min-width: 32px !important;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.v-card-title {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.v-card-text {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.v-tabs .v-tab {
|
||||
padding: 0 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
const ChatBoxRoutes = {
|
||||
path: '/chatbox',
|
||||
component: () => import('@/layouts/blank/BlankLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'ChatBox',
|
||||
path: '/chatbox',
|
||||
component: () => import('@/views/ChatBoxPage.vue'),
|
||||
children: [
|
||||
{
|
||||
path: ':conversationId',
|
||||
name: 'ChatBoxDetail',
|
||||
component: () => import('@/views/ChatBoxPage.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default ChatBoxRoutes;
|
||||
@@ -81,7 +81,15 @@ const MainRoutes = {
|
||||
{
|
||||
name: 'Chat',
|
||||
path: '/chat',
|
||||
component: () => import('@/views/ChatPage.vue')
|
||||
component: () => import('@/views/ChatPage.vue'),
|
||||
children: [
|
||||
{
|
||||
path: ':conversationId',
|
||||
name: 'ChatDetail',
|
||||
component: () => import('@/views/ChatPage.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Settings',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import MainRoutes from './MainRoutes';
|
||||
import AuthRoutes from './AuthRoutes';
|
||||
import ChatBoxRoutes from './ChatBoxRoutes';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
MainRoutes,
|
||||
AuthRoutes
|
||||
AuthRoutes,
|
||||
ChatBoxRoutes
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
import ChatPage from './ChatPage.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div id="container">
|
||||
<ChatPage chatbox-mode="true"></ChatPage>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#container {
|
||||
min-width: 600px;
|
||||
min-height: 370px;
|
||||
max-width: 1100px;
|
||||
max-height: 860px;
|
||||
padding: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +1,67 @@
|
||||
<script setup>
|
||||
import { router } from '@/router';
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
import { ref } from 'vue';
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
chatboxMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="chat-page-card">
|
||||
<v-card-text class="chat-page-container">
|
||||
<div class="chat-layout">
|
||||
<div class="sidebar-panel">
|
||||
<div style="padding: 16px; padding-top: 8px;">
|
||||
<v-btn variant="elevated" rounded="lg" class="new-chat-btn" @click="newC" :disabled="!currCid"
|
||||
prepend-icon="mdi-plus">创建对话</v-btn>
|
||||
<div class="sidebar-panel" :class="{ 'sidebar-collapsed': sidebarCollapsed }"
|
||||
@mouseenter="handleSidebarMouseEnter" @mouseleave="handleSidebarMouseLeave">
|
||||
|
||||
<div style="display: flex; align-items: center; justify-content: center; padding: 16px; padding-bottom: 0px;" v-if="props.chatboxMode">
|
||||
<img width="50" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
|
||||
<span v-if="!sidebarCollapsed" style="font-weight: 1000; font-size: 26px; margin-left: 8px;" class="text-secondary">AstrBot</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sidebar-collapse-btn-container">
|
||||
<v-btn icon class="sidebar-collapse-btn" @click="toggleSidebar" variant="text"
|
||||
color="deep-purple">
|
||||
<v-icon>{{ (sidebarCollapsed || (!sidebarCollapsed && sidebarHoverExpanded)) ?
|
||||
'mdi-chevron-right' : 'mdi-chevron-left' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="conversations-container">
|
||||
<div style="padding: 16px; padding-top: 8px;">
|
||||
<v-btn rounded="lg" class="new-chat-btn" @click="newC" :disabled="!currCid"
|
||||
v-if="!sidebarCollapsed" prepend-icon="mdi-plus">创建对话</v-btn>
|
||||
<v-btn icon="mdi-plus" rounded="lg" @click="newC" :disabled="!currCid" v-if="sidebarCollapsed"
|
||||
elevation="0"></v-btn>
|
||||
</div>
|
||||
|
||||
<div style="overflow-y: auto;" :class="{ 'fade-in': sidebarHoverExpanded }"
|
||||
v-if="!sidebarCollapsed">
|
||||
<v-card class="conversation-list-card" v-if="conversations.length > 0" flat>
|
||||
<v-list density="compact" nav class="conversation-list"
|
||||
@update:selected="getConversationMessages">
|
||||
<v-list-item v-for="(item, i) in conversations" :key="item.cid" :value="item.cid"
|
||||
rounded="lg" class="conversation-item" active-color="secondary">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small" icon="mdi-message-text-outline"></v-icon>
|
||||
<v-list-item-title v-if="!sidebarCollapsed" class="conversation-title">{{ item.title
|
||||
|| '新对话' }}</v-list-item-title>
|
||||
<!-- <v-list-item-subtitle v-if="!sidebarCollapsed" class="timestamp">{{
|
||||
formatDate(item.updated_at)
|
||||
}}</v-list-item-subtitle> -->
|
||||
|
||||
<template v-if="!sidebarCollapsed" v-slot:append>
|
||||
<v-btn icon="mdi-pencil" size="x-small" variant="text" class="edit-title-btn"
|
||||
@click.stop="showEditTitleDialog(item.cid, item.title)" />
|
||||
</template>
|
||||
<v-list-item-title class="conversation-title">新对话</v-list-item-title>
|
||||
<v-list-item-subtitle class="timestamp">{{ formatDate(item.updated_at)
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
@@ -37,12 +69,14 @@ marked.setOptions({
|
||||
<v-fade-transition>
|
||||
<div class="no-conversations" v-if="conversations.length === 0">
|
||||
<v-icon icon="mdi-message-text-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<div class="no-conversations-text">暂无对话历史</div>
|
||||
<div class="no-conversations-text" v-if="!sidebarCollapsed || sidebarHoverExpanded">
|
||||
暂无对话历史</div>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div style="padding: 16px; padding-bottom: 0px;" :class="{ 'fade-in': sidebarHoverExpanded }"
|
||||
v-if="!sidebarCollapsed">
|
||||
<div class="sidebar-section-title">
|
||||
系统状态
|
||||
</div>
|
||||
@@ -53,7 +87,7 @@ marked.setOptions({
|
||||
<v-icon :icon="status?.llm_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
size="x-small"></v-icon>
|
||||
</template>
|
||||
LLM 服务
|
||||
<span>LLM 服务</span>
|
||||
</v-chip>
|
||||
|
||||
<v-chip class="status-chip" :color="status?.stt_enabled ? 'success' : 'grey-lighten-2'"
|
||||
@@ -62,7 +96,7 @@ marked.setOptions({
|
||||
<v-icon :icon="status?.stt_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
size="x-small"></v-icon>
|
||||
</template>
|
||||
语音转文本
|
||||
<span>语音转文本</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +110,20 @@ marked.setOptions({
|
||||
|
||||
<!-- 右侧聊天内容区域 -->
|
||||
<div class="chat-content-panel">
|
||||
|
||||
<div class="conversation-header fade-in">
|
||||
<div class="conversation-header-content" v-if="currCid && getCurrentConversation">
|
||||
<h2 class="conversation-header-title">{{ getCurrentConversation.title || '新对话' }}</h2>
|
||||
<div class="conversation-header-time">{{ formatDate(getCurrentConversation.updated_at) }}</div>
|
||||
</div>
|
||||
<div class="conversation-header-actions">
|
||||
<!-- router 推送到 /chatbox -->
|
||||
<v-icon @click="router.push('/chatbox')" v-if="!props.chatboxMode"
|
||||
class="fullscreen-icon">mdi-fullscreen</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider v-if="currCid && getCurrentConversation" class="conversation-divider"></v-divider>
|
||||
|
||||
<div class="messages-container" ref="messageContainer">
|
||||
<!-- 空聊天欢迎页 -->
|
||||
<div class="welcome-container fade-in" v-if="messages.length == 0">
|
||||
@@ -195,6 +243,22 @@ marked.setOptions({
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 编辑对话标题对话框 -->
|
||||
<v-dialog v-model="editTitleDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-title">编辑对话标题</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="editingTitle" label="对话标题" variant="outlined" hide-details class="mt-2"
|
||||
@keyup.enter="saveTitle" autofocus />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="editTitleDialog = false" color="grey-darken-1">取消</v-btn>
|
||||
<v-btn text @click="saveTitle" color="primary">保存</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -230,6 +294,68 @@ export default {
|
||||
ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒
|
||||
|
||||
mediaCache: {}, // Add a cache to store media blobs
|
||||
|
||||
// 添加对话标题编辑相关变量
|
||||
editTitleDialog: false,
|
||||
editingTitle: '',
|
||||
editingCid: '',
|
||||
|
||||
// 侧边栏折叠状态
|
||||
sidebarCollapsed: false,
|
||||
sidebarHovered: false,
|
||||
sidebarHoverTimer: null,
|
||||
sidebarHoverExpanded: false,
|
||||
sidebarHoverDelay: 100, // 悬停延迟,单位毫秒
|
||||
|
||||
pendingCid: null, // Store pending conversation ID for route handling
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Get the current conversation from the conversations array
|
||||
getCurrentConversation() {
|
||||
if (!this.currCid) return null;
|
||||
return this.conversations.find(c => c.cid === this.currCid);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
// Watch for route changes to handle direct navigation to /chat/<cid>
|
||||
'$route': {
|
||||
immediate: true,
|
||||
handler(to) {
|
||||
console.log('Route changed:', to.path);
|
||||
// Check if the route matches /chat/<cid> pattern
|
||||
if (to.path.startsWith('/chat/') || to.path.startsWith('/chatbox/')) {
|
||||
const pathCid = to.path.split('/')[2];
|
||||
console.log('Path CID:', pathCid);
|
||||
if (pathCid && pathCid !== this.currCid) {
|
||||
// If conversations are already loaded
|
||||
if (this.conversations.length > 0) {
|
||||
const conversation = this.conversations.find(c => c.cid === pathCid);
|
||||
if (conversation) {
|
||||
this.getConversationMessages([pathCid]);
|
||||
}
|
||||
} else {
|
||||
// Store the cid to be used after conversations are loaded
|
||||
this.pendingCid = pathCid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Watch for conversations loaded to handle pending cid
|
||||
conversations: {
|
||||
handler(newConversations) {
|
||||
if (this.pendingCid && newConversations.length > 0) {
|
||||
const conversation = newConversations.find(c => c.cid === this.pendingCid);
|
||||
if (conversation) {
|
||||
this.getConversationMessages([this.pendingCid]);
|
||||
this.pendingCid = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -248,6 +374,12 @@ export default {
|
||||
|
||||
// 添加keyup事件监听
|
||||
document.addEventListener('keyup', this.handleInputKeyUp);
|
||||
|
||||
// 从 localStorage 获取侧边栏折叠状态
|
||||
const savedCollapseState = localStorage.getItem('sidebarCollapsed');
|
||||
if (savedCollapseState !== null) {
|
||||
this.sidebarCollapsed = JSON.parse(savedCollapseState);
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
@@ -259,11 +391,86 @@ export default {
|
||||
// 移除keyup事件监听
|
||||
document.removeEventListener('keyup', this.handleInputKeyUp);
|
||||
|
||||
// 清除悬停定时器
|
||||
if (this.sidebarHoverTimer) {
|
||||
clearTimeout(this.sidebarHoverTimer);
|
||||
}
|
||||
|
||||
// Cleanup blob URLs
|
||||
this.cleanupMediaCache();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 切换侧边栏折叠状态
|
||||
toggleSidebar() {
|
||||
if (this.sidebarHoverExpanded) {
|
||||
this.sidebarHoverExpanded = false;
|
||||
return
|
||||
}
|
||||
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||||
// 保存折叠状态到 localStorage
|
||||
localStorage.setItem('sidebarCollapsed', JSON.stringify(this.sidebarCollapsed));
|
||||
},
|
||||
|
||||
// 侧边栏鼠标悬停处理
|
||||
handleSidebarMouseEnter() {
|
||||
if (!this.sidebarCollapsed) return;
|
||||
|
||||
this.sidebarHovered = true;
|
||||
|
||||
// 设置延迟定时器
|
||||
this.sidebarHoverTimer = setTimeout(() => {
|
||||
if (this.sidebarHovered) {
|
||||
this.sidebarHoverExpanded = true;
|
||||
this.sidebarCollapsed = false;
|
||||
}
|
||||
}, this.sidebarHoverDelay);
|
||||
},
|
||||
|
||||
handleSidebarMouseLeave() {
|
||||
this.sidebarHovered = false;
|
||||
|
||||
// 清除定时器
|
||||
if (this.sidebarHoverTimer) {
|
||||
clearTimeout(this.sidebarHoverTimer);
|
||||
this.sidebarHoverTimer = null;
|
||||
}
|
||||
|
||||
if (this.sidebarHoverExpanded) {
|
||||
this.sidebarCollapsed = true;
|
||||
}
|
||||
this.sidebarHoverExpanded = false;
|
||||
},
|
||||
|
||||
// 显示编辑对话标题对话框
|
||||
showEditTitleDialog(cid, title) {
|
||||
this.editingCid = cid;
|
||||
this.editingTitle = title || ''; // 如果标题为空,则设置为空字符串
|
||||
this.editTitleDialog = true;
|
||||
},
|
||||
|
||||
// 保存对话标题
|
||||
saveTitle() {
|
||||
if (!this.editingCid) return;
|
||||
|
||||
const trimmedTitle = this.editingTitle.trim();
|
||||
axios.post('/api/chat/rename_conversation', {
|
||||
conversation_id: this.editingCid,
|
||||
title: trimmedTitle
|
||||
})
|
||||
.then(response => {
|
||||
// 更新本地对话列表中的标题
|
||||
const conversation = this.conversations.find(c => c.cid === this.editingCid);
|
||||
if (conversation) {
|
||||
conversation.title = trimmedTitle;
|
||||
}
|
||||
this.editTitleDialog = false;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('重命名对话失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
async getMediaFile(filename) {
|
||||
if (this.mediaCache[filename]) {
|
||||
return this.mediaCache[filename];
|
||||
@@ -274,7 +481,7 @@ export default {
|
||||
params: { filename },
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
|
||||
const blobUrl = URL.createObjectURL(response.data);
|
||||
this.mediaCache[filename] = blobUrl;
|
||||
return blobUrl;
|
||||
@@ -374,6 +581,14 @@ export default {
|
||||
} else if (chunk_json.type === 'end') {
|
||||
in_streaming = false;
|
||||
continue;
|
||||
} else if (chunk_json.type === 'update_title') {
|
||||
// 更新对话标题
|
||||
const conversation = this.conversations.find(c => c.cid === chunk_json.cid);
|
||||
if (conversation) {
|
||||
conversation.title = chunk_json.data;
|
||||
}
|
||||
} else {
|
||||
console.warn('未知数据类型:', chunk_json.type);
|
||||
}
|
||||
this.scrollToBottom();
|
||||
}
|
||||
@@ -472,13 +687,36 @@ export default {
|
||||
getConversations() {
|
||||
axios.get('/api/chat/conversations').then(response => {
|
||||
this.conversations = response.data.data;
|
||||
|
||||
// If there's a pending conversation ID from the route
|
||||
if (this.pendingCid) {
|
||||
const conversation = this.conversations.find(c => c.cid === this.pendingCid);
|
||||
if (conversation) {
|
||||
this.getConversationMessages([this.pendingCid]);
|
||||
this.pendingCid = null;
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err.response.status === 401) {
|
||||
this.$router.push('/auth/login?redirect=/chatbox');
|
||||
}
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
getConversationMessages(cid) {
|
||||
if (!cid[0])
|
||||
return;
|
||||
|
||||
// Update the URL to reflect the selected conversation
|
||||
if (this.$route.path !== `/chat/${cid[0]}` && this.$route.path !== `/chatbox/${cid[0]}`) {
|
||||
if (this.$route.path.startsWith('/chatbox')) {
|
||||
router.push(`/chatbox/${cid[0]}`);
|
||||
} else {
|
||||
router.push(`/chat/${cid[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
axios.get('/api/chat/get_conversation?conversation_id=' + cid[0]).then(async response => {
|
||||
this.currCid = cid[0];
|
||||
let message = JSON.parse(response.data.data.history);
|
||||
@@ -511,17 +749,31 @@ export default {
|
||||
});
|
||||
},
|
||||
async newConversation() {
|
||||
await axios.get('/api/chat/new_conversation').then(response => {
|
||||
this.currCid = response.data.data.conversation_id;
|
||||
return axios.get('/api/chat/new_conversation').then(response => {
|
||||
const cid = response.data.data.conversation_id;
|
||||
this.currCid = cid;
|
||||
// Update the URL to reflect the new conversation
|
||||
if (this.$route.path.startsWith('/chatbox')) {
|
||||
router.push(`/chatbox/${cid}`);
|
||||
} else {
|
||||
router.push(`/chat/${cid}`);
|
||||
}
|
||||
this.getConversations();
|
||||
return cid;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
newC() {
|
||||
this.currCid = '';
|
||||
this.messages = [];
|
||||
if (this.$route.path.startsWith('/chatbox')) {
|
||||
router.push('/chatbox');
|
||||
} else {
|
||||
router.push('/chat');
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(timestamp) {
|
||||
@@ -550,7 +802,8 @@ export default {
|
||||
|
||||
async sendMessage() {
|
||||
if (this.currCid == '') {
|
||||
await this.newConversation();
|
||||
const cid = await this.newConversation();
|
||||
// URL is already updated in newConversation method
|
||||
}
|
||||
|
||||
// Create a message object with actual URLs for display
|
||||
@@ -601,15 +854,15 @@ export default {
|
||||
audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [] // Already contains just filename
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
this.prompt = '';
|
||||
this.stagedImagesName = [];
|
||||
this.stagedAudioUrl = "";
|
||||
this.loadingChat = false;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
.then(response => {
|
||||
this.prompt = '';
|
||||
this.stagedImagesName = [];
|
||||
this.stagedAudioUrl = "";
|
||||
this.loadingChat = false;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
@@ -703,29 +956,43 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加淡入动画 */
|
||||
@keyframes fadeInContent {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeInContent 0.2s ease-in forwards;
|
||||
}
|
||||
|
||||
/* 聊天页面布局 */
|
||||
/* todo: 聊天页面背景颜色有问题 */
|
||||
.chat-page-card {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.chat-page-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 120px);
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 120px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chat-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 - 优化版 */
|
||||
.sidebar-panel {
|
||||
max-width: 270px;
|
||||
min-width: 240px;
|
||||
@@ -736,57 +1003,34 @@ export default {
|
||||
background-color: var(--v-theme-containerBg);
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
/* 防止内容溢出 */
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
/* 侧边栏折叠状态 */
|
||||
.sidebar-collapsed {
|
||||
max-width: 75px;
|
||||
min-width: 75px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.conversations-container {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
/* 当悬停展开时 */
|
||||
.sidebar-collapsed.sidebar-hovered {
|
||||
max-width: 270px;
|
||||
min-width: 240px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
/* 侧边栏折叠按钮 */
|
||||
.sidebar-collapse-btn-container {
|
||||
margin: 16px;
|
||||
margin-bottom: 0px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sidebar-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--v-theme-secondaryText);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.new-chat-btn {
|
||||
width: 100%;
|
||||
background-color: #673ab7 !important;
|
||||
color: white !important;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(103, 58, 183, 0.25) !important;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
|
||||
.new-chat-btn:hover {
|
||||
background-color: #7e57c2 !important;
|
||||
box-shadow: 0 4px 12px rgba(103, 58, 183, 0.3) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.conversation-list-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: none !important;
|
||||
background-color: var(--v-theme-containerBg);
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
.sidebar-collapse-btn {
|
||||
opacity: 0.6;
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
padding: 0;
|
||||
@@ -798,7 +1042,8 @@ export default {
|
||||
transition: all 0.2s ease;
|
||||
height: auto !important;
|
||||
min-height: 56px;
|
||||
padding: 8px 12px !important;
|
||||
padding: 8px 16px !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.conversation-item:hover {
|
||||
@@ -810,12 +1055,25 @@ export default {
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 2px;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 11px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
line-height: 1;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.sidebar-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--v-theme-secondaryText);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 4px;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.status-chips {
|
||||
@@ -823,6 +1081,7 @@ export default {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
@@ -840,6 +1099,7 @@ export default {
|
||||
letter-spacing: 0.25px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2em;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.delete-chat-btn:hover {
|
||||
@@ -859,6 +1119,7 @@ export default {
|
||||
.no-conversations-text {
|
||||
font-size: 14px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
/* 聊天内容区域 */
|
||||
@@ -1152,4 +1413,55 @@ export default {
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* 对话框标题样式 */
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 对话标题和时间样式 */
|
||||
.conversation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 16px 16px 16px;
|
||||
border-bottom: 1px solid var(--v-theme-border);
|
||||
width: 100%;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.conversation-header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.conversation-header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--v-theme-primaryText);
|
||||
}
|
||||
|
||||
.conversation-header-time {
|
||||
font-size: 12px;
|
||||
color: var(--v-theme-secondaryText);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.conversation-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fullscreen-icon {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fullscreen-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
<v-icon size="x-large" color="primary" class="me-2">mdi-creation</v-icon>服务提供商管理
|
||||
</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis mb-4">
|
||||
管理AI服务提供商,连接到不同的大语言模型
|
||||
管理模型服务提供商
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -20,6 +20,9 @@
|
||||
<span class="text-h6">服务提供商</span>
|
||||
<v-chip color="info" size="small" class="ml-2">{{ config_data.provider?.length || 0 }}</v-chip>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" prepend-icon="mdi-cog" variant="tonal" class="me-2" @click="showSettingsDialog = true">
|
||||
设置
|
||||
</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddProviderDialog = true">
|
||||
新增服务提供商
|
||||
</v-btn>
|
||||
@@ -253,6 +256,49 @@
|
||||
</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>服务提供商设置</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">启用提供商会话隔离</div>
|
||||
<div class="text-caption text-medium-emphasis">不同会话将可独立选择文本生成、TTS、STT 等服务提供商。</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">
|
||||
关闭
|
||||
</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">
|
||||
@@ -285,6 +331,11 @@ export default {
|
||||
metadata: {},
|
||||
showProviderCfg: false,
|
||||
|
||||
// 设置对话框相关
|
||||
showSettingsDialog: false,
|
||||
sessionSeparationEnabled: false,
|
||||
sessionSettingLoading: false,
|
||||
|
||||
newSelectedProviderName: '',
|
||||
newSelectedProviderConfig: {},
|
||||
updatingMode: false,
|
||||
@@ -354,6 +405,7 @@ export default {
|
||||
|
||||
mounted() {
|
||||
this.getConfig();
|
||||
this.getSessionSeparationStatus();
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -566,6 +618,32 @@ 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 || "获取会话隔离配置失败");
|
||||
});
|
||||
},
|
||||
|
||||
// 更新会话隔离配置
|
||||
updateSessionSeparation() {
|
||||
this.sessionSettingLoading = true;
|
||||
axios.post('/api/config/provider/set_session_seperate', {
|
||||
enable: this.sessionSeparationEnabled
|
||||
}).then((res) => {
|
||||
this.showSuccess(res.data.message || "会话隔离设置已更新");
|
||||
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";
|
||||
|
||||
@@ -23,6 +23,8 @@ async function validate(values: any, { setErrors }: any) {
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
// @ts-ignore
|
||||
authStore.returnUrl = new URLSearchParams(window.location.search).get('redirect');
|
||||
return authStore.login(username.value, password_).then((res) => {
|
||||
console.log(res);
|
||||
loading.value = false;
|
||||
|
||||
+55
-44
@@ -12,6 +12,7 @@ from astrbot.api import sp
|
||||
from astrbot.api.provider import ProviderRequest
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.provider.sources.dify_source import ProviderDify
|
||||
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
@@ -139,6 +140,7 @@ class Main(star.Star):
|
||||
{notice}"""
|
||||
|
||||
event.set_result(MessageEventResult().message(msg).use_t2i(False))
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("llm")
|
||||
async def llm(self, event: AstrMessageEvent):
|
||||
@@ -413,20 +415,21 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
event.set_result(MessageEventResult().message("删除白名单成功。"))
|
||||
except ValueError:
|
||||
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
||||
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("provider")
|
||||
async def provider(
|
||||
self, event: AstrMessageEvent, idx: Union[str, int] = None, idx2: int = None
|
||||
):
|
||||
"""查看或者切换 LLM Provider"""
|
||||
umo = event.unified_msg_origin
|
||||
|
||||
if idx is None:
|
||||
ret = "## 载入的 LLM 提供商\n"
|
||||
for idx, llm in enumerate(self.context.get_all_providers()):
|
||||
id_ = llm.meta().id
|
||||
ret += f"{idx + 1}. {id_} ({llm.meta().model})"
|
||||
provider_using = self.context.get_using_provider()
|
||||
provider_using = self.context.get_using_provider(umo=umo)
|
||||
if provider_using and provider_using.meta().id == id_:
|
||||
ret += " (当前使用)"
|
||||
ret += "\n"
|
||||
@@ -437,7 +440,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
for idx, tts in enumerate(tts_providers):
|
||||
id_ = tts.meta().id
|
||||
ret += f"{idx + 1}. {id_}"
|
||||
tts_using = self.context.get_using_tts_provider()
|
||||
tts_using = self.context.get_using_tts_provider(umo=umo)
|
||||
if tts_using and tts_using.meta().id == id_:
|
||||
ret += " (当前使用)"
|
||||
ret += "\n"
|
||||
@@ -448,7 +451,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
for idx, stt in enumerate(stt_providers):
|
||||
id_ = stt.meta().id
|
||||
ret += f"{idx + 1}. {id_}"
|
||||
stt_using = self.context.get_using_stt_provider()
|
||||
stt_using = self.context.get_using_stt_provider(umo=umo)
|
||||
if stt_using and stt_using.meta().id == id_:
|
||||
ret += " (当前使用)"
|
||||
ret += "\n"
|
||||
@@ -461,46 +464,54 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
ret += "\n使用 /provider stt <切换> STT 提供商。"
|
||||
|
||||
event.set_result(MessageEventResult().message(ret))
|
||||
else:
|
||||
if idx == "tts":
|
||||
if idx2 is None:
|
||||
event.set_result(MessageEventResult().message("请输入序号。"))
|
||||
return
|
||||
else:
|
||||
if idx2 > len(self.context.get_all_tts_providers()) or idx2 < 1:
|
||||
event.set_result(MessageEventResult().message("无效的序号。"))
|
||||
provider = self.context.get_all_tts_providers()[idx2 - 1]
|
||||
id_ = provider.meta().id
|
||||
self.context.provider_manager.curr_tts_provider_inst = provider
|
||||
sp.put("curr_provider_tts", id_)
|
||||
event.set_result(
|
||||
MessageEventResult().message(f"成功切换到 {id_}。")
|
||||
)
|
||||
elif idx == "stt":
|
||||
if idx2 is None:
|
||||
event.set_result(MessageEventResult().message("请输入序号。"))
|
||||
return
|
||||
else:
|
||||
if idx2 > len(self.context.get_all_stt_providers()) or idx2 < 1:
|
||||
event.set_result(MessageEventResult().message("无效的序号。"))
|
||||
provider = self.context.get_all_stt_providers()[idx2 - 1]
|
||||
id_ = provider.meta().id
|
||||
self.context.provider_manager.curr_stt_provider_inst = provider
|
||||
sp.put("curr_provider_stt", id_)
|
||||
event.set_result(
|
||||
MessageEventResult().message(f"成功切换到 {id_}。")
|
||||
)
|
||||
elif isinstance(idx, int):
|
||||
if idx > len(self.context.get_all_providers()) or idx < 1:
|
||||
event.set_result(MessageEventResult().message("无效的序号。"))
|
||||
|
||||
provider = self.context.get_all_providers()[idx - 1]
|
||||
id_ = provider.meta().id
|
||||
self.context.provider_manager.curr_provider_inst = provider
|
||||
sp.put("curr_provider", id_)
|
||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||
elif idx == "tts":
|
||||
if idx2 is None:
|
||||
event.set_result(MessageEventResult().message("请输入序号。"))
|
||||
return
|
||||
else:
|
||||
event.set_result(MessageEventResult().message("无效的参数。"))
|
||||
if idx2 > len(self.context.get_all_tts_providers()) or idx2 < 1:
|
||||
event.set_result(MessageEventResult().message("无效的序号。"))
|
||||
provider = self.context.get_all_tts_providers()[idx2 - 1]
|
||||
id_ = provider.meta().id
|
||||
await self.context.provider_manager.set_provider(
|
||||
provider_id=id_,
|
||||
provider_type=ProviderType.TEXT_TO_SPEECH,
|
||||
umo=umo,
|
||||
)
|
||||
event.set_result(
|
||||
MessageEventResult().message(f"成功切换到 {id_}。")
|
||||
)
|
||||
elif idx == "stt":
|
||||
if idx2 is None:
|
||||
event.set_result(MessageEventResult().message("请输入序号。"))
|
||||
return
|
||||
else:
|
||||
if idx2 > len(self.context.get_all_stt_providers()) or idx2 < 1:
|
||||
event.set_result(MessageEventResult().message("无效的序号。"))
|
||||
provider = self.context.get_all_stt_providers()[idx2 - 1]
|
||||
id_ = provider.meta().id
|
||||
await self.context.provider_manager.set_provider(
|
||||
provider_id=id_,
|
||||
provider_type=ProviderType.SPEECH_TO_TEXT,
|
||||
umo=umo,
|
||||
)
|
||||
event.set_result(
|
||||
MessageEventResult().message(f"成功切换到 {id_}。")
|
||||
)
|
||||
elif isinstance(idx, int):
|
||||
if idx > len(self.context.get_all_providers()) or idx < 1:
|
||||
event.set_result(MessageEventResult().message("无效的序号。"))
|
||||
|
||||
provider = self.context.get_all_providers()[idx - 1]
|
||||
id_ = provider.meta().id
|
||||
await self.context.provider_manager.set_provider(
|
||||
provider_id=id_,
|
||||
provider_type=ProviderType.CHAT_COMPLETION,
|
||||
umo=umo,
|
||||
)
|
||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||
else:
|
||||
event.set_result(MessageEventResult().message("无效的参数。"))
|
||||
|
||||
@filter.command("reset")
|
||||
async def reset(self, message: AstrMessageEvent):
|
||||
@@ -572,7 +583,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
ret += f"\n聊天增强: 已清除 {cnt} 条聊天记录。"
|
||||
|
||||
message.set_result(MessageEventResult().message(ret))
|
||||
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("model")
|
||||
async def model_ls(
|
||||
|
||||
@@ -26,9 +26,7 @@ class Waiter(Star):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
self.empty_mention_waiting = self.context.get_config()["platform_settings"][
|
||||
"empty_mention_waiting"
|
||||
]
|
||||
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)
|
||||
@@ -49,44 +47,49 @@ class Waiter(Star):
|
||||
if (
|
||||
isinstance(messages[0], Comp.At)
|
||||
and str(messages[0].qq) == str(event.get_self_id())
|
||||
and self.empty_mention_waiting
|
||||
and self.p_settings.get("empty_mention_waiting", True)
|
||||
) or (
|
||||
isinstance(messages[0], Comp.Plain)
|
||||
and messages[0].text.strip() in self.wake_prefix
|
||||
):
|
||||
try:
|
||||
# 尝试使用 LLM 生成更生动的回复
|
||||
func_tools_mgr = self.context.get_llm_tool_manager()
|
||||
if self.p_settings.get("empty_mention_waiting_need_reply", True):
|
||||
try:
|
||||
# 尝试使用 LLM 生成更生动的回复
|
||||
func_tools_mgr = self.context.get_llm_tool_manager()
|
||||
|
||||
# 获取用户当前的对话信息
|
||||
curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
||||
event.unified_msg_origin
|
||||
)
|
||||
conversation = None
|
||||
|
||||
if curr_cid:
|
||||
conversation = await self.context.conversation_manager.get_conversation(
|
||||
event.unified_msg_origin, curr_cid
|
||||
)
|
||||
else:
|
||||
# 创建新对话
|
||||
curr_cid = await self.context.conversation_manager.new_conversation(
|
||||
# 获取用户当前的对话信息
|
||||
curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
||||
event.unified_msg_origin
|
||||
)
|
||||
conversation = None
|
||||
|
||||
# 使用 LLM 生成回复
|
||||
yield event.request_llm(
|
||||
prompt="注意,你正在社交媒体上中与用户进行聊天,用户只是通过@来唤醒你,但并未在这条消息中输入内容,他可能会在接下来一条发送他想发送的内容。请你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。注意,你仅需要输出要回复用户的内容,不要输出其他任何东西",
|
||||
func_tool_manager=func_tools_mgr,
|
||||
session_id=curr_cid,
|
||||
contexts=[],
|
||||
system_prompt="",
|
||||
conversation=conversation,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"LLM response failed: {str(e)}")
|
||||
# LLM 回复失败,使用原始预设回复
|
||||
yield event.plain_result("想要问什么呢?😄")
|
||||
if curr_cid:
|
||||
conversation = await self.context.conversation_manager.get_conversation(
|
||||
event.unified_msg_origin, curr_cid
|
||||
)
|
||||
else:
|
||||
# 创建新对话
|
||||
curr_cid = await self.context.conversation_manager.new_conversation(
|
||||
event.unified_msg_origin
|
||||
)
|
||||
|
||||
# 使用 LLM 生成回复
|
||||
yield event.request_llm(
|
||||
prompt=(
|
||||
"注意,你正在社交媒体上中与用户进行聊天,用户只是通过@来唤醒你,但并未在这条消息中输入内容,他可能会在接下来一条发送他想发送的内容。"
|
||||
"你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
|
||||
"请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
|
||||
),
|
||||
func_tool_manager=func_tools_mgr,
|
||||
session_id=curr_cid,
|
||||
contexts=[],
|
||||
system_prompt="",
|
||||
conversation=conversation,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"LLM response failed: {str(e)}")
|
||||
# LLM 回复失败,使用原始预设回复
|
||||
yield event.plain_result("想要问什么呢?😄")
|
||||
|
||||
@session_waiter(60)
|
||||
async def empty_mention_waiter(
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "3.5.14"
|
||||
version = "3.5.15"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -204,7 +204,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "astrbot"
|
||||
version = "3.5.12"
|
||||
version = "3.5.14"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocqhttp" },
|
||||
@@ -266,7 +266,7 @@ requires-dist = [
|
||||
{ name = "defusedxml", specifier = ">=0.7.1" },
|
||||
{ name = "dingtalk-stream", specifier = ">=0.22.1" },
|
||||
{ name = "docstring-parser", specifier = ">=0.16" },
|
||||
{ name = "faiss-cpu", specifier = ">=1.11.0" },
|
||||
{ name = "faiss-cpu", specifier = ">=1.10.0" },
|
||||
{ name = "filelock", specifier = ">=3.18.0" },
|
||||
{ name = "google-genai", specifier = ">=1.14.0" },
|
||||
{ name = "googlesearch-python", specifier = ">=1.3.0" },
|
||||
|
||||
Reference in New Issue
Block a user