Compare commits

...

29 Commits

Author SHA1 Message Date
Soulter 609e723322 v3.4.24 2025-02-10 00:34:02 +08:00
Soulter c564a1d53e fix: raw_completion 没有正确传递 #439 2025-02-10 00:26:53 +08:00
Soulter a7fe31f28b fix: 修复指令不经过唤醒前缀也能生效的问题。在引用消息的时候无法使用前缀唤醒机器人 #444 2025-02-09 22:35:52 +08:00
Soulter a84dc599d6 fix: 修复 /tts 指令 2025-02-09 22:14:10 +08:00
Soulter 8da029add9 feat: 支持 TTS, STT 提供商的显示和快捷切换 2025-02-09 22:08:51 +08:00
Soulter ba45a2d270 feat: 支持设置GitHub反向代理地址 2025-02-09 18:51:53 +08:00
Soulter cb56b22aea Update README.md 2025-02-09 16:49:00 +08:00
Soulter 23cc5b31ba perf: 从压缩包上传插件时,去除branch尾缀 2025-02-09 14:59:27 +08:00
Soulter e8d99f0460 fix: 修复戳一戳消息报错 2025-02-09 13:57:33 +08:00
Soulter 6bcd10cd5c fix: gemini 报错时显示 apikey 2025-02-09 13:56:55 +08:00
Soulter 619fb20c5f fix: drun 不支持函数调用的报错 2025-02-09 01:20:11 +08:00
Soulter 386a312e96 fix: 修复一些typo 2025-02-08 22:52:24 +08:00
Soulter 2759d347e6 update: add socksio, echatpy, cryptography to dockerfile 2025-02-08 22:10:17 +08:00
Soulter b6ec327b49 perf:完善主动会话 2025-02-08 22:04:36 +08:00
Soulter ee02d622ba v3.4.23 2025-02-08 21:42:37 +08:00
Soulter 5c4a6083f5 Merge pull request #433 from Cvandia/master
支持 fishaudio tts 文字转语音
2025-02-08 21:20:03 +08:00
Soulter 49e63a3d3d perf: 优化报错显示 2025-02-08 21:19:25 +08:00
Soulter 6bae9dc9ed 👌 perf: 当响应头不为audio/wav时抛出报错 2025-02-08 21:16:09 +08:00
Cvandia 5fa1979a46 🐛 fix: 移除调试过程的不必要的文件写入操作 2025-02-08 20:49:37 +08:00
Cvandia b40d4fa315 Merge remote-tracking branch 'upstream/master' 2025-02-08 20:45:49 +08:00
Soulter 4d2ff7cd5b fix: 修复 qq 回复别人的时候也会触发机器人, Onebot at 使用 string #330 2025-02-08 20:35:10 +08:00
Cvandia d8ec0e64d0 Merge remote-tracking branch 'upstream/master' 2025-02-08 19:40:56 +08:00
Cvandia 82e979cc07 feat: 添加 FishAudio TTS API 支持,更新配置和依赖项 2025-02-08 19:37:43 +08:00
Soulter 8c132a51f5 fix: 修复子指令设置permission之后会导致其一定会被执行 #427 2025-02-08 18:51:30 +08:00
Soulter 40bd372cc1 fix: 重启gewe的时候机器人会疯狂发消息 #421 2025-02-08 18:02:42 +08:00
Soulter 212e114270 perf: 优化了一些提示 2025-02-08 15:55:46 +08:00
Soulter b0e9de6951 perf: 增加DIFY超时时间 #422 2025-02-08 12:58:54 +08:00
Soulter 3489522bbb feat: 支持展示插件是否有更新 2025-02-08 12:22:36 +08:00
Soulter 96237abc03 fix: 当群聊自动回复时,不会带上人格的Prompt #419 2025-02-08 10:17:43 +08:00
32 changed files with 484 additions and 97 deletions
+3 -1
View File
@@ -12,7 +12,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN python -m pip install -r requirements.txt
RUN python -m pip install -r requirements.txt --no-cache-dir
RUN python -m pip install socksio wechatpy cryptography --no-cache-dir
EXPOSE 6185
EXPOSE 6186
+1 -2
View File
@@ -1,7 +1,6 @@
<p align="center">
![logo](https://github.com/user-attachments/assets/07649e07-3b8e-4feb-9aa9-bf13af4f3476)
![6e1279651f16d7fdf4727558b72bbaf1](https://github.com/user-attachments/assets/ead4c551-fc3c-48f7-a6f7-afbfdb820512)
</p>
+23 -5
View File
@@ -2,7 +2,7 @@
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
"""
VERSION = "3.4.22"
VERSION = "3.4.24"
DB_PATH = "data/data_v3.db"
# 默认配置
@@ -69,7 +69,9 @@ DEFAULT_CONFIG = {
"internal_keywords": {"enable": True, "extra_keywords": []},
"baidu_aip": {"enable": False, "app_id": "", "api_key": "", "secret_key": ""},
},
"admins_id": [],
"admins_id": [
"astrbot"
],
"t2i": False,
"t2i_word_threshold": 150,
"http_proxy": "",
@@ -276,7 +278,7 @@ CONFIG_METADATA_2 = {
"items": {"type": "string"},
"obvious_hint": True,
"hint": "此功能解决由于文件系统不一致导致路径不存在的问题。格式为 <原路径>:<映射路径>。如 `/app/.config/QQ:/var/lib/docker/volumes/xxxx/_data`。这样,当消息平台下发的事件中图片和语音路径以 `/app/.config/QQ` 开头时,开头被替换为 `/var/lib/docker/volumes/xxxx/_data`。这在 AstrBot 或者平台协议端使用 Docker 部署时特别有用。",
}
},
},
},
"content_safety": {
@@ -433,6 +435,7 @@ CONFIG_METADATA_2 = {
"dify_api_key": "",
"dify_api_base": "https://api.dify.ai/v1",
"dify_workflow_output_key": "",
"timeout": 60,
},
"whisper(API)": {
"id": "whisper",
@@ -459,6 +462,15 @@ CONFIG_METADATA_2 = {
"openai-tts-voice": "alloy",
"timeout": "20",
},
"fishaudio_tts(API)": {
"id": "fishaudio_tts",
"type": "fishaudio_tts_api",
"enable": False,
"api_key": "",
"api_base": "https://api.fish-audio.cn/v1",
"fishaudio-tts-character": "可莉",
"timeout": "20",
},
},
"items": {
"timeout": {
@@ -472,6 +484,12 @@ CONFIG_METADATA_2 = {
"obvious_hint": True,
"hint": "OpenAI TTS 的声音。OpenAI 默认支持:'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'",
},
"fishaudio-tts-character": {
"description": "character",
"type": "string",
"obvious_hint": True,
"hint": "fishaudio TTS 的角色。默认为可莉。更多角色请访问:https://fish.audio/zh-CN/discovery",
},
"whisper_hint": {
"description": "本地部署 Whisper 模型须知",
"type": "string",
@@ -725,7 +743,7 @@ CONFIG_METADATA_2 = {
},
"image_caption_prompt": {
"description": "图像转述提示词",
"type": "string"
"type": "string",
},
"active_reply": {
"description": "主动回复",
@@ -756,7 +774,7 @@ CONFIG_METADATA_2 = {
"hint": "提示词。当提示词为空时,如果触发回复,则向 LLM 请求的是触发的消息的内容;否则是提示词。此项可以和定时回复(暂未实现)配合使用。",
},
},
}
},
},
},
},
+6 -4
View File
@@ -325,11 +325,13 @@ class RedBag(BaseMessageComponent):
class Poke(BaseMessageComponent):
type: ComponentType = "Poke"
qq: int
type: str = ""
id: T.Optional[int] = 0
qq: T.Optional[int] = 0
def __init__(self, **_):
super().__init__(**_)
def __init__(self, type: str, **_):
type = f"Poke:{type}"
super().__init__(type=type, **_)
class Forward(BaseMessageComponent):
@@ -31,7 +31,7 @@ class StarRequestSubStage(Stage):
# 孤立无援的 star handler
continue
logger.debug(f"执行 Star Handler {handler.handler_full_name}")
logger.debug(f"执行插件 handler {handler.handler_full_name}")
wrapper = self._call_handler(self.ctx, event, handler.handler, **params)
async for ret in wrapper:
yield ret
@@ -18,7 +18,6 @@ class ResultDecorateStage:
self.reply_prefix = ctx.astrbot_config['platform_settings']['reply_prefix']
self.reply_with_mention = ctx.astrbot_config['platform_settings']['reply_with_mention']
self.reply_with_quote = ctx.astrbot_config['platform_settings']['reply_with_quote']
self.use_tts = ctx.astrbot_config['provider_tts_settings']['enable']
self.t2i_word_threshold = ctx.astrbot_config['t2i_word_threshold']
try:
self.t2i_word_threshold = int(self.t2i_word_threshold)
@@ -39,9 +38,8 @@ class ResultDecorateStage:
handlers = star_handlers_registry.get_handlers_by_event_type(EventType.OnDecoratingResultEvent)
for handler in handlers:
# TODO: 如何让这里的 handler 也能使用 LLM 能力。也许需要将 LLMRequestSubStage 提取出来。
await handler.handler(event)
if len(result.chain) > 0:
# 回复前缀
if self.reply_prefix:
@@ -69,7 +67,7 @@ class ResultDecorateStage:
result.chain = new_chain
# TTS
if self.use_tts and result.is_llm_result():
if self.ctx.astrbot_config['provider_tts_settings']['enable'] and result.is_llm_result():
tts_provider = self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst
new_chain = []
for comp in result.chain:
@@ -84,7 +82,7 @@ class ResultDecorateStage:
logger.error(f"由于 TTS 音频文件没找到,消息段转语音失败: {comp.text}")
new_chain.append(comp)
except BaseException:
traceback.print_exc()
logger.error(traceback.format_exc())
logger.error("TTS 失败,使用文本发送。")
new_chain.append(comp)
else:
+5 -1
View File
@@ -3,7 +3,7 @@ from ..context import PipelineContext
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, Reply
from astrbot.core.star.star_handler import star_handlers_registry, EventType
from astrbot.core.star.filter.command_group import CommandGroupFilter
from astrbot.core.star.filter.permission import PermissionTypeFilter
@@ -86,6 +86,10 @@ class WakingCheckStage(Stage):
if len(handler.event_filters) == 0:
# 不可能有这种情况, 也不允许有这种情况
continue
if 'sub_command' in handler.extras_configs:
# 如果是子指令
continue
for filter in handler.event_filters:
try:
+11 -7
View File
@@ -8,7 +8,7 @@ from typing import List, Union
from astrbot.core.message.components import Plain, Image, BaseMessageComponent, Face, At, AtAll, Forward
from astrbot.core.utils.metrics import Metric
from astrbot.core.provider.entites import ProviderRequest
from astrbot.core.db.po import Conversation
@dataclass
class MessageSesion:
@@ -305,9 +305,10 @@ class AstrMessageEvent(abc.ABC):
prompt: str,
func_tool_manager = None,
session_id: str = None,
image_urls: List[str] = None,
contexts: List = None,
system_prompt: str = ""
image_urls: List[str] = [],
contexts: List = [],
system_prompt: str = "",
conversation: Conversation = None
) -> ProviderRequest:
'''
创建一个 LLM 请求。
@@ -316,10 +317,12 @@ class AstrMessageEvent(abc.ABC):
```py
yield event.request_llm(prompt="hi")
```
prompt: 提示词
session_id: 已经过时,留空即可
image_urls: 可以是 base64:// 或者 http:// 开头的图片链接,也可以是本地图片路径。
contexts: 当指定 contexts 时,将会**只**使用 contexts 作为上下文。
contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。
func_tool_manager: 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。
conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。
'''
return ProviderRequest(
prompt = prompt,
@@ -327,5 +330,6 @@ class AstrMessageEvent(abc.ABC):
image_urls = image_urls,
func_tool = func_tool_manager,
contexts = contexts,
system_prompt = system_prompt
system_prompt = system_prompt,
conversation=conversation
)
@@ -1,9 +1,7 @@
import os
import random
import asyncio
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.message_components import Plain, Image, Record
from astrbot.api.message_components import Plain, Image, Record, At
from aiocqhttp import CQHttp
from astrbot.core.utils.io import file_to_base64, download_image_by_url
@@ -33,6 +31,10 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
d['data'] = {
'file': bs64_data,
}
if isinstance(segment, At):
d['data'] = {
'qq': str(segment.qq) # 转换为字符串
}
ret.append(d)
return ret
@@ -3,6 +3,7 @@ import asyncio
import aiohttp
import quart
import base64
import datetime
from astrbot.api.platform import AstrBotMessage, MessageMember, MessageType
from astrbot.api.message_components import Plain, Image, At, Record
@@ -67,6 +68,17 @@ class SimpleGewechatClient():
logger.critical("收到 gewechat 下线通知。")
return
if 'Data' in data and 'CreateTime' in data['Data']:
# 得到系统 UTF+8 的 ts
tz_offset = datetime.timedelta(hours=8)
tz = datetime.timezone(tz_offset)
ts = datetime.datetime.now(tz).timestamp()
create_time = data['Data']['CreateTime']
if create_time < ts - 30:
logger.warning(f"消息时间戳过旧: {create_time},当前时间戳: {ts}")
return
abm = AstrBotMessage()
d = data['Data']
@@ -143,7 +155,7 @@ class SimpleGewechatClient():
abm.message.append(Record(file=file_path, url=file_path))
case _:
logger.error(f"未实现的消息类型: {d['MsgType']}")
logger.info(f"未实现的消息类型: {d['MsgType']}")
return
logger.info(f"abm: {abm}")
+10 -8
View File
@@ -114,22 +114,24 @@ class ProviderManager():
try:
match provider_cfg['type']:
case "openai_chat_completion":
from .sources.openai_source import ProviderOpenAIOfficial # noqa: F401
from .sources.openai_source import ProviderOpenAIOfficial as ProviderOpenAIOfficial
case "zhipu_chat_completion":
from .sources.zhipu_source import ProviderZhipu # noqa: F401
from .sources.zhipu_source import ProviderZhipu as ProviderZhipu
case "llm_tuner":
logger.info("加载 LLM Tuner 工具 ...")
from .sources.llmtuner_source import LLMTunerModelLoader # noqa: F401
from .sources.llmtuner_source import LLMTunerModelLoader as LLMTunerModelLoader
case "dify":
from .sources.dify_source import ProviderDify # noqa: F401
from .sources.dify_source import ProviderDify as ProviderDify
case "googlegenai_chat_completion":
from .sources.gemini_source import ProviderGoogleGenAI # noqa: F401
from .sources.gemini_source import ProviderGoogleGenAI as ProviderGoogleGenAI
case "openai_whisper_api":
from .sources.whisper_api_source import ProviderOpenAIWhisperAPI # noqa: F401
from .sources.whisper_api_source import ProviderOpenAIWhisperAPI as ProviderOpenAIWhisperAPI
case "openai_whisper_selfhost":
from .sources.whisper_selfhosted_source import ProviderOpenAIWhisperSelfHost # noqa: F401
from .sources.whisper_selfhosted_source import ProviderOpenAIWhisperSelfHost as ProviderOpenAIWhisperSelfHost
case "openai_tts_api":
from .sources.openai_tts_api_source import ProviderOpenAITTSAPI # noqa: F401
from .sources.openai_tts_api_source import ProviderOpenAITTSAPI as ProviderOpenAITTSAPI
case "fishaudio_tts_api":
from .sources.fishaudio_tts_api_source import ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI
except (ImportError, ModuleNotFoundError) as e:
logger.critical(f"加载 {provider_cfg['type']}({provider_cfg['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。")
continue
+7 -3
View File
@@ -31,7 +31,9 @@ class ProviderDify(Provider):
raise Exception("Dify API 类型不能为空。")
self.model_name = "dify"
self.workflow_output_key = provider_config.get("dify_workflow_output_key", "astrbot_wf_output")
self.timeout = provider_config.get("timeout", 120)
if isinstance(self.timeout, str):
self.timeout = int(self.timeout)
self.conversation_ids = {}
@@ -78,7 +80,8 @@ class ProviderDify(Provider):
query=prompt,
user=session_id,
conversation_id=conversation_id,
files=files_payload
files=files_payload,
timeout=self.timeout
):
logger.debug(f"dify resp chunk: {chunk}")
if chunk['event'] == "message" or \
@@ -96,7 +99,8 @@ class ProviderDify(Provider):
**session_var
},
user=session_id,
files=files_payload
files=files_payload,
timeout=self.timeout
):
match chunk['event']:
case "workflow_started":
@@ -0,0 +1,105 @@
import uuid
import ormsgpack
from pydantic import BaseModel, conint
from httpx import AsyncClient
from typing import Annotated, Literal
from ..provider import TTSProvider
from ..entites import ProviderType
from ..register import register_provider_adapter
class ServeReferenceAudio(BaseModel):
audio: bytes
text: str
class ServeTTSRequest(BaseModel):
text: str
chunk_length: Annotated[int, conint(ge=100, le=300, strict=True)] = 200
# 音频格式
format: Literal["wav", "pcm", "mp3"] = "mp3"
mp3_bitrate: Literal[64, 128, 192] = 128
# 参考音频
references: list[ServeReferenceAudio] = []
# 参考模型 ID
# 例如 https://fish.audio/m/7f92f8afb8ec43bf81429cc1c9199cb1/
# 其中reference_id为 7f92f8afb8ec43bf81429cc1c9199cb1
reference_id: str | None = None
# 对中英文文本进行标准化,这可以提高数字的稳定性
normalize: bool = True
# 平衡模式将延迟减少到300毫秒,但可能会降低稳定性
latency: Literal["normal", "balanced"] = "normal"
@register_provider_adapter(
"fishaudio_tts_api", "FishAudio TTS API", provider_type=ProviderType.TEXT_TO_SPEECH
)
class ProviderFishAudioTTSAPI(TTSProvider):
def __init__(
self,
provider_config: dict,
provider_settings: dict,
) -> None:
super().__init__(provider_config, provider_settings)
self.chosen_api_key: str = provider_config.get("api_key", "")
self.character: str = provider_config.get("fishaudio-tts-character", "可莉")
self.api_base: str = provider_config.get(
"api_base", "https://api.fish-audio.cn/v1"
)
self.headers = {
"Authorization": f"Bearer {self.chosen_api_key}",
}
self.set_model(provider_config.get("model", None))
async def _get_reference_id_by_character(self, character: str) -> str:
"""
获取角色的reference_id
Args:
character: 角色名称
Returns:
reference_id: 角色的reference_id
exception:
APIException: 获取语音角色列表为空
"""
sort_options = ["score", "task_count", "created_at"]
async with AsyncClient(base_url=self.api_base.replace("/v1", "")) as client:
for sort_by in sort_options:
params = {"title": character, "sort_by": sort_by}
response = await client.get(
"/model", params=params, headers=self.headers
)
resp_data = response.json()
if resp_data["total"] == 0:
continue
for item in resp_data["items"]:
if character in item["title"]:
return item["_id"]
return None
async def _generate_request(self, text: str) -> dict:
return ServeTTSRequest(
text=text,
format="wav",
reference_id=await self._get_reference_id_by_character(self.character),
)
async def get_audio(self, text: str) -> str:
path = f"data/temp/fishaudio_tts_api_{uuid.uuid4()}.wav"
self.headers["content-type"] = "application/msgpack"
request = await self._generate_request(text)
async with AsyncClient(base_url=self.api_base).stream(
"POST",
"/tts",
headers=self.headers,
content=ormsgpack.packb(request, option=ormsgpack.OPT_SERIALIZE_PYDANTIC),
) as response:
if response.headers["content-type"] == "audio/wav":
with open(path, "wb") as f:
async for chunk in response.aiter_bytes():
f.write(chunk)
return path
text = await response.aread()
raise Exception(f"Fish Audio API请求失败: {text}")
+12 -2
View File
@@ -48,8 +48,18 @@ class SimpleGoogleGenAIClient():
logger.debug(f"payload: {payload}")
request_url = f"{self.api_base}/v1beta/models/{model}:generateContent?key={self.api_key}"
async with self.client.post(request_url, json=payload, timeout=self.timeout) as resp:
response = await resp.json()
return response
if "application/json" in resp.headers.get("Content-Type"):
try:
response = await resp.json()
except Exception as e:
text = await resp.text()
logger.error(f"Gemini 返回了非 json 数据: {text}")
raise e
return response
else:
text = await resp.text()
logger.error(f"Gemini 返回了非 json 数据: {text}")
raise Exception("Gemini 返回了非 json 数据: ")
@register_provider_adapter("googlegenai_chat_completion", "Google Gemini Chat Completion 提供商适配器")
@@ -105,13 +105,15 @@ class ProviderOpenAIOfficial(Provider):
logger.error(f"API 返回的 completion 无法解析:{completion}")
raise Exception(f"API 返回的 completion 无法解析:{completion}")
llm_response.raw_completion = completion
return llm_response
async def text_chat(
self,
prompt: str,
session_id: str=None,
image_urls: List[str]=None,
image_urls: List[str]=[],
func_tool: FuncCall=None,
contexts=[],
system_prompt=None,
@@ -173,7 +175,10 @@ class ProviderOpenAIOfficial(Provider):
or 'Function call is not supported' in str(e) \
or 'Function calling is not enabled' in str(e) \
or 'Tool calling is not supported' in str(e) \
or 'No endpoints found that support tool use' in str(e): # siliconcloud
or 'No endpoints found that support tool use' in str(e) \
or 'model does not support function calling' in str(e) \
or ('tool' in str(e) and 'support' in str(e).lower()) \
or ('function' in str(e) and 'support' in str(e).lower()):
logger.info(f"{self.get_model()} 不支持函数工具调用,已自动去除,不影响使用。")
if 'tools' in payloads:
del payloads['tools']
+22 -2
View File
@@ -1,8 +1,8 @@
from asyncio import Queue
from typing import List, TypedDict, Union
from typing import List, Union
from astrbot.core import sp
from astrbot.core.provider.provider import Provider
from astrbot.core.provider.provider import Provider, TTSProvider, STTProvider
from astrbot.core.db import BaseDatabase
from astrbot.core.config.astrbot_config import AstrBotConfig
from astrbot.core.provider.func_tool_manager import FuncCall
@@ -127,6 +127,14 @@ class Context:
'''获取所有用于文本生成任务的 LLM Provider(Chat_Completion 类型)。'''
return self.provider_manager.provider_insts
def get_all_tts_providers(self) -> List[TTSProvider]:
'''获取所有用于 TTS 任务的 Provider。'''
return self.provider_manager.tts_provider_insts
def get_all_stt_providers(self) -> List[STTProvider]:
'''获取所有用于 STT 任务的 Provider。'''
return self.provider_manager.stt_provider_insts
def get_using_provider(self) -> Provider:
'''
获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。
@@ -135,6 +143,18 @@ class Context:
'''
return self.provider_manager.curr_provider_inst
def get_using_tts_provider(self) -> TTSProvider:
'''
获取当前使用的用于 TTS 任务的 Provider。
'''
return self.provider_manager.curr_tts_provider_inst
def get_using_stt_provider(self) -> STTProvider:
'''
获取当前使用的用于 STT 任务的 Provider。
'''
return self.provider_manager.curr_stt_provider_inst
def get_config(self) -> AstrBotConfig:
'''获取 AstrBot 的配置。'''
return self._config
+1 -1
View File
@@ -43,7 +43,7 @@ class CommandFilter(HandlerFilter, ParameterValidationMixin):
return self.handler_md
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
if not event.is_wake_up():
if not event.is_at_or_wake_command:
return False
if event.get_extra("parsing_command"):
+1 -1
View File
@@ -37,7 +37,7 @@ class CommandGroupFilter(HandlerFilter):
return result
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> Tuple[bool, StarHandlerMetadata]:
if not event.is_wake_up():
if not event.is_at_or_wake_command:
return False, None
if event.get_extra("parsing_command"):
+4 -2
View File
@@ -68,12 +68,14 @@ def register_command(command_name: str = None, *args, **kwargs):
add_to_event_filters = True
def decorator(awaitable):
if not add_to_event_filters:
kwargs['sub_command'] = True # 打一个标记,表示这是一个子指令,再 wakingstage 阶段这个 handler 将会直接被跳过(其父指令会接管)
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent, **kwargs)
new_command.init_handler_md(handler_md)
if add_to_event_filters:
# 裸指令
handler_md.event_filters.append(new_command)
return awaitable
return decorator
@@ -116,7 +118,7 @@ class RegisteringCommandable():
def register_event_message_type(event_message_type: EventMessageType, **kwargs):
'''注册一个 EventMessageType'''
def decorator(awaitable):
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent, kwargs)
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent, **kwargs)
handler_md.event_filters.append(EventMessageTypeFilter(event_message_type))
return awaitable
+3 -2
View File
@@ -376,14 +376,14 @@ class PluginManager:
logger.debug(f"unbind handler {v.handler_name} from {plugin_name} (map)")
del star_handlers_registry.star_handlers_map[k]
async def update_plugin(self, plugin_name: str):
async def update_plugin(self, plugin_name: str, proxy = ""):
plugin = self.context.get_registered_star(plugin_name)
if not plugin:
raise Exception("插件不存在。")
if plugin.reserved:
raise Exception("该插件是 AstrBot 保留插件,无法更新。")
await self.updator.update(plugin)
await self.updator.update(plugin, proxy=proxy)
await self.reload()
async def turn_off_plugin(self, plugin_name: str):
@@ -428,6 +428,7 @@ class PluginManager:
async def install_plugin_from_file(self, zip_file_path: str):
dir_name = os.path.basename(zip_file_path).replace(".zip", "")
dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
desti_dir = os.path.join(self.plugin_store_path, dir_name)
self.updator.unzip_file(zip_file_path, desti_dir)
+5 -1
View File
@@ -23,12 +23,16 @@ class PluginUpdator(RepoZipUpdator):
return plugin_path
async def update(self, plugin: StarMetadata) -> str:
async def update(self, plugin: StarMetadata, proxy="") -> str:
repo_url = plugin.repo
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}")
+1 -1
View File
@@ -110,7 +110,7 @@ class RepoZipUpdator():
releases = await self.fetch_release_info(url=release_url)
if not releases:
# download from the default branch directly.
logger.info(f"未在仓库 {author}/{repo} 中找到任何发布版本,正在从默认分支下载。")
logger.info(f"正在从默认分支下载 {author}/{repo} ")
release_url = f"https://github.com/{author}/{repo}/archive/refs/heads/master.zip"
else:
release_url = releases[0]['zipball_url']
+13 -7
View File
@@ -1,6 +1,5 @@
import traceback
import aiohttp
import uuid
from .route import Route, Response, RouteContext
from astrbot.core import logger
from quart import request
@@ -49,7 +48,7 @@ class PluginRoute(Route):
return Response().error(message).__dict__
return Response().ok(None, "重载成功。").__dict__
except Exception as e:
logger.error(f"/api/extensions/reload: {traceback.format_exc()}")
logger.error(f"/api/plugin/reload: {traceback.format_exc()}")
return Response().error(str(e)).__dict__
async def get_online_plugins(self):
@@ -59,7 +58,6 @@ class PluginRoute(Route):
urls = [custom]
else:
urls = [
"https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json",
"https://api.soulter.top/astrbot/plugins"
]
@@ -88,6 +86,7 @@ class PluginRoute(Route):
"version": plugin.version,
"reserved": plugin.reserved,
"activated": plugin.activated,
"online_vesion": "",
"handlers": await self.get_plugin_handlers_info(plugin.star_handler_full_names),
}
_plugin_resp.append(_t)
@@ -143,6 +142,12 @@ class PluginRoute(Route):
async def install_plugin(self):
post_data = await request.json
repo_url = post_data["url"]
proxy: str = post_data.get("proxy", None)
if proxy:
proxy = proxy.removesuffix("/")
repo_url = f"{proxy}/{repo_url}"
try:
logger.info(f"正在安装插件 {repo_url}")
await self.plugin_manager.install_plugin(repo_url)
@@ -183,14 +188,15 @@ class PluginRoute(Route):
async def update_plugin(self):
post_data = await request.json
plugin_name = post_data["name"]
proxy: str = post_data.get("proxy", None)
try:
logger.info(f"正在更新插件 {plugin_name}")
await self.plugin_manager.update_plugin(plugin_name)
await self.plugin_manager.update_plugin(plugin_name, proxy)
self.core_lifecycle.restart()
logger.info(f"更新插件 {plugin_name} 成功。")
return Response().ok(None, "更新成功。").__dict__
except Exception as e:
logger.error(f"/api/extensions/update: {traceback.format_exc()}")
logger.error(f"/api/plugin/update: {traceback.format_exc()}")
return Response().error(str(e)).__dict__
async def off_plugin(self):
@@ -201,7 +207,7 @@ class PluginRoute(Route):
logger.info(f"停用插件 {plugin_name}")
return Response().ok(None, "停用成功。").__dict__
except Exception as e:
logger.error(f"/api/extensions/off: {traceback.format_exc()}")
logger.error(f"/api/plugin/off: {traceback.format_exc()}")
return Response().error(str(e)).__dict__
async def on_plugin(self):
@@ -212,5 +218,5 @@ class PluginRoute(Route):
logger.info(f"启用插件 {plugin_name}")
return Response().ok(None, "启用成功。").__dict__
except Exception as e:
logger.error(f"/api/extensions/on: {traceback.format_exc()}")
logger.error(f"/api/plugin/on: {traceback.format_exc()}")
return Response().error(str(e)).__dict__
+11
View File
@@ -0,0 +1,11 @@
# What's Changed
0. ✨ 新增: 支持 海豚 AIFishAudio TTS API #433 by @Cvandia
1. 🐛 修复: 当群聊主动回复时,不会带上人格的Prompt #419
2. ✨ 新增: 支持展示插件是否有更新
3. 👌 优化: 增加DIFY超时时间 #422
4. 🐛 修复: 自部署文转图不生效 #352
5. 🐛 修复: 修复 qq 回复别人的时候也会触发机器人, Onebot at 使用 string #330
6. 👌 优化: 增加DIFY超时时间 #422
7. 🐛 修复: 重启gewe的时候机器人会疯狂发消息 #421
8. 🐛 修复: 修复子指令设置permission之后会导致其一定会被执行 #427
+11
View File
@@ -0,0 +1,11 @@
# What's Changed
0. ✨ 新增: 支持正则表达式匹配触发机器人,机器人在某一段时间内持续唤醒(不用输唤醒词)。(安装 astrbot_plugin_wake_enhance 插件)
2. ✨ 新增: 可以通过 /tts 开关TTS,通过 /provider 更换 TTS #436
3. ✨ 新增: 管理面板支持设置 GitHub 反向代理地址以优化中国大陆地区下载 AstrBot 插件的速度。(在管理面板-设置页)
4. 🐛 修复: 修复指令不经过唤醒前缀也能生效的问题。在引用消息的时候无法使用前缀唤醒机器人 #444
5. 🐛 修复: 修复 Napcat 下戳一戳消息报错
6. 👌 优化: 从压缩包上传插件时,去除仓库 -branch 尾缀
7. 🐛 修复: gemini 报错时显示 apikey
8. 🐛 修复: drun 不支持函数调用的报错
9. 🐛 修复: raw_completion 没有正确传递导致部分插件无法正常运作 #439
@@ -2,7 +2,8 @@
const props = defineProps({
title: String,
link: String,
logo: String
logo: String,
has_update: Boolean,
});
const open = (link: string | undefined) => {
@@ -17,6 +18,7 @@ const open = (link: string | undefined) => {
<img v-if="logo" :src="logo" alt="logo" style="width: 40px; height: 40px; margin-right: 8px;">
<v-card-title style="font-size: 16px;">{{ props.title }}</v-card-title>
<v-spacer></v-spacer>
<v-icon color="success" v-if="has_update">mdi-arrow-up-bold</v-icon>
<v-btn size="small" text="Read" variant="flat" border @click="open(props.link)">帮助</v-btn>
</div>
</v-card-item>
@@ -16,12 +16,12 @@ export interface menu {
const sidebarItem: menu[] = [
{
title: '面板',
title: '统计',
icon: 'mdi-view-dashboard',
to: '/dashboard/default'
},
{
title: '配置',
title: '配置文件',
icon: 'mdi-cog',
to: '/config',
},
@@ -40,6 +40,11 @@ const sidebarItem: menu[] = [
icon: 'mdi-console',
to: '/console'
},
{
title: '设置',
icon: 'mdi-wrench',
to: '/settings'
},
{
title: '关于',
icon: 'mdi-information',
+5
View File
@@ -42,6 +42,11 @@ const MainRoutes = {
path: '/chat',
component: () => import('@/views/ChatPage.vue')
},
{
name: 'Settings',
path: '/settings',
component: () => import('@/views/Settings.vue')
},
{
name: 'About',
path: '/about',
+44 -11
View File
@@ -10,8 +10,8 @@ import { max } from 'date-fns';
<template>
<v-row>
<v-alert style="margin: 16px" text="1. 如果因为网络问题安装失败,可以自行前往仓库下载压缩包然后本地上传。2. 如需插件帮助请点击 `仓库` 查看 README" title="💡提示"
type="info" variant="tonal">
<v-alert style="margin: 16px" text="1. 如果因为网络问题安装失败,点击设置页选择 GitHub 加速地址。或前往仓库下载压缩包然后本地上传。" title="💡提示"
type="info" color="primary" variant="tonal">
</v-alert>
<v-col cols="12" md="12">
<div style="background-color: white; width: 100%; padding: 16px; border-radius: 10px;">
@@ -44,13 +44,22 @@ import { max } from 'date-fns';
</v-dialog>
</div>
</div>
</v-col>
</v-col>
<v-col cols="12" md="6" lg="3" v-for="extension in extension_data.data">
<ExtensionCard :key="extension.name" :title="extension.name" :link="extension.repo" :logo="extension?.logo"
style="margin-bottom: 4px;">
<div style="min-height: 135px; max-height: 135px; overflow: none;">
<span style="font-weight: bold;">By @{{ extension.author }}</span>
<span> | 插件有 {{ extension.handlers.length }} 个行为</span>
:has_update="extension.has_update" style="margin-bottom: 4px;">
<div style="min-height: 140px; max-height: 140px; overflow: auto;">
<div>
<span style="font-weight: bold ;">By @{{ extension.author }}</span>
<span> | 插件有 {{ extension.handlers.length }} 个行为</span>
</div>
<span> 当前: <v-chip size="small" color="primary">{{ extension.version }}</v-chip>
<span v-if="extension.online_version">
| 最新: <v-chip size="small" color="primary">{{ extension.online_version }}</v-chip>
</span>
<span v-if="extension.has_update" style="font-weight: bold;">有更新
</span>
</span>
<p style="margin-top: 8px;">{{ extension.desc }}</p>
<a style="font-size: 12px; cursor: pointer; text-decoration: underline; color: #555;"
@click="reloadPlugin(extension.name)">重载插件</a>
@@ -329,6 +338,7 @@ export default {
{ title: '作者', value: 'author' },
{ title: '操作', value: 'actions', sortable: false }
],
alreadyCheckUpdate: false
}
},
mounted() {
@@ -367,10 +377,29 @@ export default {
getExtensions() {
axios.get('/api/plugin/get').then((res) => {
this.extension_data = res.data;
this.checkAlreadyInstalled();
this.checkUpdate()
});
},
checkUpdate() {
// 遍历 extension_data 和 pluginMarketData,检查是否有更新\
for (let i = 0; i < this.extension_data.data.length; i++) {
for (let j = 0; j < this.pluginMarketData.length; j++) {
console.log(this.extension_data.data[i].repo, this.pluginMarketData[j].repo);
if (this.extension_data.data[i].repo === this.pluginMarketData[j].repo ||
this.extension_data.data[i].name === this.pluginMarketData[j].name) {
this.extension_data.data[i].online_version = this.pluginMarketData[j].version;
if (this.extension_data.data[i].version !== this.pluginMarketData[j].version && this.pluginMarketData[j].version !== "未知") {
this.extension_data.data[i].has_update = true;
} else {
this.extension_data.data[i].has_update = false;
}
}
}
}
},
newExtension() {
if (this.extension_url === "" && this.upload_file === null) {
this.toast("请填写插件链接或上传插件文件", "error");
@@ -411,7 +440,8 @@ export default {
this.toast("正在从链接 " + this.extension_url + " 安装插件...", "primary");
axios.post('/api/plugin/install',
{
url: this.extension_url
url: this.extension_url,
proxy: localStorage.getItem('selectedGitHubProxy') || ""
}).then((res) => {
this.loading_ = false;
if (res.data.status === "error") {
@@ -452,7 +482,8 @@ export default {
this.loadingDialog.show = true;
axios.post('/api/plugin/update',
{
name: extension_name
name: extension_name,
proxy: localStorage.getItem('selectedGitHubProxy') || ""
}).then((res) => {
if (res.data.status === "error") {
this.onLoadingDialogResult(2, res.data.message, -1);
@@ -529,11 +560,13 @@ export default {
"desc": res.data.data[key].desc,
"author": res.data.data[key].author,
"repo": res.data.data[key].repo,
"installed": false
"installed": false,
"version": res.data.data[key]?.version ? res.data.data[key].version : "未知",
})
}
this.pluginMarketData = data;
this.checkAlreadyInstalled();
this.checkUpdate();
}).catch((err) => {
this.toast("获取插件市场数据失败: " + err, "error");
});
+52
View File
@@ -0,0 +1,52 @@
<template>
<div style="background-color: white; padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px;">
<v-list lines="two">
<v-list-subheader>网络</v-list-subheader>
<v-list-item subtitle="设置下载插件时所用的 GitHub 加速地址。这在中国大陆的网络环境有效。可以自定义,输入结果实时生效" title="GitHub 加速地址">
<v-combobox variant="outlined" style="width: 100%; margin-top: 16px;" v-model="selectedGitHubProxy" :items="githubProxies"
label="选择 GitHub 加速地址">
</v-combobox>
</v-list-item>
</v-list>
</div>
</template>
<script>
export default {
data() {
return {
githubProxies: [
"https://ghproxy.cn",
"https://gh.llkk.cc",
"https://ghproxy.net",
"https://gitproxy.click",
"https://github.tbedu.top"
],
selectedGitHubProxy: "",
}
},
methods: {
},
mounted() {
this.selectedGitHubProxy = localStorage.getItem('selectedGitHubProxy') || "";
},
watch: {
selectedGitHubProxy: function (newVal, oldVal) {
if (!newVal) {
newVal = ""
}
localStorage.setItem('selectedGitHubProxy', newVal);
}
}
}
</script>
+87 -20
View File
@@ -6,6 +6,7 @@ import astrbot.api.star as star
import astrbot.api.event.filter as filter
from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.api import sp
from astrbot.api.platform import MessageType
from astrbot.api.provider import Personality, ProviderRequest, LLMResponse
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
from astrbot.core.star.star_handler import star_handlers_registry, StarHandlerMetadata
@@ -59,6 +60,7 @@ AstrBot 指令:
[System]
/plugin: 查看插件、插件帮助
/t2i: 开关文本转图片
/tts: 开关文本转语音
/sid: 获取会话 ID
/op <admin_id>: 授权管理员(op)
/deop <admin_id>: 取消管理员(op)
@@ -83,10 +85,7 @@ AstrBot 指令:
/websearch: 网页搜索
[其他]
/set <变量名> <值>: 为会话定义变量。适用于 Dify 工作流输入
/unset <变量名>: 删除会话的变量。
提示:如要查看插件指令,请输入 /plugin 查看具体信息。
/set 变量名: 为会话定义变量(Dify 工作流输入)
{notice}"""
event.set_result(MessageEventResult().message(msg).use_t2i(False))
@@ -125,7 +124,7 @@ AstrBot 指令:
tm = self.context.get_llm_tool_manager()
for tool in tm.func_list:
self.context.deactivate_llm_tool(tool.name)
event.set_result(MessageEventResult().message(f"停用所有工具成功。"))
event.set_result(MessageEventResult().message("停用所有工具成功。"))
@filter.command("plugin")
async def plugin(self, event: AstrMessageEvent, oper1: str = None, oper2: str = None):
@@ -200,6 +199,18 @@ AstrBot 指令:
config.save_config()
event.set_result(MessageEventResult().message("已开启文本转图片模式。"))
@filter.command("tts")
async def tts(self, event: AstrMessageEvent):
config = self.context.get_config()
if config['provider_tts_settings']['enable']:
config['provider_tts_settings']['enable'] = False
config.save_config()
event.set_result(MessageEventResult().message("已关闭文本转语音。"))
return
config['provider_tts_settings']['enable'] = True
config.save_config()
event.set_result(MessageEventResult().message("已开启文本转语音。"))
@filter.command("sid")
async def sid(self, event: AstrMessageEvent):
sid = event.unified_msg_origin
@@ -245,34 +256,89 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
@filter.command("provider")
async def provider(self, event: AstrMessageEvent, idx: int = None):
async def provider(self, event: AstrMessageEvent, idx: Union[str, int] = None, idx2: int = None):
'''查看或者切换 LLM Provider'''
if not self.context.get_using_provider():
event.set_result(MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"))
return
if idx is None:
ret = "## 当前载入的 LLM 提供商\n"
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})"
if self.context.get_using_provider().meta().id == id_:
ret += " (当前使用)"
ret += "\n"
tts_providers = self.context.get_all_tts_providers()
if tts_providers:
ret += "\n## 载入的 TTS 提供商\n"
for idx, tts in enumerate(tts_providers):
id_ = tts.meta().id
ret += f"{idx + 1}. {id_}"
tts_using = self.context.get_using_tts_provider()
if tts_using and tts_using.meta().id == id_:
ret += " (当前使用)"
ret += "\n"
stt_providers = self.context.get_all_stt_providers()
if stt_providers:
ret += "\n## 载入的 STT 提供商\n"
for idx, stt in enumerate(stt_providers):
id_ = stt.meta().id
ret += f"{idx + 1}. {id_}"
stt_using = self.context.get_using_stt_provider()
if stt_using and stt_using.meta().id == id_:
ret += " (当前使用)"
ret += "\n"
ret += "\n使用 /provider <序号> 切换提供商。"
ret += "\n使用 /provider <序号> 切换 LLM 提供商。"
if tts_providers:
ret += "\n使用 /provider tts <序号> 切换 TTS 提供商。"
if stt_providers:
ret += "\n使用 /provider stt <切换> STT 提供商。"
event.set_result(MessageEventResult().message(ret))
else:
if idx > len(self.context.get_all_providers()) or idx < 1:
event.set_result(MessageEventResult().message("无效的序号。"))
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_)
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_}"))
event.set_result(MessageEventResult().message(f"成功切换到 {id_}"))
else:
event.set_result(MessageEventResult().message("无效的参数。"))
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("reset")
@@ -581,7 +647,7 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
sp.put("session_variables", session_vars)
yield event.plain_result(f"会话 {session_id} 变量 {key} 存储成功。")
yield event.plain_result(f"会话 {session_id} 变量 {key} 存储成功。使用 /unset 移除。")
@filter.command("unset")
async def unset_variable(self, event: AstrMessageEvent, key: str):
@@ -591,7 +657,7 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
session_var = session_vars.get(session_id, {})
if key not in session_var:
yield event.plain_result("没有那个变量名。")
yield event.plain_result("没有那个变量名。格式 /unset 变量名。")
else:
del session_var[key]
sp.put("session_variables", session_vars)
@@ -632,7 +698,7 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
session_curr_cid = await self.context.conversation_manager.get_curr_conversation_id(event.unified_msg_origin)
if not session_curr_cid:
logger.error("当前未处于对话状态,无法主动回复,请使用 /switch 切换或者 /new 创建。")
logger.error("当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 切换或者 /new 创建一个会话")
return
conv = await self.context.conversation_manager.get_conversation(
@@ -649,7 +715,8 @@ UID: {user_id} 此 ID 可用于设置管理员。/op <UID> 授权管理员, /deo
prompt=prompt,
func_tool_manager=self.context.get_llm_tool_manager(),
session_id=event.session_id,
contexts=history if history else []
contexts=history if history else [],
conversation=conv,
)
except BaseException as e:
logger.error(f"主动回复失败: {e}")
+2 -1
View File
@@ -16,4 +16,5 @@ pyjwt
apscheduler
docstring_parser
aiodocker
silk-python
silk-python
ormsgpack