Compare commits

...

52 Commits

Author SHA1 Message Date
Soulter bcebd0fb62 v3.4.33 2025-02-28 22:13:08 +08:00
Soulter 3817d3ca87 fix: 不记忆历史的会话 #630 2025-02-28 22:00:49 +08:00
Soulter 4dd714e814 Merge pull request #648 from Soulter/feat-edge-tts
feat: 添加对于 edge-tts 支持 #471
2025-02-28 21:46:50 +08:00
Soulter 61e8bb49ec chore: Cleanup 2025-02-28 21:33:03 +08:00
Soulter 103dcd3761 Merge pull request #645 from Quirrel-zh/master
修复&优化
2025-02-28 21:24:54 +08:00
Soulter 54ac135fc8 Merge pull request #642 from CAICAIIs/fix_bug
fix bug #621
2025-02-28 21:12:37 +08:00
Soulter 86582809fc Merge pull request #641 from Soulter/perf-plugin-search
perf: 插件市场非列表视图能够正常搜索 #640
2025-02-28 21:11:43 +08:00
Soulter 974d648f19 Merge pull request #638 from Soulter/perf-record
perf: 优化网页录音 #283
2025-02-28 21:10:09 +08:00
崔永亮 a79afc9597 feat: 添加对于 edge-tts 支持 #471 2025-02-28 16:57:44 +08:00
quirrel-zh e4883241d9 🐛fixed:
1、由于tooltip移入时会消失无法点击其中链接,更改为按钮出发
	2、修复了由于已安装插件与插件市场中name不一致或repo链接大小写不一致导致的检测不到是否安装或有更新的bug
2025-02-28 15:58:57 +08:00
yxw babf223745 fix bug #621 2025-02-28 14:22:59 +08:00
崔永亮 c7d91730b6 perf: 插件市场非列表视图能够正常搜索 #640 2025-02-28 14:18:10 +08:00
Soulter 71246b65c9 Update README.md 2025-02-28 14:06:31 +08:00
Soulter 50076b647e Merge pull request #639 from CAICAIIs/master
docs: add English README
2025-02-28 14:06:00 +08:00
yxw a1a788dce8 docs: add English README 2025-02-28 13:39:45 +08:00
崔永亮 a611b4f346 perf: 优化网页录音 #283
1. 为防止输入一大堆 k,改 k 键为 Ctrl 键;
2. 改为长按录音,松手结束;
3. 为防止误触改为只有点击输入框之后才会生效
2025-02-28 13:22:55 +08:00
Soulter 7f6ed674b4 ‼️🐛 fix: 修复钩子函数无法终止事件传播的问题;修复某些情况下终止事件传播后仍然会请求 LLM 的问题 2025-02-28 00:02:17 +08:00
Soulter aa3cfd887a fix: correct STT model path and improve logging in provider manager and pip installer 2025-02-27 11:33:53 +08:00
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
37 changed files with 1328 additions and 121 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
+1
View File
@@ -18,6 +18,7 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fstats&query=v&label=7%E6%97%A5%E6%B6%88%E6%81%AF%E4%B8%8A%E8%A1%8C%E9%87%8F&cacheSeconds=3600)
[![codecov](https://codecov.io/gh/Soulter/AstrBot/graph/badge.svg?token=FF3P5967B8)](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
View File
@@ -0,0 +1,164 @@
<p align="center">
![6e1279651f16d7fdf4727558b72bbaf1](https://github.com/user-attachments/assets/ead4c551-fc3c-48f7-a6f7-afbfdb820512)
</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>
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Soulter/AstrBot)](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">
[![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e.svg)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fstats&query=v&label=7%E6%97%A5%E6%B6%88%E6%81%AF%E4%B8%8A%E8%A1%8C%E9%87%8F&cacheSeconds=3600)
[![codecov](https://codecov.io/gh/Soulter/AstrBot/graph/badge.svg?token=FF3P5967B8)](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
[![Run on Repl.it](https://repl.it/badge/github/Soulter/AstrBot)](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 ✨_
![webchat](https://drive.soulter.top/f/vlsA/ezgif-5-fb044b2542.gif)
_✨ Built-in Web Chat Interface ✨_
</div>
## ⭐ Star History
> [!TIP]
> If this project helps you, please give it a star <3
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=soulter/astrbot&type=Date)](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
-->
_私は、高性能ですから!_
+91 -36
View File
@@ -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": "是否开启情绪识别。happysadangryneutralfearfuldisgustedsurprisedunknown",
},
"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": {
+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'
@@ -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
+4
View File
@@ -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()
+3
View File
@@ -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
+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)
@@ -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)
+19 -5
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,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)}")
+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 -1
View File
@@ -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)
+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
+13
View File
@@ -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',
+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]
}
}
});
+51 -11
View File
@@ -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();
}
}
},
},
}
+54 -26
View File
@@ -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);
}
// 将已安装的插件移动到最后面
+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