Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bcebd0fb62 | |||
| 3817d3ca87 | |||
| 4dd714e814 | |||
| 61e8bb49ec | |||
| 103dcd3761 | |||
| 54ac135fc8 | |||
| 86582809fc | |||
| 974d648f19 | |||
| a79afc9597 | |||
| e4883241d9 | |||
| babf223745 | |||
| c7d91730b6 | |||
| 71246b65c9 | |||
| 50076b647e | |||
| a1a788dce8 | |||
| a611b4f346 | |||
| 7f6ed674b4 | |||
| aa3cfd887a | |||
| 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
|
||||
|
||||
@@ -18,6 +18,7 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
|
||||

|
||||
[](https://codecov.io/gh/Soulter/AstrBot)
|
||||
|
||||
<a href="https://github.com/Soulter/AstrBot/blob/master/README_en.md">English</a> |
|
||||
<a href="https://github.com/Soulter/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||
<a href="https://astrbot.app/">查看文档</a> |
|
||||
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a>
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<p align="center">
|
||||
|
||||

|
||||
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
_✨ Easy-to-use Multi-platform LLM Chatbot & Development Framework ✨_
|
||||
|
||||
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
[](https://github.com/Soulter/AstrBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
||||
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/QQ群-630166526-purple">
|
||||
[](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
|
||||

|
||||
[](https://codecov.io/gh/Soulter/AstrBot)
|
||||
|
||||
<a href="https://astrbot.app/">Documentation</a> |
|
||||
<a href="https://github.com/Soulter/AstrBot/issues">Issue Tracking</a>
|
||||
</div>
|
||||
|
||||
AstrBot is a loosely coupled, asynchronous chatbot and development framework that supports multi-platform deployment, featuring an easy-to-use plugin system and comprehensive Large Language Model (LLM) integration capabilities.
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
1. **LLM Conversations** - Supports various LLMs including OpenAI API, Google Gemini, Llama, Deepseek, ChatGLM, etc. Enables local model deployment via Ollama/LLMTuner. Features multi-turn dialogues, personality contexts, multimodal capabilities (image understanding), and speech-to-text (Whisper).
|
||||
2. **Multi-platform Integration** - Supports QQ (OneBot), QQ Channels, WeChat (Gewechat), Feishu, and Telegram. Planned support for DingTalk, Discord, WhatsApp, and Xiaomi Smart Speakers. Includes rate limiting, whitelisting, keyword filtering, and Baidu content moderation.
|
||||
3. **Agent Capabilities** - Native support for code execution, natural language TODO lists, web search. Integrates with [Dify Platform](https://astrbot.app/others/dify.html) for easy access to Dify assistants/knowledge bases/workflows.
|
||||
4. **Plugin System** - Optimized plugin mechanism with minimal development effort. Supports multiple installed plugins.
|
||||
5. **Web Dashboard** - Visual configuration management, plugin controls, logging, and WebChat interface for direct LLM interaction.
|
||||
6. **High Stability & Modularity** - Event bus and pipeline architecture ensures high modularization and loose coupling.
|
||||
|
||||
> [!TIP]
|
||||
> Dashboard Demo: [https://demo.astrbot.app/](https://demo.astrbot.app/)
|
||||
> Username: `astrbot`, Password: `astrbot` (LLM not configured for chat page)
|
||||
|
||||
## ✨ Deployment
|
||||
|
||||
#### Docker Deployment
|
||||
|
||||
See docs: [Deploy with Docker](https://astrbot.app/deploy/astrbot/docker.html#docker-deployment)
|
||||
|
||||
#### Windows Installer
|
||||
|
||||
Requires Python (>3.10). See docs: [Windows Installer Guide](https://astrbot.app/deploy/astrbot/windows.html)
|
||||
|
||||
#### Replit Deployment
|
||||
|
||||
[](https://repl.it/github/Soulter/AstrBot)
|
||||
|
||||
#### CasaOS Deployment
|
||||
|
||||
Community-contributed method.
|
||||
See docs: [CasaOS Deployment](https://astrbot.app/deploy/astrbot/casaos.html)
|
||||
|
||||
#### Manual Deployment
|
||||
|
||||
See docs: [Source Code Deployment](https://astrbot.app/deploy/astrbot/cli.html)
|
||||
|
||||
## ⚡ Platform Support
|
||||
|
||||
| Platform | Status | Details | Message Types |
|
||||
| -------------------------------------------------------------- | ------ | ------------------- | ------------------- |
|
||||
| QQ (Official Bot) | ✔ | Private/Group chats | Text, Images |
|
||||
| QQ (OneBot) | ✔ | Private/Group chats | Text, Images, Voice |
|
||||
| WeChat (Personal) | ✔ | Private/Group chats | Text, Images, Voice |
|
||||
| [Telegram](https://github.com/Soulter/astrbot_plugin_telegram) | ✔ | Private/Group chats | Text, Images |
|
||||
| [WeChat Work](https://github.com/Soulter/astrbot_plugin_wecom) | ✔ | Private chats | Text, Images, Voice |
|
||||
| Feishu | ✔ | Group chats | Text, Images |
|
||||
| WeChat Open Platform | 🚧 | Planned | - |
|
||||
| Discord | 🚧 | Planned | - |
|
||||
| WhatsApp | 🚧 | Planned | - |
|
||||
| Xiaomi Speakers | 🚧 | Planned | - |
|
||||
|
||||
# 🦌 Roadmap
|
||||
|
||||
> [!TIP]
|
||||
> Suggestions welcome via Issues <3
|
||||
|
||||
- [ ] Ensure feature parity across all platform adapters
|
||||
- [ ] Optimize plugin APIs
|
||||
- [ ] Add default TTS services (e.g., GPT-Sovits)
|
||||
- [ ] Enhance chat features with persistent memory
|
||||
- [ ] i18n Planning
|
||||
|
||||
## ❤️ Contributions
|
||||
|
||||
All Issues/PRs welcome! Simply submit your changes to this project :)
|
||||
|
||||
For major features, please discuss via Issues first.
|
||||
|
||||
## 🌟 Support
|
||||
|
||||
- Star this project!
|
||||
- Support via [Afdian](https://afdian.com/a/soulter)
|
||||
- WeChat support: [QR Code](https://drive.soulter.top/f/pYfA/d903f4fa49a496fda3f16d2be9e023b5.png)
|
||||
|
||||
## ✨ Demos
|
||||
|
||||
> [!NOTE]
|
||||
> Code executor file I/O currently tested with Napcat(QQ)/Lagrange(QQ)
|
||||
|
||||
<div align='center'>
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/4ee688d9-467d-45c8-99d6-368f9a8a92d8" width="600">
|
||||
|
||||
_✨ Docker-based Sandboxed Code Executor (Beta) ✨_
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/0378f407-6079-4f64-ae4c-e97ab20611d2" height=500>
|
||||
|
||||
_✨ Multimodal Input, Web Search, Text-to-Image ✨_
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/8ec12797-e70f-460a-959e-48eca39ca2bb" height=100>
|
||||
|
||||
_✨ Natural Language TODO Lists ✨_
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/e137a9e1-340a-4bf2-bb2b-771132780735" height=150>
|
||||
<img src="https://github.com/user-attachments/assets/480f5e82-cf6a-4955-a869-0d73137aa6e1" height=150>
|
||||
|
||||
_✨ Plugin System Showcase ✨_
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/592a8630-14c7-4e06-b496-9c0386e4f36c" width=600>
|
||||
|
||||
_✨ Web Dashboard ✨_
|
||||
|
||||

|
||||
|
||||
_✨ Built-in Web Chat Interface ✨_
|
||||
|
||||
</div>
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
> [!TIP]
|
||||
> If this project helps you, please give it a star <3
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://star-history.com/#soulter/astrbot&Date)
|
||||
|
||||
</div>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
1. Licensed under `AGPL-v3`.
|
||||
2. WeChat integration uses [Gewechat](https://github.com/Devo919/Gewechat). Use at your own risk with non-critical accounts.
|
||||
3. Users must comply with local laws and regulations.
|
||||
|
||||
<!-- ## ✨ ATRI [Beta]
|
||||
|
||||
Available as plugin: [astrbot_plugin_atri](https://github.com/Soulter/astrbot_plugin_atri)
|
||||
|
||||
1. Qwen1.5-7B-Chat Lora model fine-tuned with ATRI character data
|
||||
2. Long-term memory
|
||||
3. Meme understanding & responses
|
||||
4. TTS integration
|
||||
-->
|
||||
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
|
||||
"""
|
||||
|
||||
VERSION = "3.4.31"
|
||||
VERSION = "3.4.33"
|
||||
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": "iic/SenseVoiceSmall",
|
||||
"is_emotion": False,
|
||||
},
|
||||
"OpenAI_TTS(API)": {
|
||||
"id": "openai_tts",
|
||||
"type": "openai_tts_api",
|
||||
"enable": False,
|
||||
@@ -574,7 +601,14 @@ CONFIG_METADATA_2 = {
|
||||
"openai-tts-voice": "alloy",
|
||||
"timeout": "20",
|
||||
},
|
||||
"fishaudio_tts(API)": {
|
||||
"Edge_TTS": {
|
||||
"id": "edge_tts",
|
||||
"type": "edge_tts",
|
||||
"enable": False,
|
||||
"edge-tts-voice": "zh-CN-XiaoxiaoNeural",
|
||||
"timeout": 20,
|
||||
},
|
||||
"FishAudio_TTS(API)": {
|
||||
"id": "fishaudio_tts",
|
||||
"type": "fishaudio_tts_api",
|
||||
"enable": False,
|
||||
@@ -585,6 +619,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 +652,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 +776,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'
|
||||
|
||||
@@ -75,6 +75,10 @@ class LLMRequestSubStage(Stage):
|
||||
except BaseException:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
if event.is_stopped():
|
||||
logger.info(f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。")
|
||||
return
|
||||
|
||||
if isinstance(req.contexts, str):
|
||||
req.contexts = json.loads(req.contexts)
|
||||
|
||||
@@ -92,6 +96,11 @@ class LLMRequestSubStage(Stage):
|
||||
await handler.handler(event, llm_response)
|
||||
except BaseException:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
if event.is_stopped():
|
||||
logger.info(f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。")
|
||||
return
|
||||
|
||||
|
||||
# 保存到历史记录
|
||||
await self._save_to_history(event, req, llm_response)
|
||||
@@ -142,6 +151,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:
|
||||
return
|
||||
|
||||
if llm_response.role == "assistant":
|
||||
# 文本回复
|
||||
contexts = req.contexts
|
||||
|
||||
@@ -95,5 +95,9 @@ class RespondStage(Stage):
|
||||
await handler.handler(event)
|
||||
except BaseException:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
if event.is_stopped():
|
||||
logger.info(f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。")
|
||||
return
|
||||
|
||||
event.clear_result()
|
||||
@@ -70,6 +70,10 @@ class ResultDecorateStage(Stage):
|
||||
logger.debug(f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name} 将消息结果清空。")
|
||||
except BaseException:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
if event.is_stopped():
|
||||
logger.info(f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。")
|
||||
return
|
||||
|
||||
# 需要再获取一次。插件可能直接对 chain 进行了替换。
|
||||
result = event.get_result()
|
||||
|
||||
@@ -27,6 +27,9 @@ class PipelineScheduler():
|
||||
logger.debug(f"阶段 {stage.__class__ .__name__} 已终止事件传播。")
|
||||
break
|
||||
await self._process_stages(event, i + 1)
|
||||
if event.is_stopped():
|
||||
logger.debug(f"阶段 {stage.__class__ .__name__} 已终止事件传播。")
|
||||
break
|
||||
else:
|
||||
await coro
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -75,7 +75,7 @@ class AiocqhttpAdapter(Platform):
|
||||
else:
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
if self.unique_session and abm.type == MessageType.GROUP_MESSAGE:
|
||||
abm.session_id = abm.sender.user_id + "_" + str(event.group_id)
|
||||
abm.session_id = str(abm.sender.user_id) + "_" + str(event.group_id)
|
||||
abm.message_str = ''
|
||||
abm.message = []
|
||||
abm.timestamp = int(time.time())
|
||||
@@ -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,12 +144,16 @@ 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":
|
||||
from .sources.whisper_selfhosted_source import ProviderOpenAIWhisperSelfHost as ProviderOpenAIWhisperSelfHost
|
||||
case "openai_tts_api":
|
||||
from .sources.openai_tts_api_source import ProviderOpenAITTSAPI as ProviderOpenAITTSAPI
|
||||
case "edge_tts":
|
||||
from .sources.edge_tts_source import ProviderEdgeTTS as ProviderEdgeTTS
|
||||
case "fishaudio_tts_api":
|
||||
from .sources.fishaudio_tts_api_source import ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
@@ -215,7 +221,7 @@ class ProviderManager():
|
||||
|
||||
self.inst_map[provider_config['id']] = inst
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"实例化 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}")
|
||||
|
||||
async def reload(self, provider_config: dict):
|
||||
@@ -242,7 +248,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 +256,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):
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import uuid
|
||||
import os
|
||||
import edge_tts
|
||||
import subprocess
|
||||
from ..provider import TTSProvider
|
||||
from ..entites import ProviderType
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core import logger
|
||||
|
||||
"""
|
||||
edge_tts 方式,能够免费、快速生成语音,使用需要先安装edge-tts库
|
||||
```
|
||||
pip install edge_tts
|
||||
```
|
||||
Windows 如果提示找不到指定文件,以管理员身份运行命令行窗口,然后再次运行 AstrBot
|
||||
"""
|
||||
|
||||
@register_provider_adapter("edge_tts", "Microsoft Edge TTS", provider_type=ProviderType.TEXT_TO_SPEECH)
|
||||
class ProviderEdgeTTS(TTSProvider):
|
||||
def __init__(
|
||||
self,
|
||||
provider_config: dict,
|
||||
provider_settings: dict,
|
||||
) -> None:
|
||||
super().__init__(provider_config, provider_settings)
|
||||
|
||||
# 设置默认语音,如果没有指定则使用中文小萱
|
||||
self.voice = provider_config.get("edge-tts-voice", "zh-CN-XiaoxiaoNeural")
|
||||
self.rate = provider_config.get("rate", None)
|
||||
self.volume = provider_config.get("volume", None)
|
||||
self.pitch = provider_config.get("pitch", None)
|
||||
self.timeout = provider_config.get("timeout", 30)
|
||||
|
||||
self.set_model("edge_tts")
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
os.makedirs("data/temp", exist_ok=True)
|
||||
mp3_path = f'data/temp/edge_tts_temp_{uuid.uuid4()}.mp3'
|
||||
wav_path = f'data/temp/edge_tts_{uuid.uuid4()}.wav'
|
||||
|
||||
# 构建Edge TTS参数
|
||||
kwargs = {"text": text, "voice": self.voice}
|
||||
if self.rate:
|
||||
kwargs["rate"] = self.rate
|
||||
if self.volume:
|
||||
kwargs["volume"] = self.volume
|
||||
if self.pitch:
|
||||
kwargs["pitch"] = self.pitch
|
||||
|
||||
try:
|
||||
communicate = edge_tts.Communicate(**kwargs)
|
||||
await communicate.save(mp3_path)
|
||||
|
||||
# 使用ffmpeg将MP3转换为标准WAV格式
|
||||
_ = subprocess.run([
|
||||
"ffmpeg",
|
||||
"-y", # 覆盖输出文件
|
||||
"-i", mp3_path, # 输入文件
|
||||
"-acodec", "pcm_s16le", # 16位PCM编码
|
||||
"-ar", "24000", # 采样率24kHz (适合微信语音)
|
||||
"-ac", "1", # 单声道
|
||||
wav_path # 输出文件
|
||||
], capture_output=True, check=True)
|
||||
|
||||
|
||||
os.remove(mp3_path)
|
||||
if os.path.exists(wav_path) and os.path.getsize(wav_path) > 0:
|
||||
return wav_path
|
||||
else:
|
||||
logger.error("生成的WAV文件不存在或为空")
|
||||
raise RuntimeError("生成的WAV文件不存在或为空")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"FFmpeg转换失败: {e.stderr.decode() if e.stderr else str(e)}")
|
||||
try:
|
||||
if os.path.exists(mp3_path):
|
||||
os.remove(mp3_path)
|
||||
except:
|
||||
pass
|
||||
raise RuntimeError(f"FFmpeg转换失败: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"音频生成失败: {str(e)}")
|
||||
try:
|
||||
if os.path.exists(mp3_path):
|
||||
os.remove(mp3_path)
|
||||
except:
|
||||
pass
|
||||
raise RuntimeError(f"音频生成失败: {str(e)}")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from pip import main as pip_main
|
||||
|
||||
logger = logging.getLogger("astrbot")
|
||||
class PipInstaller():
|
||||
def __init__(self, pip_install_arg: str):
|
||||
self.pip_install_arg = pip_install_arg
|
||||
@@ -20,7 +21,7 @@ class PipInstaller():
|
||||
if self.pip_install_arg:
|
||||
args.extend(self.pip_install_arg.split())
|
||||
|
||||
print(f"Pip 包管理器: {' '.join(args)}")
|
||||
logger.info(f"Pip 包管理器: pip {' '.join(args)}")
|
||||
|
||||
result_code = pip_main(args)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
# What's Changed
|
||||
|
||||
1. ✨ 新增: add English README by @CAICAIIs
|
||||
2. ✨ 新增: perf: 优化网页录音 [#283](https://github.com/Soulter/AstrBot/issues/283) by @Fridemn
|
||||
3. ✨ 新增: 添加对于 Edge-TTS 的支持 [#471](https://github.com/Soulter/AstrBot/issues/471) by @Fridemn
|
||||
4. ⚡ 优化: 为防止输入一大堆 k,改 k 键为 Ctrl 键;改为长按录音,松手结束;为防止误触改为只有点击输入框之后才会生效 by @Fridemn
|
||||
5. ⚡ 优化: 插件市场非列表视图能够正常搜索 [#640](https://github.com/Soulter/AstrBot/issues/640) by @Fridemn
|
||||
6. ⚡ 优化: 插件市场帮助按钮 tooltip 移入时会消失无法点击其中链接,更改为按钮触发 by @Quirrel-zh
|
||||
7. ‼️‼️ 🐛 修复: v3.4.32 无法记忆历史的会话 [#630](https://github.com/Soulter/AstrBot/issues/630)
|
||||
8. ‼️🐛 修复: 钩子函数无法终止事件传播的问题;修复某些情况下终止事件传播后仍然会请求 LLM 的问题
|
||||
9. ‼️🐛 修复: OneBot V11 通知类事件某些情况无法回复问题 by @CAICAIIs
|
||||
10. 🐛 修复: Correct STT model path and improve logging in provider manager and pip installer
|
||||
11. 🐛 修复: 由于已安装插件与插件市场中 name 不一致或 repo 链接大小写不一致导致的检测不到是否安装或有更新 by @Quirrel-zh
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,10 +60,10 @@ marked.setOptions({
|
||||
<span>获取帮助 😊</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #aaa;">
|
||||
<span>按</span>
|
||||
<span>长按</span>
|
||||
<span
|
||||
style="background-color: #eee; padding-left: 4px; padding-right: 4px; margin: 2px; border-radius: 4px;">K</span>
|
||||
<span>开始语音 🎤</span>
|
||||
style="background-color: #eee; padding-left: 4px; padding-right: 4px; margin: 2px; border-radius: 4px;">Ctrl</span>
|
||||
<span>录制语音 🎤</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #aaa;">
|
||||
<span>按</span>
|
||||
@@ -112,7 +112,8 @@ marked.setOptions({
|
||||
|
||||
<v-text-field id="input-field" variant="outlined" v-model="prompt" :label="inputFieldLabel"
|
||||
placeholder="Start typing..." loading clear-icon="mdi-close-circle" clearable
|
||||
@click:clear="clearMessage" style="width: 100%; max-width: 850px;">
|
||||
@click:clear="clearMessage" style="width: 100%; max-width: 850px;"
|
||||
@keydown="handleInputKeyDown">
|
||||
<template v-slot:loader>
|
||||
<v-progress-linear :active="loadingChat" height="6"
|
||||
indeterminate></v-progress-linear>
|
||||
@@ -189,7 +190,12 @@ export default {
|
||||
status: {},
|
||||
statusText: '',
|
||||
|
||||
eventSource: null
|
||||
eventSource: null,
|
||||
|
||||
// 添加Ctrl键长按相关变量
|
||||
ctrlKeyDown: false,
|
||||
ctrlKeyTimer: null,
|
||||
ctrlKeyLongPressThreshold: 300 // 长按阈值,单位毫秒
|
||||
}
|
||||
},
|
||||
|
||||
@@ -205,11 +211,9 @@ export default {
|
||||
this.sendMessage();
|
||||
}
|
||||
}.bind(this));
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.keyCode == 75) {
|
||||
this.isRecording ? this.stopRecording() : this.startRecording();
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
// 添加keyup事件监听
|
||||
document.addEventListener('keyup', this.handleInputKeyUp);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
@@ -218,6 +222,9 @@ export default {
|
||||
this.eventSource.cancel();
|
||||
console.log('SSE连接已断开');
|
||||
}
|
||||
|
||||
// 移除keyup事件监听
|
||||
document.removeEventListener('keyup', this.handleInputKeyUp);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -531,7 +538,40 @@ export default {
|
||||
const container = this.$refs.messageContainer;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleInputKeyDown(e) {
|
||||
if (e.keyCode === 17) { // Ctrl键
|
||||
// 防止重复触发
|
||||
if (this.ctrlKeyDown) return;
|
||||
|
||||
this.ctrlKeyDown = true;
|
||||
|
||||
// 设置定时器识别长按
|
||||
this.ctrlKeyTimer = setTimeout(() => {
|
||||
if (this.ctrlKeyDown && !this.isRecording) {
|
||||
this.startRecording();
|
||||
}
|
||||
}, this.ctrlKeyLongPressThreshold);
|
||||
}
|
||||
},
|
||||
|
||||
handleInputKeyUp(e) {
|
||||
if (e.keyCode === 17) { // Ctrl键
|
||||
this.ctrlKeyDown = false;
|
||||
|
||||
// 清除定时器
|
||||
if (this.ctrlKeyTimer) {
|
||||
clearTimeout(this.ctrlKeyTimer);
|
||||
this.ctrlKeyTimer = null;
|
||||
}
|
||||
|
||||
// 如果正在录音,停止录音
|
||||
if (this.isRecording) {
|
||||
this.stopRecording();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import axios from 'axios';
|
||||
import { max } from 'date-fns';
|
||||
|
||||
</script>
|
||||
|
||||
@@ -94,12 +93,15 @@ import { max } from 'date-fns';
|
||||
|
||||
🧩 插件市场
|
||||
|
||||
<v-btn icon size="small" style="margin-left: 8px" variant="plain">
|
||||
<v-btn icon size="small" style="margin-left: 8px" variant="plain" @click="jumpToPluginMarket()">
|
||||
<v-icon size="small">mdi-help</v-icon>
|
||||
<v-tooltip activator="parent" location="start">
|
||||
如无法显示,请打开 <a href="https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json">链接</a> 复制想安装插件对应的
|
||||
<span>
|
||||
如无法显示,请单击此按钮跳转至插件市场,复制想安装插件对应的
|
||||
`repo`
|
||||
链接然后点击右下角 + 号安装,或打开链接下载压缩包安装。
|
||||
</span>
|
||||
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
@@ -141,7 +143,7 @@ import { max } from 'date-fns';
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-row style="margin: 8px;">
|
||||
<v-col cols="12" md="6" lg="3" v-for="plugin in pluginMarketData">
|
||||
<v-col cols="12" md="6" lg="3" v-for="plugin in filteredPluginMarketData">
|
||||
<ExtensionCard :key="plugin.name" :title="plugin.name" :link="plugin.repo" style="margin-bottom: 4px;">
|
||||
<div style="min-height: 130px; max-height: 130px; overflow: hidden;">
|
||||
<p style="font-weight: bold;">By @{{ plugin.author }}</p>
|
||||
@@ -380,6 +382,17 @@ export default {
|
||||
alreadyCheckUpdate: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredPluginMarketData() {
|
||||
if (!this.marketSearch) {
|
||||
return this.pluginMarketData;
|
||||
}
|
||||
const search = this.marketSearch.toLowerCase();
|
||||
return this.pluginMarketData.filter(plugin =>
|
||||
plugin.name.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getExtensions();
|
||||
this.fetchPluginCollection();
|
||||
@@ -390,6 +403,9 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
jumpToPluginMarket() {
|
||||
window.open('https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json', '_blank');
|
||||
},
|
||||
toast(message, success) {
|
||||
this.snack_message = message;
|
||||
this.snack_show = true;
|
||||
@@ -422,21 +438,34 @@ export default {
|
||||
},
|
||||
|
||||
checkUpdate() {
|
||||
// 遍历 extension_data 和 pluginMarketData,检查是否有更新\
|
||||
for (let i = 0; i < this.extension_data.data.length; i++) {
|
||||
for (let j = 0; j < this.pluginMarketData.length; j++) {
|
||||
console.log(this.extension_data.data[i].repo, this.pluginMarketData[j].repo);
|
||||
if (this.extension_data.data[i].repo === this.pluginMarketData[j].repo ||
|
||||
this.extension_data.data[i].name === this.pluginMarketData[j].name) {
|
||||
this.extension_data.data[i].online_version = this.pluginMarketData[j].version;
|
||||
if (this.extension_data.data[i].version !== this.pluginMarketData[j].version && this.pluginMarketData[j].version !== "未知") {
|
||||
this.extension_data.data[i].has_update = true;
|
||||
} else {
|
||||
this.extension_data.data[i].has_update = false;
|
||||
}
|
||||
}
|
||||
// 创建在线插件的map
|
||||
const onlinePluginsMap = new Map();
|
||||
const onlinePluginsNameMap = new Map();
|
||||
|
||||
// 将在线插件信息存储到map中
|
||||
this.pluginMarketData.forEach(plugin => {
|
||||
if (plugin.repo) {
|
||||
onlinePluginsMap.set(plugin.repo.toLowerCase(), plugin);
|
||||
}
|
||||
}
|
||||
onlinePluginsNameMap.set(plugin.name, plugin);
|
||||
});
|
||||
|
||||
// 遍历本地插件列表
|
||||
this.extension_data.data.forEach(extension => {
|
||||
// 通过repo或name查找在线版本
|
||||
const repoKey = extension.repo?.toLowerCase();
|
||||
const onlinePlugin = repoKey ? onlinePluginsMap.get(repoKey) : null;
|
||||
const onlinePluginByName = onlinePluginsNameMap.get(extension.name);
|
||||
const matchedPlugin = onlinePlugin || onlinePluginByName;
|
||||
|
||||
if (matchedPlugin) {
|
||||
extension.online_version = matchedPlugin.version;
|
||||
extension.has_update = extension.version !== matchedPlugin.version &&
|
||||
matchedPlugin.version !== "未知";
|
||||
} else {
|
||||
extension.has_update = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
newExtension() {
|
||||
@@ -613,15 +642,14 @@ export default {
|
||||
});
|
||||
},
|
||||
checkAlreadyInstalled() {
|
||||
// 创建已安装插件的仓库和名称集合 统一格式
|
||||
const installedRepos = new Set(this.extension_data.data.map(ext => ext.repo?.toLowerCase()));
|
||||
const installedNames = new Set(this.extension_data.data.map(ext => ext.name));
|
||||
|
||||
// 遍历检查安装状态
|
||||
for (let i = 0; i < this.pluginMarketData.length; i++) {
|
||||
this.pluginMarketData[i].installed = false;
|
||||
}
|
||||
for (let i = 0; i < this.pluginMarketData.length; i++) {
|
||||
for (let j = 0; j < this.extension_data.data.length; j++) {
|
||||
if (this.pluginMarketData[i].repo === this.extension_data.data[j].repo || this.pluginMarketData[i].name === this.extension_data.data[j].name) {
|
||||
this.pluginMarketData[i].installed = true;
|
||||
}
|
||||
}
|
||||
const plugin = this.pluginMarketData[i];
|
||||
plugin.installed = installedRepos.has(plugin.repo?.toLowerCase()) || installedNames.has(plugin.name);
|
||||
}
|
||||
|
||||
// 将已安装的插件移动到最后面
|
||||
|
||||
@@ -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