Compare commits

...

34 Commits

Author SHA1 Message Date
Soulter 2649d46d8d chore: remove ts 2025-02-27 01:01:28 +08:00
Soulter e23ffe6f02 chore: remove ts 2025-02-27 00:57:55 +08:00
Soulter 96f3c3729a v3.4.32 2025-02-27 00:44:23 +08:00
Soulter 11e9d47ce2 fix: dify active message error #616 2025-02-27 00:26:04 +08:00
Soulter efbc8e4383 Merge pull request #614 from Raven95676/master
🐛 fix: 修复telegram适配器中未处理base64的问题
2025-02-27 00:03:38 +08:00
Soulter bc7404409f Merge pull request #612 from diudiu62/feat-sensevoice
新增sensevoice语言识别能力
2025-02-26 23:56:03 +08:00
Soulter 8677d70baf feat: add sensevoice adapter 2025-02-26 23:55:00 +08:00
Soulter f39253f0e1 Merge branch 'master' into feat-sensevoice 2025-02-26 23:27:04 +08:00
Soulter 68c1957267 chore: update gitignore 2025-02-26 23:21:28 +08:00
Raven95676 a275aa2e4d 🐛 fix: 修复telegram适配器中未处理base64的问题 2025-02-26 16:35:44 +08:00
Soulter cadbac9948 🐛 fix: update 404 error message to reference FAQ for better user guidance 2025-02-26 11:56:40 +08:00
diudiu62 82673e8ddd 依赖放到了参数配置地方提醒,docker提前自行打包依赖 2025-02-26 09:46:30 +08:00
Soulter bee51024b3 perf: 修复 wecom 配置项的空格问题,确保正确传递 #599 2025-02-26 00:57:54 +08:00
Soulter 3437cb73ec Merge pull request #605 from Soulter/feat-update-btn
feat: 添加面板下载按钮置灰
2025-02-25 22:26:12 +08:00
diudiu62 d01d1a8520 增加依赖 2025-02-25 18:03:29 +08:00
diudiu62 5aa842cf66 增加sensevoice配置 2025-02-25 14:15:22 +08:00
Soulter 03282dee0f 🐛 fix: handle message end and error events in Dify provider, improve logging and error reporting 2025-02-25 14:09:12 +08:00
Soulter 98e8ecb8e2 🐛 fix: add type check for completion response from API to ensure correct handling 2025-02-25 11:46:44 +08:00
Soulter 9451dc3fd4 🐛 fix: 修复某些情况下热重载 provider 时可能没有正确应用的问题 2025-02-25 11:46:44 +08:00
崔永亮 e1d3759f55 feat: 添加面板下载按钮置灰 2025-02-25 10:13:34 +08:00
diudiu62 0ec382c86b 尝试集成sensevoice 2025-02-25 09:05:24 +08:00
Soulter 756087c9f1 feat: 扩展 PlatformAdapterType,支持 Telegram、WeCom 和 Lark 适配器 #601 2025-02-25 01:39:34 +08:00
Soulter 3e7c47e873 feat: 在 Telegram 适配器中支持@功能,增强消息处理能力 2025-02-25 01:32:44 +08:00
Soulter e3ffdbc308 feat: openai_source 支持传入任何自定义参数以适配 Ollama 和 FastGPT 等 2025-02-25 00:51:09 +08:00
Soulter 645cace4d6 feat: 添加企业微信适配器配置并优化默认配置格式 2025-02-24 23:00:41 +08:00
Soulter 0959d5986b feat: 将 astrbot_plugin_wecom 集成至 astrbot 2025-02-24 22:43:43 +08:00
Soulter 89605c29a7 🐛 fix: ping docker 后关闭 Docker 连接以避免资源泄漏 2025-02-24 22:26:46 +08:00
Soulter e527f31213 feat: 集成 astrbot_plugin_telegram 至 astrbot 2025-02-24 22:26:23 +08:00
Soulter a0dbd99928 feat: 在静态文件路由中添加新的URL路径以增强功能 2025-02-24 22:09:42 +08:00
Soulter 17d39c7a4a 🐛 fix: increase forward threshold from 200 to 1500 in default configuration 2025-02-24 15:38:22 +08:00
Soulter 54edaebbd9 🐛 fix: remove unnecessary verification flag for captcha handling in SimpleGewechatClient 2025-02-24 15:36:37 +08:00
Soulter d587a6f64c feat: add draggable iframe for tutorial links and enhance platform configuration UI 2025-02-24 13:50:07 +08:00
Soulter 2371c32be5 Update LICENSE 2025-02-24 00:31:57 +08:00
Soulter c9abb8352c Update LICENSE 2025-02-24 00:29:27 +08:00
27 changed files with 922 additions and 81 deletions
+1 -2
View File
@@ -25,5 +25,4 @@ package.json
venv/*
packages/python_interpreter/workplace
.venv/*
.conda/
.conda/
+2 -2
View File
@@ -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
+84 -36
View File
@@ -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": "是否开启情绪识别。happysadangryneutralfearfuldisgustedsurprisedunknown",
},
"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": {
+1 -1
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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)
+16 -4
View File
@@ -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):
+19 -3
View File
@@ -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):
+2 -2
View File
@@ -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
+22 -6
View File
@@ -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")
+15
View File
@@ -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',
+14
View File
@@ -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]
}
}
});
+30 -4
View File
@@ -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) => {
+2
View File
@@ -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
View File
@@ -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