Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2649d46d8d | |||
| e23ffe6f02 | |||
| 96f3c3729a | |||
| 11e9d47ce2 | |||
| efbc8e4383 | |||
| bc7404409f | |||
| 8677d70baf | |||
| f39253f0e1 | |||
| 68c1957267 | |||
| a275aa2e4d | |||
| cadbac9948 | |||
| 82673e8ddd | |||
| bee51024b3 | |||
| 3437cb73ec | |||
| d01d1a8520 | |||
| 5aa842cf66 | |||
| 03282dee0f | |||
| 98e8ecb8e2 | |||
| 9451dc3fd4 | |||
| e1d3759f55 | |||
| 0ec382c86b | |||
| 756087c9f1 | |||
| 3e7c47e873 | |||
| e3ffdbc308 | |||
| 645cace4d6 | |||
| 0959d5986b | |||
| 89605c29a7 | |||
| e527f31213 | |||
| a0dbd99928 | |||
| 17d39c7a4a | |||
| 54edaebbd9 | |||
| d587a6f64c | |||
| 2371c32be5 | |||
| c9abb8352c |
+1
-2
@@ -25,5 +25,4 @@ package.json
|
||||
venv/*
|
||||
packages/python_interpreter/workplace
|
||||
.venv/*
|
||||
|
||||
.conda/
|
||||
.conda/
|
||||
|
||||
@@ -629,8 +629,8 @@ to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
AstrBot is a llm-powered chatbot and develop framework.
|
||||
Copyright (C) 2022-2099 Soulter
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
|
||||
"""
|
||||
|
||||
VERSION = "3.4.31"
|
||||
VERSION = "3.4.32"
|
||||
DB_PATH = "data/data_v3.db"
|
||||
|
||||
# 默认配置
|
||||
@@ -16,7 +16,7 @@ DEFAULT_CONFIG = {
|
||||
"strategy": "stall", # stall, discard
|
||||
},
|
||||
"reply_prefix": "",
|
||||
"forward_threshold": 200,
|
||||
"forward_threshold": 1500,
|
||||
"enable_id_white_list": True,
|
||||
"id_whitelist": [],
|
||||
"id_whitelist_log": True,
|
||||
@@ -67,17 +67,15 @@ DEFAULT_CONFIG = {
|
||||
"method": "possibility_reply",
|
||||
"possibility_reply": 0.1,
|
||||
"prompt": "",
|
||||
"whitelist": []
|
||||
}
|
||||
"whitelist": [],
|
||||
},
|
||||
},
|
||||
"content_safety": {
|
||||
"also_use_in_response": False,
|
||||
"internal_keywords": {"enable": True, "extra_keywords": []},
|
||||
"baidu_aip": {"enable": False, "app_id": "", "api_key": "", "secret_key": ""},
|
||||
},
|
||||
"admins_id": [
|
||||
"astrbot"
|
||||
],
|
||||
"admins_id": ["astrbot"],
|
||||
"t2i": False,
|
||||
"t2i_word_threshold": 150,
|
||||
"http_proxy": "",
|
||||
@@ -85,7 +83,7 @@ DEFAULT_CONFIG = {
|
||||
"enable": True,
|
||||
"username": "astrbot",
|
||||
"password": "77b90590a8945a7d36c963981a307dc9",
|
||||
"port": 6185
|
||||
"port": 6185,
|
||||
},
|
||||
"platform": [],
|
||||
"wake_prefix": ["/"],
|
||||
@@ -122,9 +120,9 @@ CONFIG_METADATA_2 = {
|
||||
"enable": False,
|
||||
"appid": "",
|
||||
"secret": "",
|
||||
"port": 6196
|
||||
"port": 6196,
|
||||
},
|
||||
"aiocqhtp(QQ)": {
|
||||
"aiocqhttp(OneBotv11)": {
|
||||
"id": "default",
|
||||
"type": "aiocqhttp",
|
||||
"enable": False,
|
||||
@@ -140,6 +138,14 @@ CONFIG_METADATA_2 = {
|
||||
"host": "这里填写你的局域网IP或者公网服务器IP",
|
||||
"port": 11451,
|
||||
},
|
||||
"wecom(企业微信)": {
|
||||
"corpid": "",
|
||||
"secret": "",
|
||||
"port": 6195,
|
||||
"token": "",
|
||||
"encoding_aes_key": "",
|
||||
"api_base_url": "https://qyapi.weixin.qq.com/cgi-bin/",
|
||||
},
|
||||
"lark(飞书)": {
|
||||
"id": "lark",
|
||||
"type": "lark",
|
||||
@@ -147,10 +153,23 @@ CONFIG_METADATA_2 = {
|
||||
"lark_bot_name": "",
|
||||
"app_id": "",
|
||||
"app_secret": "",
|
||||
"domain": "https://open.feishu.cn"
|
||||
"domain": "https://open.feishu.cn",
|
||||
},
|
||||
"telegram": {
|
||||
"id": "telegram",
|
||||
"type": "telegram",
|
||||
"enable": False,
|
||||
"telegram_token": "your_bot_token",
|
||||
"start_message": "Hello, I'm AstrBot!",
|
||||
"telegram_api_base_url": "https://api.telegram.org/bot",
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
"telegram_token": {
|
||||
"description": "Bot Token",
|
||||
"type": "string",
|
||||
"hint": "如果你的网络环境为中国大陆,请在 `其他配置` 处设置代理或更改 api_base。",
|
||||
},
|
||||
"id": {
|
||||
"description": "ID",
|
||||
"type": "string",
|
||||
@@ -201,8 +220,8 @@ CONFIG_METADATA_2 = {
|
||||
"description": "飞书机器人的名字",
|
||||
"type": "string",
|
||||
"hint": "请务必填对,否则 @ 机器人将无法唤醒,只能通过前缀唤醒。",
|
||||
"obvious_hint": True
|
||||
}
|
||||
"obvious_hint": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"platform_settings": {
|
||||
@@ -250,7 +269,7 @@ CONFIG_METADATA_2 = {
|
||||
"description": "间隔时间计算方法",
|
||||
"type": "string",
|
||||
"options": ["random", "log"],
|
||||
"hint": "分段回复的间隔时间计算方法。random 为随机时间,log 为根据消息长度计算,$y=log_{log\_base}(x)$,x为字数,y的单位为秒。",
|
||||
"hint": "分段回复的间隔时间计算方法。random 为随机时间,log 为根据消息长度计算,$y=log_<log_base>(x)$,x为字数,y的单位为秒。",
|
||||
},
|
||||
"interval": {
|
||||
"description": "随机间隔时间(秒)",
|
||||
@@ -387,7 +406,7 @@ CONFIG_METADATA_2 = {
|
||||
"description": "服务提供商配置",
|
||||
"type": "list",
|
||||
"config_template": {
|
||||
"openai": {
|
||||
"OpenAI": {
|
||||
"id": "openai",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -398,7 +417,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "gpt-4o-mini",
|
||||
},
|
||||
},
|
||||
"azure_openai": {
|
||||
"Azure_OpenAI": {
|
||||
"id": "azure",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -410,7 +429,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "gpt-4o-mini",
|
||||
},
|
||||
},
|
||||
"xAI": {
|
||||
"xAI(grok)": {
|
||||
"id": "xai",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -421,7 +440,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "grok-2-latest",
|
||||
},
|
||||
},
|
||||
"anthropic(claude)": {
|
||||
"Anthropic(claude)": {
|
||||
"id": "claude",
|
||||
"type": "anthropic_chat_completion",
|
||||
"enable": True,
|
||||
@@ -433,7 +452,7 @@ CONFIG_METADATA_2 = {
|
||||
"max_tokens": 4096,
|
||||
},
|
||||
},
|
||||
"ollama": {
|
||||
"Ollama": {
|
||||
"id": "ollama_default",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -443,7 +462,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "llama3.1-8b",
|
||||
},
|
||||
},
|
||||
"gemini(OpenAI兼容)": {
|
||||
"Gemini(OpenAI兼容)": {
|
||||
"id": "gemini_default",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -454,7 +473,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "gemini-1.5-flash",
|
||||
},
|
||||
},
|
||||
"gemini(googlegenai原生)": {
|
||||
"Gemini(googlegenai原生)": {
|
||||
"id": "gemini_default",
|
||||
"type": "googlegenai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -465,7 +484,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "gemini-1.5-flash",
|
||||
},
|
||||
},
|
||||
"deepseek": {
|
||||
"DeepSeek": {
|
||||
"id": "deepseek_default",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -476,7 +495,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "deepseek-chat",
|
||||
},
|
||||
},
|
||||
"zhipu": {
|
||||
"Zhipu(智谱)": {
|
||||
"id": "zhipu_default",
|
||||
"type": "zhipu_chat_completion",
|
||||
"enable": True,
|
||||
@@ -487,7 +506,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "glm-4-flash",
|
||||
},
|
||||
},
|
||||
"siliconflow": {
|
||||
"SiliconFlow(硅基流动)": {
|
||||
"id": "siliconflow",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -498,7 +517,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "deepseek-ai/DeepSeek-V3",
|
||||
},
|
||||
},
|
||||
"moonshot(kimi)": {
|
||||
"MoonShot(Kimi)": {
|
||||
"id": "moonshot",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -509,7 +528,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "moonshot-v1-8k",
|
||||
},
|
||||
},
|
||||
"llmtuner": {
|
||||
"LLMTuner": {
|
||||
"id": "llmtuner_default",
|
||||
"type": "llm_tuner",
|
||||
"enable": True,
|
||||
@@ -519,7 +538,7 @@ CONFIG_METADATA_2 = {
|
||||
"finetuning_type": "lora",
|
||||
"quantization_bit": 4,
|
||||
},
|
||||
"dify": {
|
||||
"Dify": {
|
||||
"id": "dify_app_default",
|
||||
"type": "dify",
|
||||
"enable": True,
|
||||
@@ -531,7 +550,7 @@ CONFIG_METADATA_2 = {
|
||||
"variables": {},
|
||||
"timeout": 60,
|
||||
},
|
||||
"dashscope": {
|
||||
"Dashscope(阿里云百炼应用)": {
|
||||
"id": "dashscope",
|
||||
"type": "dashscope",
|
||||
"enable": True,
|
||||
@@ -541,7 +560,7 @@ CONFIG_METADATA_2 = {
|
||||
"variables": {},
|
||||
"timeout": 60,
|
||||
},
|
||||
"fastgpt": {
|
||||
"FastGPT": {
|
||||
"id": "fastgpt",
|
||||
"type": "openai_chat_completion",
|
||||
"enable": True,
|
||||
@@ -549,7 +568,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.fastgpt.in/api/v1",
|
||||
"timeout": 60,
|
||||
},
|
||||
"whisper(API)": {
|
||||
"Whisper(API)": {
|
||||
"id": "whisper",
|
||||
"type": "openai_whisper_api",
|
||||
"enable": False,
|
||||
@@ -557,14 +576,22 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "",
|
||||
"model": "whisper-1",
|
||||
},
|
||||
"whisper(本地加载)": {
|
||||
"Whisper(本地加载)": {
|
||||
"whisper_hint": "(不用修改我)",
|
||||
"enable": False,
|
||||
"id": "whisper",
|
||||
"type": "openai_whisper_selfhost",
|
||||
"model": "tiny",
|
||||
},
|
||||
"openai_tts(API)": {
|
||||
"sensevoice(本地加载)": {
|
||||
"sensevoice_hint": "(不用修改我)",
|
||||
"enable": False,
|
||||
"id": "sensevoice",
|
||||
"type": "sensevoice_stt_selfhost",
|
||||
"stt_model": "icc/SenseVoiceSmall",
|
||||
"is_emotion": False,
|
||||
},
|
||||
"OpenAI_TTS(API)": {
|
||||
"id": "openai_tts",
|
||||
"type": "openai_tts_api",
|
||||
"enable": False,
|
||||
@@ -574,7 +601,7 @@ CONFIG_METADATA_2 = {
|
||||
"openai-tts-voice": "alloy",
|
||||
"timeout": "20",
|
||||
},
|
||||
"fishaudio_tts(API)": {
|
||||
"FishAudio_TTS(API)": {
|
||||
"id": "fishaudio_tts",
|
||||
"type": "fishaudio_tts_api",
|
||||
"enable": False,
|
||||
@@ -585,6 +612,22 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
"sensevoice_hint": {
|
||||
"description": "部署SenseVoice",
|
||||
"type": "string",
|
||||
"hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"is_emotion": {
|
||||
"description": "情绪识别",
|
||||
"type": "bool",
|
||||
"hint": "是否开启情绪识别。happy|sad|angry|neutral|fearful|disgusted|surprised|unknown",
|
||||
},
|
||||
"stt_model": {
|
||||
"description": "模型名称",
|
||||
"type": "string",
|
||||
"hint": "modelscope 上的模型名称。默认:iic/SenseVoiceSmall。",
|
||||
},
|
||||
# "variables": {
|
||||
# "description": "工作流固定输入变量",
|
||||
# "type": "object",
|
||||
@@ -602,7 +645,12 @@ CONFIG_METADATA_2 = {
|
||||
"description": "应用类型",
|
||||
"type": "string",
|
||||
"hint": "阿里云百炼应用的应用类型。",
|
||||
"options": ["agent", "agent-arrange", "dialog-workflow", "task-workflow"],
|
||||
"options": [
|
||||
"agent",
|
||||
"agent-arrange",
|
||||
"dialog-workflow",
|
||||
"task-workflow",
|
||||
],
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"timeout": {
|
||||
@@ -721,10 +769,10 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"dify_query_input_key": {
|
||||
"description": "Prompt 输入变量名",
|
||||
"type": "string",
|
||||
"type": "string",
|
||||
"hint": "发送的消息文本内容对应的输入变量名。默认为 astrbot_text_query。",
|
||||
"obvious": True,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"provider_settings": {
|
||||
|
||||
@@ -27,7 +27,7 @@ class AstrBotCoreLifecycle:
|
||||
|
||||
os.environ['https_proxy'] = self.astrbot_config['http_proxy']
|
||||
os.environ['http_proxy'] = self.astrbot_config['http_proxy']
|
||||
os.environ['no_proxy'] = 'localhost,127.0.0.1'
|
||||
os.environ['no_proxy'] = 'localhost'
|
||||
|
||||
async def initialize(self):
|
||||
logger.info("AstrBot v"+ VERSION)
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ from typing import List
|
||||
|
||||
CACHED_SIZE = 200
|
||||
log_color_config = {
|
||||
'DEBUG': 'bold_blue', 'INFO': 'bold_cyan',
|
||||
'DEBUG': 'green', 'INFO': 'bold_cyan',
|
||||
'WARNING': 'bold_yellow', 'ERROR': 'red',
|
||||
'CRITICAL': 'bold_red', 'RESET': 'reset',
|
||||
'asctime': 'green'
|
||||
|
||||
@@ -142,6 +142,9 @@ class LLMRequestSubStage(Stage):
|
||||
return
|
||||
|
||||
async def _save_to_history(self, event: AstrMessageEvent, req: ProviderRequest, llm_response: LLMResponse):
|
||||
if not req or not req.conversation or not llm_response or not req.contexts:
|
||||
return
|
||||
|
||||
if llm_response.role == "assistant":
|
||||
# 文本回复
|
||||
contexts = req.contexts
|
||||
|
||||
@@ -49,6 +49,8 @@ class PlatformManager():
|
||||
from .sources.gewechat.gewechat_platform_adapter import GewechatPlatformAdapter # noqa: F401
|
||||
case "lark":
|
||||
from .sources.lark.lark_adapter import LarkPlatformAdapter # noqa: F401
|
||||
case "telegram":
|
||||
from .sources.telegram.tg_adapter import TelegramPlatformAdapter # noqa: F401
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.error(f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。")
|
||||
except Exception as e:
|
||||
@@ -56,7 +58,7 @@ class PlatformManager():
|
||||
|
||||
|
||||
if platform_config['type'] not in platform_cls_map:
|
||||
logger.error(f"未找到适用于 {platform_config['type']}({platform_config['id']}) 平台适配器,请检查是否已经安装或者名称填写错误。")
|
||||
logger.error(f"未找到适用于 {platform_config['type']}({platform_config['id']}) 平台适配器,请检查是否已经安装或者名称填写错误")
|
||||
return
|
||||
cls_type = platform_cls_map[platform_config['type']]
|
||||
inst = cls_type(platform_config, self.settings, self.event_queue)
|
||||
|
||||
@@ -231,7 +231,7 @@ class AiocqhttpAdapter(Platform):
|
||||
|
||||
@self.bot.on_websocket_connection
|
||||
def on_websocket_connection(_):
|
||||
logger.info("aiocqhttp 适配器已连接。")
|
||||
logger.info("aiocqhttp(OneBot v11) 适配器已连接。")
|
||||
|
||||
bot = self.bot.run_task(host=self.host, port=int(self.port), shutdown_trigger=self.shutdown_trigger_placeholder)
|
||||
|
||||
|
||||
@@ -305,12 +305,11 @@ class SimpleGewechatClient():
|
||||
"uuid": qr_uuid,
|
||||
"appId": appid
|
||||
})
|
||||
verify_flag = False
|
||||
while retry_cnt > 0:
|
||||
retry_cnt -= 1
|
||||
|
||||
# 需要验证码
|
||||
if verify_flag or os.path.exists("data/temp/gewe_code"):
|
||||
if os.path.exists("data/temp/gewe_code"):
|
||||
with open("data/temp/gewe_code", "r") as f:
|
||||
code = f.read().strip()
|
||||
if not code:
|
||||
@@ -339,7 +338,6 @@ class SimpleGewechatClient():
|
||||
msg = json_blob['data']['msg']
|
||||
if ret == 500 and '安全验证码' in msg:
|
||||
logger.warning("此次登录需要安全验证码,请在管理面板聊天页输入 /gewe_code 验证码 来验证,如 /gewe_code 123456")
|
||||
verify_flag = True
|
||||
else:
|
||||
status = json_blob['data']['status']
|
||||
nickname = json_blob['data'].get('nickName', '')
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import sys
|
||||
import uuid
|
||||
import asyncio
|
||||
|
||||
from astrbot.api.platform import Platform, AstrBotMessage, MessageMember, PlatformMetadata, MessageType
|
||||
from astrbot.api.event import MessageChain
|
||||
from astrbot.api.message_components import Plain, Image, Record, File as AstrBotFile, Video, At
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.api.platform import register_platform_adapter
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import ApplicationBuilder, ContextTypes, filters
|
||||
from telegram.constants import ChatType
|
||||
from telegram.ext import MessageHandler as TelegramMessageHandler
|
||||
from .tg_event import TelegramPlatformEvent
|
||||
from astrbot.api import logger
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
@register_platform_adapter("telegram", "telegram 适配器")
|
||||
class TelegramPlatformAdapter(Platform):
|
||||
|
||||
def __init__(self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config
|
||||
self.settings = platform_settings
|
||||
self.client_self_id = uuid.uuid4().hex[:8]
|
||||
|
||||
@override
|
||||
async def send_by_session(self, session: MessageSesion, message_chain: MessageChain):
|
||||
from_username = session.session_id
|
||||
await TelegramPlatformEvent.send_with_client(self.client, message_chain, from_username)
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
@override
|
||||
def meta(self) -> PlatformMetadata:
|
||||
return PlatformMetadata(
|
||||
"telegram",
|
||||
"telegram 适配器",
|
||||
)
|
||||
|
||||
@override
|
||||
async def run(self):
|
||||
base_url = self.config.get("telegram_api_base_url", "https://api.telegram.org/bot")
|
||||
if not base_url:
|
||||
base_url = "https://api.telegram.org/bot"
|
||||
|
||||
self.application = ApplicationBuilder().token(self.config['telegram_token']).base_url(base_url).build()
|
||||
message_handler = TelegramMessageHandler(
|
||||
filters=filters.ALL, # receive all messages
|
||||
callback=self.convert_message
|
||||
)
|
||||
self.application.add_handler(message_handler)
|
||||
await self.application.initialize()
|
||||
await self.application.start()
|
||||
queue = self.application.updater.start_polling()
|
||||
self.client = self.application.bot
|
||||
logger.info("Telegram Platform Adapter is running.")
|
||||
|
||||
await queue
|
||||
|
||||
async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await context.bot.send_message(chat_id=update.effective_chat.id, text=self.config["start_message"])
|
||||
|
||||
async def convert_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> AstrBotMessage:
|
||||
message = AstrBotMessage()
|
||||
# 获得是群聊还是私聊
|
||||
if update.effective_chat.type == ChatType.PRIVATE:
|
||||
message.type = MessageType.FRIEND_MESSAGE
|
||||
else:
|
||||
message.type = MessageType.GROUP_MESSAGE
|
||||
message.group_id = update.effective_chat.id
|
||||
message.message_id = str(update.message.message_id)
|
||||
message.session_id = str(update.effective_chat.id)
|
||||
message.sender = MessageMember(str(update.effective_user.id), update.effective_user.username)
|
||||
message.self_id = str(context.bot.id)
|
||||
message.raw_message = update
|
||||
message.message_str = ""
|
||||
message.message = []
|
||||
|
||||
logger.debug(f"Telegram message: {update.message}")
|
||||
|
||||
if update.message.text:
|
||||
plain_text = update.message.text
|
||||
|
||||
if update.message.entities:
|
||||
for entity in update.message.entities:
|
||||
if entity.type == "mention":
|
||||
name = plain_text[entity.offset:entity.offset+entity.length]
|
||||
message.message.append(At(qq=message.self_id, name=name))
|
||||
plain_text = plain_text[:entity.offset] + plain_text[entity.offset+entity.length:]
|
||||
|
||||
message.message.append(Plain(plain_text))
|
||||
message.message_str = plain_text
|
||||
|
||||
|
||||
elif update.message.voice:
|
||||
file = await update.message.voice.get_file()
|
||||
message.message = [Record(file=file.file_path, url=file.file_path),]
|
||||
|
||||
elif update.message.photo:
|
||||
for photo in update.message.photo:
|
||||
file = await photo.get_file()
|
||||
message.message.append(Image(file=file.file_path, url=file.file_path))
|
||||
|
||||
elif update.message.document:
|
||||
file = await update.message.document.get_file()
|
||||
message.message = [AstrBotFile(file=file.file_path, name="file"),]
|
||||
|
||||
elif update.message.video:
|
||||
file = await update.message.video.get_file()
|
||||
message.message = [Video(file=file.file_path, path=file.file_path),]
|
||||
|
||||
|
||||
await self.handle_msg(message)
|
||||
|
||||
async def handle_msg(self, message: AstrBotMessage):
|
||||
message_event = TelegramPlatformEvent(
|
||||
message_str=message.message_str,
|
||||
message_obj=message,
|
||||
platform_meta=self.meta(),
|
||||
session_id=message.session_id,
|
||||
client=self.client
|
||||
)
|
||||
self.commit_event(message_event)
|
||||
@@ -0,0 +1,57 @@
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata, MessageType
|
||||
from astrbot.api.message_components import Plain, Image, Reply, At
|
||||
from telegram.ext import ExtBot
|
||||
|
||||
class TelegramPlatformEvent(AstrMessageEvent):
|
||||
def __init__(self, message_str: str, message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, client: ExtBot):
|
||||
super().__init__(message_str, message_obj, platform_meta, session_id)
|
||||
self.client = client
|
||||
|
||||
@staticmethod
|
||||
async def send_with_client(client: ExtBot, message: MessageChain, user_name: str):
|
||||
image_path = None
|
||||
|
||||
has_reply = False
|
||||
reply_message_id = None
|
||||
at_user_id = None
|
||||
for i in message.chain:
|
||||
if isinstance(i, Reply):
|
||||
has_reply = True
|
||||
reply_message_id = i.id
|
||||
if isinstance(i, At):
|
||||
at_user_id = i.name
|
||||
|
||||
at_flag = False
|
||||
for i in message.chain:
|
||||
payload = {
|
||||
"chat_id": user_name,
|
||||
}
|
||||
if has_reply:
|
||||
payload["reply_to_message_id"] = reply_message_id
|
||||
|
||||
if isinstance(i, Plain):
|
||||
if at_user_id and not at_flag:
|
||||
i.text = f"@{at_user_id} " + i.text
|
||||
at_flag = True
|
||||
await client.send_message(text=i.text, **payload)
|
||||
elif isinstance(i, Image):
|
||||
if i.path:
|
||||
image_path = i.path
|
||||
else:
|
||||
image_path = i.file
|
||||
|
||||
if image_path.startswith("base64://"):
|
||||
import base64
|
||||
base64_data = image_path[9:]
|
||||
image_bytes = base64.b64decode(base64_data)
|
||||
await client.send_photo(photo=image_bytes, **payload)
|
||||
else:
|
||||
await client.send_photo(photo=image_path, **payload)
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
if self.get_message_type() == MessageType.GROUP_MESSAGE:
|
||||
await self.send_with_client(self.client, message, self.message_obj.group_id)
|
||||
else:
|
||||
await self.send_with_client(self.client, message, self.get_sender_id())
|
||||
await super().send(message)
|
||||
@@ -0,0 +1,230 @@
|
||||
import sys
|
||||
import uuid
|
||||
import asyncio
|
||||
import quart
|
||||
|
||||
from astrbot.api.platform import Platform, AstrBotMessage, MessageMember, PlatformMetadata, MessageType
|
||||
from astrbot.api.event import MessageChain
|
||||
from astrbot.api.message_components import Plain, Image, Record
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.api.platform import register_platform_adapter
|
||||
from astrbot.core import logger
|
||||
from requests import Response
|
||||
|
||||
from wechatpy.enterprise.crypto import WeChatCrypto
|
||||
from wechatpy.enterprise import WeChatClient
|
||||
from wechatpy.enterprise.messages import TextMessage, ImageMessage, VoiceMessage
|
||||
from wechatpy.exceptions import InvalidSignatureException
|
||||
from wechatpy.enterprise import parse_message
|
||||
from .wecom_event import WecomPlatformEvent
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
class WecomServer():
|
||||
def __init__(
|
||||
self,
|
||||
event_queue: asyncio.Queue,
|
||||
config: dict
|
||||
):
|
||||
self.server = quart.Quart(__name__)
|
||||
self.port = int(config.get("port"))
|
||||
self.server.add_url_rule('/callback/command', view_func=self.verify, methods=['GET'])
|
||||
self.server.add_url_rule('/callback/command', view_func=self.callback_command, methods=['POST'])
|
||||
self.event_queue = event_queue
|
||||
|
||||
self.crypto = WeChatCrypto(
|
||||
config['token'].strip(),
|
||||
config['encoding_aes_key'].strip(),
|
||||
config['corpid'].strip()
|
||||
)
|
||||
|
||||
self.callback = None
|
||||
|
||||
async def verify(self):
|
||||
logger.info(f"验证请求有效性: {quart.request.args}")
|
||||
args = quart.request.args
|
||||
try:
|
||||
echo_str = self.crypto.check_signature(
|
||||
args.get('msg_signature'),
|
||||
args.get('timestamp'),
|
||||
args.get('nonce'),
|
||||
args.get('echostr')
|
||||
)
|
||||
logger.info("验证请求有效性成功。")
|
||||
return echo_str
|
||||
except InvalidSignatureException:
|
||||
logger.error("验证请求有效性失败,签名异常,请检查配置。")
|
||||
raise
|
||||
|
||||
async def callback_command(self):
|
||||
data = await quart.request.get_data()
|
||||
msg_signature = quart.request.args.get('msg_signature')
|
||||
timestamp = quart.request.args.get('timestamp')
|
||||
nonce = quart.request.args.get('nonce')
|
||||
try:
|
||||
xml = self.crypto.decrypt_message(
|
||||
data,
|
||||
msg_signature,
|
||||
timestamp,
|
||||
nonce
|
||||
)
|
||||
except InvalidSignatureException:
|
||||
logger.error("解密失败,签名异常,请检查配置。")
|
||||
raise
|
||||
else:
|
||||
msg = parse_message(xml)
|
||||
logger.info(f"解析成功: {msg}")
|
||||
|
||||
if self.callback:
|
||||
await self.callback(msg)
|
||||
|
||||
return "success"
|
||||
|
||||
async def start_polling(self):
|
||||
logger.info(f"将在 0.0.0.0:{self.port} 端口启动 企业微信 适配器。")
|
||||
await self.server.run_task(
|
||||
host='0.0.0.0',
|
||||
port=self.port,
|
||||
shutdown_trigger=self.shutdown_trigger_placeholder
|
||||
)
|
||||
|
||||
async def shutdown_trigger_placeholder(self):
|
||||
while not self.event_queue.closed:
|
||||
await asyncio.sleep(1)
|
||||
logger.info("企业微信 适配器已关闭。")
|
||||
|
||||
|
||||
@register_platform_adapter("wecom", "wecom 适配器")
|
||||
class WecomPlatformAdapter(Platform):
|
||||
def __init__(self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config
|
||||
self.settingss = platform_settings
|
||||
self.client_self_id = uuid.uuid4().hex[:8]
|
||||
self.api_base_url = platform_config.get("api_base_url", "https://qyapi.weixin.qq.com/cgi-bin/")
|
||||
|
||||
if not self.api_base_url:
|
||||
self.api_base_url = "https://qyapi.weixin.qq.com/cgi-bin/"
|
||||
|
||||
if self.api_base_url.endswith("/"):
|
||||
self.api_base_url = self.api_base_url[:-1]
|
||||
if not self.api_base_url.endswith("/cgi-bin"):
|
||||
self.api_base_url += "/cgi-bin"
|
||||
|
||||
if not self.api_base_url.endswith("/"):
|
||||
self.api_base_url += "/"
|
||||
|
||||
@override
|
||||
async def send_by_session(self, session: MessageSesion, message_chain: MessageChain):
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
@override
|
||||
def meta(self) -> PlatformMetadata:
|
||||
return PlatformMetadata(
|
||||
"wecom",
|
||||
"wecom 适配器",
|
||||
)
|
||||
|
||||
@override
|
||||
async def run(self):
|
||||
self.server = WecomServer(
|
||||
self._event_queue,
|
||||
self.config
|
||||
)
|
||||
|
||||
self.client = WeChatClient(
|
||||
self.config['corpid'].strip(),
|
||||
self.config['secret'].strip(),
|
||||
)
|
||||
self.client.API_BASE_URL = self.api_base_url
|
||||
|
||||
async def callback(msg):
|
||||
await self.convert_message(msg)
|
||||
|
||||
self.server.callback = callback
|
||||
|
||||
await self.server.start_polling()
|
||||
|
||||
async def convert_message(self, msg):
|
||||
abm = AstrBotMessage()
|
||||
if msg.type == 'text':
|
||||
assert isinstance(msg, TextMessage)
|
||||
abm.message_str = msg.content
|
||||
abm.self_id = str(msg.agent)
|
||||
abm.message = [Plain(msg.content)]
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
abm.sender = MessageMember(
|
||||
msg.source,
|
||||
msg.source,
|
||||
)
|
||||
abm.message_id = msg.id
|
||||
abm.timestamp = msg.time
|
||||
abm.session_id = abm.sender.user_id
|
||||
abm.raw_message = msg
|
||||
elif msg.type == 'image':
|
||||
assert isinstance(msg, ImageMessage)
|
||||
abm.message_str = "[图片]"
|
||||
abm.self_id = str(msg.agent)
|
||||
abm.message = [Image(file=msg.image, url=msg.image)]
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
abm.sender = MessageMember(
|
||||
msg.source,
|
||||
msg.source,
|
||||
)
|
||||
abm.message_id = msg.id
|
||||
abm.timestamp = msg.time
|
||||
abm.session_id = abm.sender.user_id
|
||||
abm.raw_message = msg
|
||||
elif msg.type == 'voice':
|
||||
assert isinstance(msg, VoiceMessage)
|
||||
|
||||
resp: Response = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
self.client.media.download,
|
||||
msg.media_id
|
||||
)
|
||||
path = f"data/temp/wecom_{msg.media_id}.amr"
|
||||
with open(path, 'wb') as f:
|
||||
f.write(resp.content)
|
||||
|
||||
try:
|
||||
from pydub import AudioSegment
|
||||
|
||||
path_wav = f"data/temp/wecom_{msg.media_id}.wav"
|
||||
audio = AudioSegment.from_file(path)
|
||||
audio.export(path_wav, format="wav")
|
||||
except Exception as e:
|
||||
logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。")
|
||||
path_wav = path
|
||||
return
|
||||
|
||||
abm.message_str = ""
|
||||
abm.self_id = str(msg.agent)
|
||||
abm.message = [Record(file=path_wav, url=path_wav)]
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
abm.sender = MessageMember(
|
||||
msg.source,
|
||||
msg.source,
|
||||
)
|
||||
abm.message_id = msg.id
|
||||
abm.timestamp = msg.time
|
||||
abm.session_id = abm.sender.user_id
|
||||
abm.raw_message = msg
|
||||
|
||||
|
||||
|
||||
logger.info(f"abm: {abm}")
|
||||
await self.handle_msg(abm)
|
||||
|
||||
async def handle_msg(self, message: AstrBotMessage):
|
||||
message_event = WecomPlatformEvent(
|
||||
message_str=message.message_str,
|
||||
message_obj=message,
|
||||
platform_meta=self.meta(),
|
||||
session_id=message.session_id,
|
||||
client=self.client
|
||||
)
|
||||
self.commit_event(message_event)
|
||||
@@ -0,0 +1,103 @@
|
||||
import uuid
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||
from astrbot.api.message_components import Plain, Image, Record
|
||||
from wechatpy.enterprise import WeChatClient
|
||||
from astrbot.core.utils.io import download_image_by_url, download_file
|
||||
|
||||
from astrbot.api import logger
|
||||
|
||||
try:
|
||||
import pydub
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"检测到 pydub 库未安装,企业微信将无法语音收发。如需使用语音,请前往管理面板 -> 控制台 -> 安装 Pip 库安装 pydub。"
|
||||
)
|
||||
pass
|
||||
|
||||
|
||||
class WecomPlatformEvent(AstrMessageEvent):
|
||||
def __init__(
|
||||
self,
|
||||
message_str: str,
|
||||
message_obj: AstrBotMessage,
|
||||
platform_meta: PlatformMetadata,
|
||||
session_id: str,
|
||||
client: WeChatClient,
|
||||
):
|
||||
super().__init__(message_str, message_obj, platform_meta, session_id)
|
||||
self.client = client
|
||||
|
||||
@staticmethod
|
||||
async def send_with_client(
|
||||
client: WeChatClient, message: MessageChain, user_name: str
|
||||
):
|
||||
pass
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
message_obj = self.message_obj
|
||||
|
||||
for comp in message.chain:
|
||||
if isinstance(comp, Plain):
|
||||
self.client.message.send_text(
|
||||
message_obj.self_id, message_obj.session_id, comp.text
|
||||
)
|
||||
elif isinstance(comp, Image):
|
||||
img_url = comp.file
|
||||
img_path = ""
|
||||
if img_url.startswith("file:///"):
|
||||
img_path = img_url[8:]
|
||||
elif comp.file and comp.file.startswith("http"):
|
||||
img_path = await download_image_by_url(comp.file)
|
||||
else:
|
||||
img_path = img_url
|
||||
|
||||
with open(img_path, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("image", f)
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信上传图片失败: {e}")
|
||||
await self.send(
|
||||
MessageChain().message(f"企业微信上传图片失败: {e}")
|
||||
)
|
||||
return
|
||||
logger.info(f"企业微信上传图片返回: {response}")
|
||||
self.client.message.send_image(
|
||||
message_obj.self_id,
|
||||
message_obj.session_id,
|
||||
response["media_id"],
|
||||
)
|
||||
elif isinstance(comp, Record):
|
||||
record_url = comp.file
|
||||
record_path = ""
|
||||
|
||||
if record_url.startswith("file:///"):
|
||||
record_path = record_url[8:]
|
||||
elif record_url.startswith("http"):
|
||||
await download_file(record_url, f"data/temp/{uuid.uuid4()}.wav")
|
||||
else:
|
||||
record_path = record_url
|
||||
|
||||
# 转成amr
|
||||
record_path_amr = f"data/temp/{uuid.uuid4()}.amr"
|
||||
pydub.AudioSegment.from_wav(record_path).export(
|
||||
record_path_amr, format="amr"
|
||||
)
|
||||
|
||||
with open(record_path_amr, "rb") as f:
|
||||
try:
|
||||
response = self.client.media.upload("voice", f)
|
||||
except Exception as e:
|
||||
logger.error(f"企业微信上传语音失败: {e}")
|
||||
await self.send(
|
||||
MessageChain().message(f"企业微信上传语音失败: {e}")
|
||||
)
|
||||
return
|
||||
logger.info(f"企业微信上传语音返回: {response}")
|
||||
self.client.message.send_voice(
|
||||
message_obj.self_id,
|
||||
message_obj.session_id,
|
||||
response["media_id"],
|
||||
)
|
||||
|
||||
await super().send(message)
|
||||
@@ -104,7 +104,8 @@ class ProviderManager():
|
||||
kdb_cfg = config.get("knowledge_db", {})
|
||||
if kdb_cfg and len(kdb_cfg):
|
||||
self.curr_kdb_name = list(kdb_cfg.keys())[0]
|
||||
|
||||
|
||||
|
||||
async def initialize(self):
|
||||
for provider_config in self.providers_config:
|
||||
await self.load_provider(provider_config)
|
||||
@@ -123,6 +124,7 @@ class ProviderManager():
|
||||
return
|
||||
|
||||
logger.info(f"载入 {provider_config['type']}({provider_config['id']}) 服务提供商适配器 ...")
|
||||
logger.debug(f"Provider Config: {provider_config}")
|
||||
|
||||
# 动态导入
|
||||
try:
|
||||
@@ -142,6 +144,8 @@ class ProviderManager():
|
||||
from .sources.dashscope_source import ProviderDashscope as ProviderDashscope
|
||||
case "googlegenai_chat_completion":
|
||||
from .sources.gemini_source import ProviderGoogleGenAI as ProviderGoogleGenAI
|
||||
case "sensevoice_stt_selfhost":
|
||||
from .sources.sensevoice_selfhosted_source import ProviderSenseVoiceSTTSelfHost as ProviderSenseVoiceSTTSelfHost
|
||||
case "openai_whisper_api":
|
||||
from .sources.whisper_api_source import ProviderOpenAIWhisperAPI as ProviderOpenAIWhisperAPI
|
||||
case "openai_whisper_selfhost":
|
||||
@@ -242,7 +246,7 @@ class ProviderManager():
|
||||
async def terminate_provider(self, provider_id: str):
|
||||
if provider_id in self.inst_map:
|
||||
|
||||
logger.info(f"终止 {provider_id} 提供商适配器 ...")
|
||||
logger.info(f"终止 {provider_id} 提供商适配器({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)}) ...")
|
||||
|
||||
if self.inst_map[provider_id] in self.provider_insts:
|
||||
self.provider_insts.remove(self.inst_map[provider_id])
|
||||
@@ -250,11 +254,19 @@ class ProviderManager():
|
||||
self.stt_provider_insts.remove(self.inst_map[provider_id])
|
||||
if self.inst_map[provider_id] in self.tts_provider_insts:
|
||||
self.tts_provider_insts.remove(self.inst_map[provider_id])
|
||||
|
||||
if self.inst_map[provider_id] == self.curr_provider_inst:
|
||||
self.curr_provider_inst = None
|
||||
if self.inst_map[provider_id] == self.curr_stt_provider_inst:
|
||||
self.curr_stt_provider_inst = None
|
||||
if self.inst_map[provider_id] == self.curr_tts_provider_inst:
|
||||
self.curr_tts_provider_inst = None
|
||||
|
||||
if getattr(self.inst_map[provider_id], 'terminate', None):
|
||||
await self.inst_map[provider_id].terminate()
|
||||
logger.info(f"{provider_id} 提供商适配器已终止。")
|
||||
del self.inst_map[provider_id]
|
||||
|
||||
logger.info(f"{provider_id} 提供商适配器已终止({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)})")
|
||||
del self.inst_map[provider_id]
|
||||
|
||||
async def terminate(self):
|
||||
for provider_inst in self.provider_insts:
|
||||
|
||||
@@ -99,6 +99,12 @@ class ProviderDify(Provider):
|
||||
if not conversation_id:
|
||||
self.conversation_ids[session_id] = chunk['conversation_id']
|
||||
conversation_id = chunk['conversation_id']
|
||||
elif chunk['event'] == 'message_end':
|
||||
logger.debug("Dify message end")
|
||||
break
|
||||
elif chunk['event'] == 'error':
|
||||
logger.error(f"Dify 出现错误:{chunk}")
|
||||
raise Exception(f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}")
|
||||
|
||||
case "workflow":
|
||||
async for chunk in self.api_client.workflow_run(
|
||||
@@ -130,6 +136,9 @@ class ProviderDify(Provider):
|
||||
logger.error(f"Dify 请求失败:{str(e)}")
|
||||
return LLMResponse(role="err", completion_text=f"Dify 请求失败:{str(e)}")
|
||||
|
||||
if not result:
|
||||
logger.warning("Dify 请求结果为空,请查看 Debug 日志。")
|
||||
|
||||
return LLMResponse(role="assistant", completion_text=result)
|
||||
|
||||
async def forget(self, session_id):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import inspect
|
||||
|
||||
from openai import AsyncOpenAI, AsyncAzureOpenAI
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
@@ -49,6 +50,8 @@ class ProviderOpenAIOfficial(Provider):
|
||||
timeout=self.timeout
|
||||
)
|
||||
|
||||
self.default_params = inspect.signature(self.client.chat.completions.create).parameters.keys()
|
||||
|
||||
model_config = provider_config.get("model_config", {})
|
||||
model = model_config.get("model", "unknown")
|
||||
self.set_model(model)
|
||||
@@ -69,13 +72,26 @@ class ProviderOpenAIOfficial(Provider):
|
||||
tool_list = tools.get_func_desc_openai_style()
|
||||
if tool_list:
|
||||
payloads['tools'] = tool_list
|
||||
|
||||
|
||||
# 不在默认参数中的参数放在 extra_body 中
|
||||
extra_body = {}
|
||||
to_del = []
|
||||
for key in payloads.keys():
|
||||
if key not in self.default_params:
|
||||
extra_body[key] = payloads[key]
|
||||
to_del.append(key)
|
||||
for key in to_del:
|
||||
del payloads[key]
|
||||
|
||||
completion = await self.client.chat.completions.create(
|
||||
**payloads,
|
||||
stream=False
|
||||
stream=False,
|
||||
extra_body=extra_body
|
||||
)
|
||||
|
||||
assert isinstance(completion, ChatCompletion)
|
||||
if not isinstance(completion, ChatCompletion):
|
||||
raise Exception(f"API 返回的 completion 类型错误:{type(completion)}: {completion}。")
|
||||
|
||||
logger.debug(f"completion: {completion}")
|
||||
|
||||
if len(completion.choices) == 0:
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
'''
|
||||
Author: diudiu62
|
||||
Date: 2025-02-24 18:04:18
|
||||
LastEditTime: 2025-02-25 14:06:30
|
||||
'''
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import os
|
||||
import re
|
||||
from funasr_onnx import SenseVoiceSmall
|
||||
from funasr_onnx.utils.postprocess_utils import rich_transcription_postprocess
|
||||
from ..provider import STTProvider
|
||||
from ..entites import ProviderType
|
||||
from astrbot.core.utils.io import download_file
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.utils.tencent_record_helper import tencent_silk_to_wav
|
||||
|
||||
@register_provider_adapter("sensevoice_stt_selfhost", "SenseVoice 自托管语音识别 模型部署", provider_type=ProviderType.SPEECH_TO_TEXT)
|
||||
class ProviderSenseVoiceSTTSelfHost(STTProvider):
|
||||
def __init__(
|
||||
self,
|
||||
provider_config: dict,
|
||||
provider_settings: dict,
|
||||
) -> None:
|
||||
super().__init__(provider_config, provider_settings)
|
||||
self.set_model(provider_config.get("stt_model", None))
|
||||
self.model = None
|
||||
self.is_emotion = provider_config.get("is_emotion", False)
|
||||
|
||||
async def initialize(self):
|
||||
logger.info("下载或者加载 SenseVoice 模型中,这可能需要一些时间 ...")
|
||||
|
||||
|
||||
# 将模型加载放到线程池中执行
|
||||
self.model = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: SenseVoiceSmall(self.model_name, quantize=True, batch_size=16)
|
||||
)
|
||||
|
||||
logger.info("SenseVoice 模型加载完成。")
|
||||
|
||||
async def get_timestamped_path(self) -> str:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return os.path.join("data", "temp", f"{timestamp}")
|
||||
|
||||
async def _convert_audio(self, path: str) -> str:
|
||||
from pyffmpeg import FFmpeg
|
||||
filename = await self.get_timestamped_path() + '.mp3'
|
||||
ff = FFmpeg()
|
||||
output_path = ff.convert(path, os.path.join('data","temp', filename))
|
||||
return output_path
|
||||
|
||||
async def _is_silk_file(self, file_path):
|
||||
silk_header = b"SILK"
|
||||
with open(file_path, "rb") as f:
|
||||
file_header = f.read(8)
|
||||
|
||||
if silk_header in file_header:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
async def get_text(self, audio_url: str) -> str:
|
||||
try:
|
||||
is_tencent = audio_url.startswith("http") and "multimedia.nt.qq.com.cn" in audio_url
|
||||
|
||||
if is_tencent:
|
||||
path = await self.get_timestamped_path()
|
||||
await download_file(audio_url, path)
|
||||
audio_url = path
|
||||
|
||||
if not os.path.isfile(audio_url):
|
||||
raise FileNotFoundError(f"文件不存在: {audio_url}")
|
||||
|
||||
if audio_url.endswith((".amr", ".silk")) or is_tencent:
|
||||
is_silk = await self._is_silk_file(audio_url)
|
||||
if is_silk:
|
||||
logger.info("Converting silk file to wav ...")
|
||||
output_path = await self.get_timestamped_path()+'.wav'
|
||||
await tencent_silk_to_wav(audio_url, output_path)
|
||||
audio_url = output_path
|
||||
|
||||
# 使用 run_in_executor 来调用模型进行识别
|
||||
loop = asyncio.get_event_loop()
|
||||
res = await loop.run_in_executor(
|
||||
None, # 使用默认的线程池
|
||||
lambda: self.model(audio_url, language="auto", use_itn=True)
|
||||
)
|
||||
|
||||
# res = self.model(audio_url, language="auto", use_itn=True)
|
||||
logger.debug(f"SenseVoice识别到的文案:{res}")
|
||||
text = rich_transcription_postprocess(res[0])
|
||||
if self.is_emotion:
|
||||
# 提取第二个匹配的值
|
||||
matches = re.findall(r'<\|([^|]+)\|>', res[0])
|
||||
if len(matches) >= 2:
|
||||
emotion = matches[1]
|
||||
text = f"(当前的情绪:{emotion}) {text}"
|
||||
else:
|
||||
logger.warning("未能提取到情绪信息")
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.error(f"处理音频文件时出错: {e}")
|
||||
raise
|
||||
@@ -9,13 +9,19 @@ class PlatformAdapterType(enum.Flag):
|
||||
QQOFFICIAL = enum.auto()
|
||||
VCHAT = enum.auto()
|
||||
GEWECHAT = enum.auto()
|
||||
ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT
|
||||
TELEGRAM = enum.auto()
|
||||
WECOM = enum.auto()
|
||||
LARK = enum.auto()
|
||||
ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT | TELEGRAM | WECOM | LARK
|
||||
|
||||
ADAPTER_NAME_2_TYPE = {
|
||||
"aiocqhttp": PlatformAdapterType.AIOCQHTTP,
|
||||
"qq_official": PlatformAdapterType.QQOFFICIAL,
|
||||
"vchat": PlatformAdapterType.VCHAT,
|
||||
"gewechat": PlatformAdapterType.GEWECHAT
|
||||
"gewechat": PlatformAdapterType.GEWECHAT,
|
||||
"telegram": PlatformAdapterType.TELEGRAM,
|
||||
"wecom": PlatformAdapterType.WECOM,
|
||||
"lark": PlatformAdapterType.LARK
|
||||
}
|
||||
|
||||
class PlatformAdapterTypeFilter(HandlerFilter):
|
||||
|
||||
@@ -181,8 +181,8 @@ class ChatRoute(Route):
|
||||
self.db.update_conversation(username, cid, history=json.dumps(history))
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
except BaseException as e:
|
||||
logger.debug(f"用户 {username} 断开聊天长连接: {str(e)}。")
|
||||
except BaseException as _:
|
||||
logger.debug(f"用户 {username} 断开聊天长连接。")
|
||||
self.curr_chat_sse.pop(username)
|
||||
return
|
||||
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
from .route import Route, RouteContext
|
||||
|
||||
|
||||
class StaticFileRoute(Route):
|
||||
def __init__(self, context: RouteContext) -> None:
|
||||
super().__init__(context)
|
||||
|
||||
index_ = ['/', '/auth/login', '/config', '/logs', '/extension', '/dashboard/default', '/project-atri', '/console', '/chat']
|
||||
|
||||
index_ = [
|
||||
"/",
|
||||
"/auth/login",
|
||||
"/config",
|
||||
"/logs",
|
||||
"/extension",
|
||||
"/dashboard/default",
|
||||
"/project-atri",
|
||||
"/console",
|
||||
"/chat",
|
||||
"/settings",
|
||||
"/platforms",
|
||||
"/providers",
|
||||
"/about",
|
||||
]
|
||||
for i in index_:
|
||||
self.app.add_url_rule(i, view_func=self.index)
|
||||
|
||||
|
||||
@self.app.errorhandler(404)
|
||||
async def page_not_found(e):
|
||||
return "404 Not found。如果你初次使用打开面板发现 404,请参考文档: https://astrbot.app/deploy/dashboard-404.html"
|
||||
|
||||
return "404 Not found。如果你初次使用打开面板发现 404, 请参考文档: https://astrbot.app/faq.html。"
|
||||
|
||||
async def index(self):
|
||||
return await self.app.send_static_file('index.html')
|
||||
return await self.app.send_static_file("index.html")
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# What's Changed
|
||||
|
||||
|
||||
1. ✨ 新增: Add a draggable iframe for tutorial links and enhance platform configuration UI
|
||||
2. ✨ 新增: 集成 astrbot_plugin_telegram/企业微信 至 astrbot
|
||||
3. ✨ 新增: openai_source 支持传入任何自定义参数以适配 Ollama 和 FastGPT 等 provider
|
||||
4. ✨ 新增: Telegram 适配器中支持 @ 唤醒
|
||||
5. ✨ 新增: 添加面板下载按钮置灰 by @Fridemn
|
||||
6. ✨ 新增: 添加 SenseVoice 语音转文本(STT)服务 by @diudiu62
|
||||
7. ⚡ 优化: Increase forward threshold from 200 to 1500 in default configuration
|
||||
8. ⚡ 优化: 添加控制台关闭自动滚动按钮 by @Fridemn
|
||||
9. 🐛 修复: 修复前端面板部分页面刷新后的 404 错误
|
||||
10. 🐛 修复: 修复某些情况下热重载 服务提供商 时可能没有正确应用的问题
|
||||
11. 🐛 修复: 修复 Telegram 适配器中未处理 base64 的问题 @Raven95676
|
||||
12. 🐛 修复: 修复 Dify 主动回复报错的问题 #616
|
||||
@@ -300,7 +300,7 @@ commonStore.getStartTime();
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-btn color="primary" style="border-radius: 10px;" @click="updateDashboard()">
|
||||
<v-btn color="primary" style="border-radius: 10px;" @click="updateDashboard()" :disabled="!dashboardHasNewVersion">
|
||||
下载并更新
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from 'vue';
|
||||
<script setup>
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { useCustomizerStore } from '../../../stores/customizer';
|
||||
import sidebarItems from './sidebarItem';
|
||||
import NavItem from './NavItem.vue';
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const sidebarMenu = shallowRef(sidebarItems);
|
||||
|
||||
const showIframe = ref(false);
|
||||
|
||||
const dragButtonStyle = {
|
||||
width: '100%',
|
||||
padding: '4px',
|
||||
cursor: 'move',
|
||||
background: '#f0f0f0',
|
||||
borderBottom: '1px solid #ccc',
|
||||
borderTopLeftRadius: '8px',
|
||||
borderTopRightRadius: '8px'
|
||||
};
|
||||
|
||||
function toggleIframe() {
|
||||
showIframe.value = !showIframe.value;
|
||||
}
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let isDragging = false;
|
||||
|
||||
// @ts-ignore
|
||||
function onMouseDown(event) {
|
||||
isDragging = true;
|
||||
offsetX = event.clientX - event.target.parentElement.getBoundingClientRect().left;
|
||||
offsetY = event.clientY - event.target.parentElement.getBoundingClientRect().top;
|
||||
}
|
||||
// @ts-ignore
|
||||
function onMouseMove(event) {
|
||||
if (isDragging) {
|
||||
const dm = document.getElementById('draggable-iframe');
|
||||
// @ts-ignore
|
||||
dm.style.left = (event.clientX - offsetX) + 'px';
|
||||
// @ts-ignore
|
||||
dm.style.top = (event.clientY - offsetY) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer left v-model="customizer.Sidebar_drawer" elevation="0" rail-width="80"
|
||||
app class="leftSidebar" :rail="customizer.mini_sidebar">
|
||||
<v-navigation-drawer left v-model="customizer.Sidebar_drawer" elevation="0" rail-width="80" app class="leftSidebar"
|
||||
:rail="customizer.mini_sidebar">
|
||||
<v-list class="pa-4 listitem" style="height: auto">
|
||||
<template v-for="(item, i) in sidebarMenu" :key="i">
|
||||
<NavItem :item="item" class="leftPadding" />
|
||||
@@ -21,9 +63,9 @@ const sidebarMenu = shallowRef(sidebarItems);
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; bottom: 32px; width: 100%" class="text-center">
|
||||
<v-list-item v-if="!customizer.mini_sidebar" href="https://astrbot.app/">
|
||||
<v-list-item v-if="!customizer.mini_sidebar" @click="toggleIframe">
|
||||
<v-btn variant="plain" size="small">
|
||||
🤔 初次使用?点击查看文档!
|
||||
🤔 点击查看悬浮文档!
|
||||
</v-btn>
|
||||
</v-list-item>
|
||||
<small style="display: block;" v-if="buildVer">构建: {{ buildVer }}</small>
|
||||
@@ -34,14 +76,25 @@ const sidebarMenu = shallowRef(sidebarItems);
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
|
||||
|
||||
<small style="display: block; margin-top: 8px;">© 2025 AstrBot</small>
|
||||
</div>
|
||||
|
||||
</v-navigation-drawer>
|
||||
<div v-if="showIframe"
|
||||
id="draggable-iframe"
|
||||
style="position: fixed; bottom: 16px; right: 16px; width: 500px; height: 400px; border: 1px solid #ccc; background: white; resize: both; overflow: auto; z-index: 10000000; border-radius: 8px;"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseup="onMouseUp"
|
||||
@mouseleave="onMouseUp">
|
||||
<div :style="dragButtonStyle" @mousedown="onMouseDown">
|
||||
<v-icon icon="mdi-cursor-move" />
|
||||
</div>
|
||||
<iframe src="https://astrbot.app" style="width: 100%; height: calc(100% - 24px); border: none; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
export default {
|
||||
name: 'VerticalSidebar',
|
||||
|
||||
@@ -9,6 +9,17 @@ export const useCommonStore = defineStore({
|
||||
log_cache: [],
|
||||
log_cache_max_len: 1000,
|
||||
startTime: -1,
|
||||
|
||||
tutorial_map: {
|
||||
"qq_official_webhook": "https://astrbot.app/deploy/platform/qqofficial/webhook.html",
|
||||
"qq_official": "https://astrbot.app/deploy/platform/qqofficial/websockets.html",
|
||||
"aiocqhttp": "https://astrbot.app/deploy/platform/aiocqhttp/napcat.html",
|
||||
"wecom": "https://astrbot.app/deploy/platform/wecom.html",
|
||||
"gewechat": "https://astrbot.app/deploy/platform/gewechat.html",
|
||||
"lark": "https://astrbot.app/deploy/platform/lark.html",
|
||||
"telegram": "https://astrbot.app/deploy/platform/telegram.html",
|
||||
}
|
||||
|
||||
}),
|
||||
actions: {
|
||||
createWebSocket() {
|
||||
@@ -39,5 +50,8 @@ export const useCommonStore = defineStore({
|
||||
this.startTime = res.data.data.start_time
|
||||
})
|
||||
},
|
||||
getTutorialLink(platform) {
|
||||
return this.tutorial_map[platform]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -47,14 +47,29 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-dialog v-model="showPlatformCfg" width="700">
|
||||
<v-dialog v-model="showPlatformCfg">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="text-h4">{{ newSelectedPlatformName }} 配置</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<AstrBotConfig :iterable="newSelectedPlatformConfig"
|
||||
:metadata="metadata['platform_group']['metadata']" metadataKey="platform" />
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<AstrBotConfig :iterable="newSelectedPlatformConfig"
|
||||
:metadata="metadata['platform_group']['metadata']" metadataKey="platform" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn :loading="iframeLoading" @click="refreshIframe" variant="tonal" color="primary" style="float: right;">
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
刷新
|
||||
</v-btn>
|
||||
<iframe v-show="!iframeLoading"
|
||||
:src="store.getTutorialLink(newSelectedPlatformConfig.type)"
|
||||
@load="iframeLoading = false" style="width: 100%; border: none; height: 100%;">
|
||||
</iframe>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -66,7 +81,8 @@
|
||||
</v-dialog>
|
||||
|
||||
|
||||
<v-btn style="margin-top: 16px" class="flex-grow-1" variant="tonal" size="large" rounded="lg" color="gray" @click="showConsole = !showConsole">
|
||||
<v-btn style="margin-top: 16px" class="flex-grow-1" variant="tonal" size="large" rounded="lg" color="gray"
|
||||
@click="showConsole = !showConsole">
|
||||
<template v-slot:default>
|
||||
<v-icon>mdi-console-line</v-icon>
|
||||
{{ showConsole ? '隐藏' : '显示' }}日志
|
||||
@@ -91,6 +107,7 @@ import axios from 'axios';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
|
||||
export default {
|
||||
name: 'PlatformPage',
|
||||
@@ -117,6 +134,9 @@ export default {
|
||||
save_message_success: "",
|
||||
|
||||
showConsole: false,
|
||||
iframeLoading: true,
|
||||
|
||||
store: useCommonStore()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -125,6 +145,12 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshIframe() {
|
||||
this.iframeLoading = true;
|
||||
const iframe = document.querySelector('iframe');
|
||||
console.log(iframe.src);
|
||||
iframe.src = iframe.src + '?t=' + new Date().getTime();
|
||||
},
|
||||
getConfig() {
|
||||
// 获取配置
|
||||
axios.get('/api/config/get').then((res) => {
|
||||
|
||||
@@ -146,6 +146,7 @@ class Main(star.Star):
|
||||
try:
|
||||
docker = aiodocker.Docker()
|
||||
await docker.version()
|
||||
await docker.close()
|
||||
return True
|
||||
except BaseException as e:
|
||||
logger.info(f"检查 Docker 可用性: {e}")
|
||||
@@ -310,6 +311,7 @@ class Main(star.Star):
|
||||
# 启动容器
|
||||
docker = aiodocker.Docker()
|
||||
|
||||
|
||||
# 检查有没有image
|
||||
image_name = await self.get_image_name()
|
||||
try:
|
||||
|
||||
+3
-4
@@ -9,7 +9,6 @@ beautifulsoup4
|
||||
googlesearch-python
|
||||
readability-lxml
|
||||
quart
|
||||
psutil
|
||||
lxml_html_clean
|
||||
colorlog
|
||||
aiocqhttp
|
||||
@@ -19,9 +18,9 @@ docstring_parser
|
||||
aiodocker
|
||||
silk-python
|
||||
psutil>=5.8.0
|
||||
|
||||
lark-oapi
|
||||
ormsgpack
|
||||
cryptography
|
||||
|
||||
dashscope
|
||||
dashscope
|
||||
python-telegram-bot
|
||||
wechatpy
|
||||
|
||||
Reference in New Issue
Block a user