Merge remote-tracking branch 'origin/master' into RC-CHN/master
This commit is contained in:
@@ -144,7 +144,7 @@ uvx astrbot init
|
||||
|
||||
| 名称 | 支持性 | 类型 | 备注 |
|
||||
| -------- | ------- | ------- | ------- |
|
||||
| OpenAI API | ✔ | 文本生成 | 也支持 DeepSeek、Google Gemini、GLM、Kimi、xAI 等兼容 OpenAI API 的服务 |
|
||||
| OpenAI API | ✔ | 文本生成 | 也支持 DeepSeek、Gemini、Kimi、xAI 等兼容 OpenAI API 的服务 |
|
||||
| Claude API | ✔ | 文本生成 | |
|
||||
| Google Gemini API | ✔ | 文本生成 | |
|
||||
| Dify | ✔ | LLMOps | |
|
||||
@@ -152,6 +152,8 @@ uvx astrbot init
|
||||
| Ollama | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
|
||||
| LM Studio | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
|
||||
| LLMTuner | ✔ | 模型加载器 | 本地加载 lora 等微调模型 |
|
||||
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | ✔ | 模型 API 及算力服务平台 | |
|
||||
| [302.AI](https://share.302.ai/rr1M3l) | ✔ | 模型 API 服务平台 | |
|
||||
| 硅基流动 | ✔ | 模型 API 服务平台 | |
|
||||
| PPIO 派欧云 | ✔ | 模型 API 服务平台 | |
|
||||
| OneAPI | ✔ | LLM 分发系统 | |
|
||||
|
||||
+45
-171
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "3.5.19"
|
||||
VERSION = "3.5.22"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -318,8 +318,7 @@ CONFIG_METADATA_2 = {
|
||||
"id": {
|
||||
"description": "机器人名称",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "机器人名称(ID)不能和其它的平台适配器重复。",
|
||||
"hint": "机器人名称",
|
||||
},
|
||||
"type": {
|
||||
"description": "适配器类型",
|
||||
@@ -370,7 +369,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "飞书机器人的名字",
|
||||
"type": "string",
|
||||
"hint": "请务必填对,否则 @ 机器人将无法唤醒,只能通过前缀唤醒。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"discord_token": {
|
||||
"description": "Discord Bot Token",
|
||||
@@ -486,13 +484,11 @@ CONFIG_METADATA_2 = {
|
||||
"regex": {
|
||||
"description": "正则表达式",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。re.findall(r'<regex>', text)",
|
||||
},
|
||||
"content_cleanup_rule": {
|
||||
"description": "过滤分段后的内容",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "移除分段后的内容中的指定的内容。支持正则表达式。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.sub(r'<regex>', '', text)",
|
||||
},
|
||||
},
|
||||
@@ -515,7 +511,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "ID 白名单",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"obvious_hint": True,
|
||||
"hint": "只处理填写的 ID 发来的消息事件,为空时不启用。可使用 /sid 指令获取在平台上的会话 ID(类似 abc:GroupMessage:123)。管理员可使用 /wl 添加白名单",
|
||||
},
|
||||
"id_whitelist_log": {
|
||||
@@ -545,7 +540,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "路径映射",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"obvious_hint": True,
|
||||
"hint": "此功能解决由于文件系统不一致导致路径不存在的问题。格式为 <原路径>:<映射路径>。如 `/app/.config/QQ:/var/lib/docker/volumes/xxxx/_data`。这样,当消息平台下发的事件中图片和语音路径以 `/app/.config/QQ` 开头时,开头被替换为 `/var/lib/docker/volumes/xxxx/_data`。这在 AstrBot 或者平台协议端使用 Docker 部署时特别有用。",
|
||||
},
|
||||
},
|
||||
@@ -605,6 +599,7 @@ CONFIG_METADATA_2 = {
|
||||
"config_template": {
|
||||
"OpenAI": {
|
||||
"id": "openai",
|
||||
"provider": "openai",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -617,6 +612,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Azure OpenAI": {
|
||||
"id": "azure",
|
||||
"provider": "azure",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -630,6 +626,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"xAI": {
|
||||
"id": "xai",
|
||||
"provider": "xai",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -642,6 +639,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Anthropic": {
|
||||
"id": "claude",
|
||||
"provider": "anthropic",
|
||||
"type": "anthropic_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -655,6 +653,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Ollama": {
|
||||
"id": "ollama_default",
|
||||
"provider": "ollama",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -666,6 +665,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"LM Studio": {
|
||||
"id": "lm_studio",
|
||||
"provider": "lm_studio",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -677,6 +677,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Gemini(OpenAI兼容)": {
|
||||
"id": "gemini_default",
|
||||
"provider": "google",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -689,6 +690,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Gemini": {
|
||||
"id": "gemini_default",
|
||||
"provider": "google",
|
||||
"type": "googlegenai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -714,6 +716,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"DeepSeek": {
|
||||
"id": "deepseek_default",
|
||||
"provider": "deepseek",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -726,6 +729,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"302.AI": {
|
||||
"id": "302ai",
|
||||
"provider": "302ai",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -738,6 +742,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"硅基流动": {
|
||||
"id": "siliconflow",
|
||||
"provider": "siliconflow",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -750,6 +755,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"PPIO派欧云": {
|
||||
"id": "ppio",
|
||||
"provider": "ppio",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -762,6 +768,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Kimi": {
|
||||
"id": "moonshot",
|
||||
"provider": "moonshot",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -774,6 +781,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"智谱 AI": {
|
||||
"id": "zhipu_default",
|
||||
"provider": "zhipu",
|
||||
"type": "zhipu_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -786,6 +794,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Dify": {
|
||||
"id": "dify_app_default",
|
||||
"provider": "dify",
|
||||
"type": "dify",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -799,6 +808,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"阿里云百炼应用": {
|
||||
"id": "dashscope",
|
||||
"provider": "dashscope",
|
||||
"type": "dashscope",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -815,6 +825,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"FastGPT": {
|
||||
"id": "fastgpt",
|
||||
"provider": "fastgpt",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
@@ -824,6 +835,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Whisper(API)": {
|
||||
"id": "whisper",
|
||||
"provider": "openai",
|
||||
"type": "openai_whisper_api",
|
||||
"provider_type": "speech_to_text",
|
||||
"enable": False,
|
||||
@@ -833,15 +845,17 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"Whisper(本地加载)": {
|
||||
"whisper_hint": "(不用修改我)",
|
||||
"provider": "openai",
|
||||
"type": "openai_whisper_selfhost",
|
||||
"provider_type": "speech_to_text",
|
||||
"enable": False,
|
||||
"id": "whisper",
|
||||
"id": "whisper_selfhost",
|
||||
"model": "tiny",
|
||||
},
|
||||
"SenseVoice(本地加载)": {
|
||||
"sensevoice_hint": "(不用修改我)",
|
||||
"type": "sensevoice_stt_selfhost",
|
||||
"provider": "sensevoice",
|
||||
"provider_type": "speech_to_text",
|
||||
"enable": False,
|
||||
"id": "sensevoice",
|
||||
@@ -851,6 +865,7 @@ CONFIG_METADATA_2 = {
|
||||
"OpenAI TTS(API)": {
|
||||
"id": "openai_tts",
|
||||
"type": "openai_tts_api",
|
||||
"provider": "openai",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": False,
|
||||
"api_key": "",
|
||||
@@ -862,6 +877,7 @@ CONFIG_METADATA_2 = {
|
||||
"Edge TTS": {
|
||||
"edgetts_hint": "提示:使用这个服务前需要安装有 ffmpeg,并且可以直接在终端调用 ffmpeg 指令。",
|
||||
"id": "edge_tts",
|
||||
"provider": "microsoft",
|
||||
"type": "edge_tts",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": False,
|
||||
@@ -871,6 +887,7 @@ CONFIG_METADATA_2 = {
|
||||
"GSV TTS(本地加载)": {
|
||||
"id": "gsv_tts",
|
||||
"enable": False,
|
||||
"provider": "gpt_sovits",
|
||||
"type": "gsv_tts_selfhost",
|
||||
"provider_type": "text_to_speech",
|
||||
"api_base": "http://127.0.0.1:9880",
|
||||
@@ -902,6 +919,7 @@ CONFIG_METADATA_2 = {
|
||||
"GSVI TTS(API)": {
|
||||
"id": "gsvi_tts",
|
||||
"type": "gsvi_tts_api",
|
||||
"provider": "gpt_sovits_inference",
|
||||
"provider_type": "text_to_speech",
|
||||
"api_base": "http://127.0.0.1:5000",
|
||||
"character": "",
|
||||
@@ -911,6 +929,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"FishAudio TTS(API)": {
|
||||
"id": "fishaudio_tts",
|
||||
"provider": "fishaudio",
|
||||
"type": "fishaudio_tts_api",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": False,
|
||||
@@ -921,6 +940,7 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
"阿里云百炼 TTS(API)": {
|
||||
"id": "dashscope_tts",
|
||||
"provider": "dashscope",
|
||||
"type": "dashscope_tts",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": False,
|
||||
@@ -932,6 +952,7 @@ CONFIG_METADATA_2 = {
|
||||
"Azure TTS": {
|
||||
"id": "azure_tts",
|
||||
"type": "azure_tts",
|
||||
"provider": "azure",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": True,
|
||||
"azure_tts_voice": "zh-CN-YunxiaNeural",
|
||||
@@ -945,6 +966,7 @@ CONFIG_METADATA_2 = {
|
||||
"MiniMax TTS(API)": {
|
||||
"id": "minimax_tts",
|
||||
"type": "minimax_tts_api",
|
||||
"provider": "minimax",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": False,
|
||||
"api_key": "",
|
||||
@@ -966,6 +988,7 @@ CONFIG_METADATA_2 = {
|
||||
"火山引擎_TTS(API)": {
|
||||
"id": "volcengine_tts",
|
||||
"type": "volcengine_tts",
|
||||
"provider": "volcengine",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": False,
|
||||
"api_key": "",
|
||||
@@ -979,6 +1002,7 @@ CONFIG_METADATA_2 = {
|
||||
"Gemini TTS": {
|
||||
"id": "gemini_tts",
|
||||
"type": "gemini_tts",
|
||||
"provider": "google",
|
||||
"provider_type": "text_to_speech",
|
||||
"enable": False,
|
||||
"gemini_tts_api_key": "",
|
||||
@@ -991,6 +1015,7 @@ CONFIG_METADATA_2 = {
|
||||
"OpenAI Embedding": {
|
||||
"id": "openai_embedding",
|
||||
"type": "openai_embedding",
|
||||
"provider": "openai",
|
||||
"provider_type": "embedding",
|
||||
"enable": True,
|
||||
"embedding_api_key": "",
|
||||
@@ -1002,6 +1027,7 @@ CONFIG_METADATA_2 = {
|
||||
"Gemini Embedding": {
|
||||
"id": "gemini_embedding",
|
||||
"type": "gemini_embedding",
|
||||
"provider": "google",
|
||||
"provider_type": "embedding",
|
||||
"enable": True,
|
||||
"embedding_api_key": "",
|
||||
@@ -1012,17 +1038,19 @@ CONFIG_METADATA_2 = {
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"invisible": True,
|
||||
},
|
||||
"gpt_weights_path": {
|
||||
"description": "GPT模型文件路径",
|
||||
"type": "string",
|
||||
"hint": "即“.ckpt”后缀的文件,请使用绝对路径,路径两端不要带双引号,不填则默认用GPT_SoVITS内置的SoVITS模型(建议直接在GPT_SoVITS中改默认模型)",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"sovits_weights_path": {
|
||||
"description": "SoVITS模型文件路径",
|
||||
"type": "string",
|
||||
"hint": "即“.pth”后缀的文件,请使用绝对路径,路径两端不要带双引号,不填则默认用GPT_SoVITS内置的SoVITS模型(建议直接在GPT_SoVITS中改默认模型)",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"gsv_default_parms": {
|
||||
"description": "GPT_SoVITS默认参数",
|
||||
@@ -1033,13 +1061,11 @@ CONFIG_METADATA_2 = {
|
||||
"description": "参考音频文件路径",
|
||||
"type": "string",
|
||||
"hint": "必填!请使用绝对路径!路径两端不要带双引号!",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"gsv_prompt_text": {
|
||||
"description": "参考音频文本",
|
||||
"type": "string",
|
||||
"hint": "必填!请填写参考音频讲述的文本",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"gsv_prompt_lang": {
|
||||
"description": "参考音频文本语言",
|
||||
@@ -1266,19 +1292,16 @@ CONFIG_METADATA_2 = {
|
||||
"description": "启用原生搜索功能",
|
||||
"type": "bool",
|
||||
"hint": "启用后所有函数工具将全部失效,免费次数限制请查阅官方文档",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"gm_native_coderunner": {
|
||||
"description": "启用原生代码执行器",
|
||||
"type": "bool",
|
||||
"hint": "启用后所有函数工具将全部失效",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"gm_url_context": {
|
||||
"description": "启用URL上下文功能",
|
||||
"type": "bool",
|
||||
"hint": "启用后所有函数工具将全部失效",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"gm_safety_settings": {
|
||||
"description": "安全过滤器",
|
||||
@@ -1462,7 +1485,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "部署SenseVoice",
|
||||
"type": "string",
|
||||
"hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"is_emotion": {
|
||||
"description": "情绪识别",
|
||||
@@ -1477,18 +1499,10 @@ CONFIG_METADATA_2 = {
|
||||
"variables": {
|
||||
"description": "工作流固定输入变量",
|
||||
"type": "object",
|
||||
"obvious_hint": True,
|
||||
"items": {},
|
||||
"hint": "可选。工作流固定输入变量,将会作为工作流的输入。也可以在对话时使用 /set 指令动态设置变量。如果变量名冲突,优先使用动态设置的变量。",
|
||||
"invisible": True,
|
||||
},
|
||||
# "fastgpt_app_type": {
|
||||
# "description": "应用类型",
|
||||
# "type": "string",
|
||||
# "hint": "FastGPT 应用的应用类型。",
|
||||
# "options": ["agent", "workflow", "plugin"],
|
||||
# "obvious_hint": True,
|
||||
# },
|
||||
"dashscope_app_type": {
|
||||
"description": "应用类型",
|
||||
"type": "string",
|
||||
@@ -1499,7 +1513,6 @@ CONFIG_METADATA_2 = {
|
||||
"dialog-workflow",
|
||||
"task-workflow",
|
||||
],
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"timeout": {
|
||||
"description": "超时时间",
|
||||
@@ -1509,26 +1522,22 @@ CONFIG_METADATA_2 = {
|
||||
"openai-tts-voice": {
|
||||
"description": "voice",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "OpenAI TTS 的声音。OpenAI 默认支持:'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'",
|
||||
},
|
||||
"fishaudio-tts-character": {
|
||||
"description": "character",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "fishaudio TTS 的角色。默认为可莉。更多角色请访问:https://fish.audio/zh-CN/discovery",
|
||||
},
|
||||
"whisper_hint": {
|
||||
"description": "本地部署 Whisper 模型须知",
|
||||
"type": "string",
|
||||
"hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"id": {
|
||||
"description": "ID",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "ID 不能和其它的服务提供商重复,否则将发生严重冲突。",
|
||||
"hint": "模型提供商名字。",
|
||||
},
|
||||
"type": {
|
||||
"description": "模型提供商种类",
|
||||
@@ -1543,53 +1552,27 @@ CONFIG_METADATA_2 = {
|
||||
"enable": {
|
||||
"description": "启用",
|
||||
"type": "bool",
|
||||
"hint": "是否启用该模型。未启用的模型将不会被使用。",
|
||||
"hint": "是否启用。",
|
||||
},
|
||||
"key": {
|
||||
"description": "API Key",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"hint": "API Key 列表。填写好后输入回车即可添加 API Key。支持多个 API Key。",
|
||||
"hint": "提供商 API Key。",
|
||||
},
|
||||
"api_base": {
|
||||
"description": "API Base URL",
|
||||
"type": "string",
|
||||
"hint": "API Base URL 请在在模型提供商处获得。如出现 404 报错,尝试在地址末尾加上 /v1",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"base_model_path": {
|
||||
"description": "基座模型路径",
|
||||
"type": "string",
|
||||
"hint": "基座模型路径。",
|
||||
},
|
||||
"adapter_model_path": {
|
||||
"description": "Adapter 模型路径",
|
||||
"type": "string",
|
||||
"hint": "Adapter 模型路径。如 Lora",
|
||||
},
|
||||
"llmtuner_template": {
|
||||
"description": "template",
|
||||
"type": "string",
|
||||
"hint": "基座模型的类型。如 llama3, qwen, 请参考 LlamaFactory 文档。",
|
||||
},
|
||||
"finetuning_type": {
|
||||
"description": "微调类型",
|
||||
"type": "string",
|
||||
"hint": "微调类型。如 `lora`",
|
||||
},
|
||||
"quantization_bit": {
|
||||
"description": "量化位数",
|
||||
"type": "int",
|
||||
"hint": "量化位数。如 4",
|
||||
"hint": "API Base URL 请在模型提供商处获得。如出现 404 报错,尝试在地址末尾加上 /v1",
|
||||
},
|
||||
"model_config": {
|
||||
"description": "文本生成模型",
|
||||
"description": "模型配置",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"model": {
|
||||
"description": "模型名称",
|
||||
"type": "string",
|
||||
"hint": "大语言模型的名称,一般是小写的英文。如 gpt-4o-mini, deepseek-chat 等。",
|
||||
"hint": "模型名称,如 gpt-4o-mini, deepseek-chat。",
|
||||
},
|
||||
"max_tokens": {
|
||||
"description": "模型最大输出长度(tokens)",
|
||||
@@ -1636,7 +1619,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "启用大语言模型聊天",
|
||||
"type": "bool",
|
||||
"hint": "如需切换大语言模型提供商,请使用 /provider 命令。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"separate_provider": {
|
||||
"description": "提供商会话隔离",
|
||||
@@ -1656,13 +1638,11 @@ CONFIG_METADATA_2 = {
|
||||
"web_search": {
|
||||
"description": "启用网页搜索",
|
||||
"type": "bool",
|
||||
"obvious_hint": True,
|
||||
"hint": "能访问 Google 时效果最佳(国内需要在 `其他配置` 开启 HTTP 代理)。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。",
|
||||
},
|
||||
"web_search_link": {
|
||||
"description": "网页搜索引用链接",
|
||||
"type": "bool",
|
||||
"obvious_hint": True,
|
||||
"hint": "开启后,将会传入网页搜索结果的链接给模型,并引导模型输出引用链接。",
|
||||
},
|
||||
"display_reasoning_text": {
|
||||
@@ -1673,13 +1653,11 @@ CONFIG_METADATA_2 = {
|
||||
"identifier": {
|
||||
"description": "启动识别群员",
|
||||
"type": "bool",
|
||||
"obvious_hint": True,
|
||||
"hint": "在 Prompt 前加上群成员的名字以让模型更好地了解群聊状态。启用将略微增加 token 开销。",
|
||||
},
|
||||
"datetime_system_prompt": {
|
||||
"description": "启用日期时间系统提示",
|
||||
"type": "bool",
|
||||
"obvious_hint": True,
|
||||
"hint": "启用后,会在系统提示词中加上当前机器的日期时间。",
|
||||
},
|
||||
"default_personality": {
|
||||
@@ -1736,7 +1714,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "人格名称",
|
||||
"type": "string",
|
||||
"hint": "人格名称,用于在多个人格中区分。使用 /persona 指令可切换人格。在 大语言模型设置 处可以设置默认人格。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"prompt": {
|
||||
"description": "设定(系统提示词)",
|
||||
@@ -1748,14 +1725,12 @@ CONFIG_METADATA_2 = {
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"hint": "可选。在每个对话前会插入这些预设对话。对话需要成对(用户和助手),输入完一个角色的内容之后按【回车】。需要偶数个对话",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"mood_imitation_dialogs": {
|
||||
"description": "对话风格模仿",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"hint": "旨在让模型尽可能模仿学习到所填写的对话的语气风格。格式和 `预设对话` 一致。对话需要成对(用户和助手),输入完一个角色的内容之后按【回车】。需要偶数个对话",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1767,7 +1742,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "启用语音转文本(STT)",
|
||||
"type": "bool",
|
||||
"hint": "启用前请在 服务提供商配置 处创建支持 语音转文本任务 的提供商。如 whisper。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "提供商 ID",
|
||||
@@ -1784,7 +1758,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "启用文本转语音(TTS)",
|
||||
"type": "bool",
|
||||
"hint": "启用前请在 服务提供商配置 处创建支持 语音转文本任务 的提供商。如 openai_tts。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"provider_id": {
|
||||
"description": "提供商 ID",
|
||||
@@ -1795,7 +1768,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "启用语音和文字双输出",
|
||||
"type": "bool",
|
||||
"hint": "启用后,Bot 将同时输出语音和文字消息。",
|
||||
"obvious_hint": True,
|
||||
},
|
||||
"use_file_service": {
|
||||
"description": "使用文件服务提供 TTS 语音文件",
|
||||
@@ -1811,25 +1783,21 @@ CONFIG_METADATA_2 = {
|
||||
"group_icl_enable": {
|
||||
"description": "群聊内记录各群员对话",
|
||||
"type": "bool",
|
||||
"obvious_hint": True,
|
||||
"hint": "启用后,会记录群聊内各群员的对话。使用 /reset 命令清除记录。推荐使用 gpt-4o-mini 模型。",
|
||||
},
|
||||
"group_message_max_cnt": {
|
||||
"description": "群聊消息最大数量",
|
||||
"type": "int",
|
||||
"obvious_hint": True,
|
||||
"hint": "群聊消息最大数量。超过此数量后,会自动清除旧消息。",
|
||||
},
|
||||
"image_caption": {
|
||||
"description": "群聊图像转述(需模型支持)",
|
||||
"type": "bool",
|
||||
"obvious_hint": True,
|
||||
"hint": "用模型将群聊中的图片消息转述为文字,推荐 gpt-4o-mini 模型。和机器人的唤醒聊天中的图片消息仍然会直接作为上下文输入。",
|
||||
},
|
||||
"image_caption_provider_id": {
|
||||
"description": "图像转述提供商 ID",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "可选。图像转述提供商 ID。如为空将选择聊天使用的提供商。",
|
||||
},
|
||||
"image_caption_prompt": {
|
||||
@@ -1843,14 +1811,12 @@ CONFIG_METADATA_2 = {
|
||||
"enable": {
|
||||
"description": "启用主动回复",
|
||||
"type": "bool",
|
||||
"obvious_hint": True,
|
||||
"hint": "启用后,会根据触发概率主动回复群聊内的对话。QQ官方API(qq_official)不可用",
|
||||
},
|
||||
"whitelist": {
|
||||
"description": "主动回复白名单",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"obvious_hint": True,
|
||||
"hint": "启用后,只有在白名单内的群聊会被主动回复。为空时不启用白名单过滤。需要通过 /sid 获取 SID 添加到这里。",
|
||||
},
|
||||
"method": {
|
||||
@@ -1862,13 +1828,11 @@ CONFIG_METADATA_2 = {
|
||||
"possibility_reply": {
|
||||
"description": "回复概率",
|
||||
"type": "float",
|
||||
"obvious_hint": True,
|
||||
"hint": "回复概率。当回复方法为 possibility_reply 时有效。当概率 >= 1 时,每条消息都会回复。",
|
||||
},
|
||||
"prompt": {
|
||||
"description": "提示词",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "提示词。当提示词为空时,如果触发回复,则向 LLM 请求的是触发的消息的内容;否则是提示词。此项可以和定时回复(暂未实现)配合使用。",
|
||||
},
|
||||
},
|
||||
@@ -1884,7 +1848,6 @@ CONFIG_METADATA_2 = {
|
||||
"description": "机器人唤醒前缀",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"obvious_hint": True,
|
||||
"hint": "在不 @ 机器人的情况下,可以通过外加消息前缀来唤醒机器人。更改此配置将影响整个 Bot 的功能唤醒,包括所有指令。如果您不保留 `/`,则内置指令(help等)将需要通过您的唤醒前缀来触发。",
|
||||
},
|
||||
"t2i": {
|
||||
@@ -1911,13 +1874,11 @@ CONFIG_METADATA_2 = {
|
||||
"timezone": {
|
||||
"description": "时区",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab",
|
||||
},
|
||||
"callback_api_base": {
|
||||
"description": "对外可达的回调接口地址",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定 “外部服务如何访问 AstrBot” 的地址。如 http://localhost:6185,https://example.com 等。",
|
||||
},
|
||||
"log_level": {
|
||||
@@ -1965,90 +1926,3 @@ DEFAULT_VALUE_MAP = {
|
||||
"list": [],
|
||||
"object": {},
|
||||
}
|
||||
|
||||
|
||||
# "project_atri": {
|
||||
# "description": "Project ATRI 配置",
|
||||
# "type": "object",
|
||||
# "items": {
|
||||
# "enable": {"description": "启用", "type": "bool"},
|
||||
# "long_term_memory": {
|
||||
# "description": "长期记忆",
|
||||
# "type": "object",
|
||||
# "items": {
|
||||
# "enable": {"description": "启用", "type": "bool"},
|
||||
# "summary_threshold_cnt": {
|
||||
# "description": "摘要阈值",
|
||||
# "type": "int",
|
||||
# "hint": "当一个会话的对话记录数量超过该阈值时,会自动进行摘要。",
|
||||
# },
|
||||
# "embedding_provider_id": {
|
||||
# "description": "Embedding provider ID",
|
||||
# "type": "string",
|
||||
# "hint": "只有当启用了长期记忆时,才需要填写此项。将会使用指定的 provider 来获取 Embedding,请确保所填的 provider id 在 `配置页` 中存在并且设置了 Embedding 配置",
|
||||
# "obvious_hint": True,
|
||||
# },
|
||||
# "summarize_provider_id": {
|
||||
# "description": "Summary provider ID",
|
||||
# "type": "string",
|
||||
# "hint": "只有当启用了长期记忆时,才需要填写此项。将会使用指定的 provider 来获取 Summary,请确保所填的 provider id 在 `配置页` 中存在。",
|
||||
# "obvious_hint": True,
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
# "active_message": {
|
||||
# "description": "主动消息",
|
||||
# "type": "object",
|
||||
# "items": {
|
||||
# "enable": {"description": "启用", "type": "bool"},
|
||||
# },
|
||||
# },
|
||||
# "vision": {
|
||||
# "description": "视觉理解",
|
||||
# "type": "object",
|
||||
# "items": {
|
||||
# "enable": {"description": "启用", "type": "bool"},
|
||||
# "provider_id_or_ofa_model_path": {
|
||||
# "description": "提供商 ID 或 OFA 模型路径",
|
||||
# "type": "string",
|
||||
# "hint": "将会使用指定的 provider 来进行视觉处理,请确保所填的 provider id 在 `配置页` 中存在。",
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
# "split_response": {
|
||||
# "description": "是否分割回复",
|
||||
# "type": "bool",
|
||||
# "hint": "启用后,将会根据句子分割回复以更像人类回复。每次回复之间具有随机的时间间隔。默认启用。",
|
||||
# },
|
||||
# "persona": {
|
||||
# "description": "人格",
|
||||
# "type": "string",
|
||||
# "hint": "默认人格。当启动 ATRI 之后,在 Provider 处设置的人格将会失效。",
|
||||
# "obvious_hint": True,
|
||||
# },
|
||||
# "chat_provider_id": {
|
||||
# "description": "Chat provider ID",
|
||||
# "type": "string",
|
||||
# "hint": "将会使用指定的 provider 来进行文本聊天,请确保所填的 provider id 在 `配置页` 中存在。",
|
||||
# "obvious_hint": True,
|
||||
# },
|
||||
# "chat_base_model_path": {
|
||||
# "description": "用于聊天的基座模型路径",
|
||||
# "type": "string",
|
||||
# "hint": "用于聊天的基座模型路径。当填写此项和 Lora 路径后,将会忽略上面设置的 Chat provider ID。",
|
||||
# "obvious_hint": True,
|
||||
# },
|
||||
# "chat_adapter_model_path": {
|
||||
# "description": "用于聊天的 Lora 模型路径",
|
||||
# "type": "string",
|
||||
# "hint": "Lora 模型路径。",
|
||||
# "obvious_hint": True,
|
||||
# },
|
||||
# "quantization_bit": {
|
||||
# "description": "量化位数",
|
||||
# "type": "int",
|
||||
# "hint": "模型量化位数。如果你不知道这是什么,请不要修改。默认为 4。",
|
||||
# "obvious_hint": True,
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
|
||||
@@ -166,6 +166,7 @@ class LLMRequestSubStage(Stage):
|
||||
event=event,
|
||||
pipeline_ctx=self.ctx,
|
||||
)
|
||||
logger.debug(f"handle provider[id: {provider.provider_config['id']}] request: {req}")
|
||||
await tool_loop_agent.reset(req=req, streaming=self.streaming_response)
|
||||
|
||||
async def requesting():
|
||||
@@ -184,7 +185,8 @@ class LLMRequestSubStage(Stage):
|
||||
await event.send(resp.data["chain"])
|
||||
continue
|
||||
# 对于其他情况,暂时先不处理
|
||||
if resp.type == "tool_call":
|
||||
continue
|
||||
elif resp.type == "tool_call":
|
||||
if self.streaming_response:
|
||||
# 用来标记流式响应需要分节
|
||||
yield MessageChain(chain=[], type="break")
|
||||
|
||||
@@ -73,7 +73,7 @@ class PipelineScheduler:
|
||||
await self._process_stages(event)
|
||||
|
||||
# 如果没有发送操作, 则发送一个空消息, 以便于后续的处理
|
||||
if not event._has_send_oper and event.get_platform_name() == "webchat":
|
||||
if event.get_platform_name() == "webchat":
|
||||
await event.send(None)
|
||||
|
||||
logger.debug("pipeline 执行完毕。")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import re
|
||||
from typing import AsyncGenerator, Dict, List
|
||||
from aiocqhttp import CQHttp
|
||||
from aiocqhttp import CQHttp, Event
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.message_components import (
|
||||
Image,
|
||||
@@ -58,50 +58,85 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
||||
ret.append(d)
|
||||
return ret
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
@classmethod
|
||||
async def _dispatch_send(
|
||||
cls,
|
||||
bot: CQHttp,
|
||||
event: Event | None,
|
||||
is_group: bool,
|
||||
session_id: str,
|
||||
messages: list[dict],
|
||||
):
|
||||
if event:
|
||||
await bot.send(event=event, message=messages)
|
||||
elif is_group:
|
||||
await bot.send_group_msg(group_id=session_id, message=messages)
|
||||
else:
|
||||
await bot.send_private_msg(user_id=session_id, message=messages)
|
||||
|
||||
@classmethod
|
||||
async def send_message(
|
||||
cls,
|
||||
bot: CQHttp,
|
||||
message_chain: MessageChain,
|
||||
event: Event | None = None,
|
||||
is_group: bool = False,
|
||||
session_id: str = None,
|
||||
):
|
||||
"""发送消息"""
|
||||
|
||||
# 转发消息、文件消息不能和普通消息混在一起发送
|
||||
send_one_by_one = any(
|
||||
isinstance(seg, (Node, Nodes, File)) for seg in message.chain
|
||||
isinstance(seg, (Node, Nodes, File)) for seg in message_chain.chain
|
||||
)
|
||||
if send_one_by_one:
|
||||
for seg in message.chain:
|
||||
if isinstance(seg, (Node, Nodes)):
|
||||
# 合并转发消息
|
||||
|
||||
if isinstance(seg, Node):
|
||||
nodes = Nodes([seg])
|
||||
seg = nodes
|
||||
|
||||
payload = await seg.to_dict()
|
||||
|
||||
if self.get_group_id():
|
||||
payload["group_id"] = self.get_group_id()
|
||||
await self.bot.call_action("send_group_forward_msg", **payload)
|
||||
else:
|
||||
payload["user_id"] = self.get_sender_id()
|
||||
await self.bot.call_action(
|
||||
"send_private_forward_msg", **payload
|
||||
)
|
||||
elif isinstance(seg, File):
|
||||
d = await AiocqhttpMessageEvent._from_segment_to_dict(seg)
|
||||
await self.bot.send(
|
||||
self.message_obj.raw_message,
|
||||
[d],
|
||||
)
|
||||
else:
|
||||
await self.bot.send(
|
||||
self.message_obj.raw_message,
|
||||
await AiocqhttpMessageEvent._parse_onebot_json(
|
||||
MessageChain([seg])
|
||||
),
|
||||
)
|
||||
await asyncio.sleep(0.5)
|
||||
else:
|
||||
ret = await AiocqhttpMessageEvent._parse_onebot_json(message)
|
||||
if not send_one_by_one:
|
||||
ret = await cls._parse_onebot_json(message_chain)
|
||||
if not ret:
|
||||
return
|
||||
await self.bot.send(self.message_obj.raw_message, ret)
|
||||
await cls._dispatch_send(bot, event, is_group, session_id, ret)
|
||||
return
|
||||
for seg in message_chain.chain:
|
||||
if isinstance(seg, (Node, Nodes)):
|
||||
# 合并转发消息
|
||||
if isinstance(seg, Node):
|
||||
nodes = Nodes([seg])
|
||||
seg = nodes
|
||||
|
||||
payload = await seg.to_dict()
|
||||
|
||||
if is_group:
|
||||
payload["group_id"] = session_id
|
||||
await bot.call_action("send_group_forward_msg", **payload)
|
||||
else:
|
||||
payload["user_id"] = session_id
|
||||
await bot.call_action("send_private_forward_msg", **payload)
|
||||
elif isinstance(seg, File):
|
||||
d = await cls._from_segment_to_dict(seg)
|
||||
await cls._dispatch_send(bot, event, is_group, session_id, [d])
|
||||
else:
|
||||
messages = await cls._parse_onebot_json(MessageChain([seg]))
|
||||
if not messages:
|
||||
continue
|
||||
await cls._dispatch_send(bot, event, is_group, session_id, messages)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
"""发送消息"""
|
||||
event = self.message_obj.raw_message
|
||||
assert isinstance(event, Event), "Event must be an instance of aiocqhttp.Event"
|
||||
is_group = False
|
||||
if self.get_group_id():
|
||||
is_group = True
|
||||
session_id = self.get_group_id()
|
||||
else:
|
||||
session_id = self.get_sender_id()
|
||||
await self.send_message(
|
||||
bot=self.bot,
|
||||
message_chain=message,
|
||||
event=event,
|
||||
is_group=is_group,
|
||||
session_id=session_id,
|
||||
)
|
||||
await super().send(message)
|
||||
|
||||
async def send_streaming(
|
||||
|
||||
@@ -83,19 +83,18 @@ class AiocqhttpAdapter(Platform):
|
||||
async def send_by_session(
|
||||
self, session: MessageSesion, message_chain: MessageChain
|
||||
):
|
||||
ret = await AiocqhttpMessageEvent._parse_onebot_json(message_chain)
|
||||
match session.message_type.value:
|
||||
case MessageType.GROUP_MESSAGE.value:
|
||||
if "_" in session.session_id:
|
||||
# 独立会话
|
||||
_, group_id = session.session_id.split("_")
|
||||
await self.bot.send_group_msg(group_id=group_id, message=ret)
|
||||
else:
|
||||
await self.bot.send_group_msg(
|
||||
group_id=session.session_id, message=ret
|
||||
)
|
||||
case MessageType.FRIEND_MESSAGE.value:
|
||||
await self.bot.send_private_msg(user_id=session.session_id, message=ret)
|
||||
is_group = session.message_type == MessageType.GROUP_MESSAGE
|
||||
if is_group:
|
||||
session_id = session.session_id.split("_")[-1]
|
||||
else:
|
||||
session_id = session.session_id
|
||||
await AiocqhttpMessageEvent.send_message(
|
||||
bot=self.bot,
|
||||
message_chain=message_chain,
|
||||
event=None, # 这里不需要 event,因为是通过 session 发送的
|
||||
is_group=is_group,
|
||||
session_id=session_id,
|
||||
)
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
async def convert_message(self, event: Event) -> AstrBotMessage:
|
||||
@@ -307,7 +306,9 @@ class AiocqhttpAdapter(Platform):
|
||||
user_id=int(m["data"]["qq"]),
|
||||
)
|
||||
if at_info:
|
||||
nickname = at_info.get("nick", "") or at_info.get("nickname", "")
|
||||
nickname = at_info.get("nick", "") or at_info.get(
|
||||
"nickname", ""
|
||||
)
|
||||
is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"}
|
||||
|
||||
abm.message.append(
|
||||
|
||||
@@ -22,7 +22,11 @@ class WebChatMessageEvent(AstrMessageEvent):
|
||||
web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
|
||||
if not message:
|
||||
await web_chat_back_queue.put(
|
||||
{"type": "end", "data": "", "streaming": False}
|
||||
{
|
||||
"type": "end",
|
||||
"data": "",
|
||||
"streaming": False,
|
||||
} # end means this request is finished
|
||||
)
|
||||
return ""
|
||||
|
||||
@@ -99,16 +103,6 @@ class WebChatMessageEvent(AstrMessageEvent):
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
await WebChatMessageEvent._send(message, session_id=self.session_id)
|
||||
cid = self.session_id.split("!")[-1]
|
||||
web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
|
||||
await web_chat_back_queue.put(
|
||||
{
|
||||
"type": "end",
|
||||
"data": "",
|
||||
"streaming": False,
|
||||
"cid": cid,
|
||||
}
|
||||
)
|
||||
await super().send(message)
|
||||
|
||||
async def send_streaming(self, generator, use_fallback: bool = False):
|
||||
@@ -120,7 +114,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
||||
# 分割符
|
||||
await web_chat_back_queue.put(
|
||||
{
|
||||
"type": "end",
|
||||
"type": "break", # break means a segment end
|
||||
"data": final_data,
|
||||
"streaming": True,
|
||||
"cid": cid,
|
||||
@@ -134,7 +128,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
||||
|
||||
await web_chat_back_queue.put(
|
||||
{
|
||||
"type": "end",
|
||||
"type": "complete", # complete means we return the final result
|
||||
"data": final_data,
|
||||
"streaming": True,
|
||||
"cid": cid,
|
||||
|
||||
@@ -7,7 +7,7 @@ from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from astrbot.core.db import BaseDatabase
|
||||
|
||||
from .entities import ProviderType
|
||||
from .provider import Personality, Provider, STTProvider, TTSProvider
|
||||
from .provider import Personality, Provider, STTProvider, TTSProvider, EmbeddingProvider
|
||||
from .register import llm_tools, provider_cls_map
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ class ProviderManager:
|
||||
"""加载的 Speech To Text Provider 的实例"""
|
||||
self.tts_provider_insts: List[TTSProvider] = []
|
||||
"""加载的 Text To Speech Provider 的实例"""
|
||||
self.embedding_provider_insts: List[Provider] = []
|
||||
self.embedding_provider_insts: List[EmbeddingProvider] = []
|
||||
"""加载的 Embedding Provider 的实例"""
|
||||
self.inst_map: dict[str, Provider] = {}
|
||||
"""Provider 实例映射. key: provider_id, value: Provider 实例"""
|
||||
|
||||
@@ -482,13 +482,8 @@ class ProviderOpenAIOfficial(Provider):
|
||||
"""
|
||||
new_contexts = []
|
||||
|
||||
flag = False
|
||||
for context in contexts:
|
||||
if flag:
|
||||
flag = False # 删除 image 后,下一条(LLM 响应)也要删除
|
||||
continue
|
||||
if "content" in context and isinstance(context["content"], list):
|
||||
flag = True
|
||||
# continue
|
||||
new_content = []
|
||||
for item in context["content"]:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .star import StarMetadata, star_map
|
||||
from .star import StarMetadata, star_map, star_registry
|
||||
from .star_manager import PluginManager
|
||||
from .context import Context
|
||||
from astrbot.core.provider import Provider
|
||||
@@ -16,11 +16,16 @@ class Star(CommandParserMixin):
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
metadata = StarMetadata(
|
||||
star_cls_type=cls,
|
||||
module_path=cls.__module__,
|
||||
)
|
||||
star_map[cls.__module__] = metadata
|
||||
if not star_map.get(cls.__module__):
|
||||
metadata = StarMetadata(
|
||||
star_cls_type=cls,
|
||||
module_path=cls.__module__,
|
||||
)
|
||||
star_map[cls.__module__] = metadata
|
||||
star_registry.append(metadata)
|
||||
else:
|
||||
star_map[cls.__module__].star_cls_type = cls
|
||||
star_map[cls.__module__].module_path = cls.__module__
|
||||
|
||||
@staticmethod
|
||||
async def text_to_image(text: str, return_url=True) -> str:
|
||||
|
||||
@@ -2,7 +2,7 @@ from asyncio import Queue
|
||||
from typing import List, Union
|
||||
|
||||
from astrbot.core import sp
|
||||
from astrbot.core.provider.provider import Provider, TTSProvider, STTProvider
|
||||
from astrbot.core.provider.provider import Provider, TTSProvider, STTProvider, EmbeddingProvider
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
@@ -141,6 +141,10 @@ class Context:
|
||||
"""获取所有用于 STT 任务的 Provider。"""
|
||||
return self.provider_manager.stt_provider_insts
|
||||
|
||||
def get_all_embedding_providers(self) -> List[EmbeddingProvider]:
|
||||
"""获取所有用于 Embedding 任务的 Provider。"""
|
||||
return self.provider_manager.embedding_provider_insts
|
||||
|
||||
def get_using_provider(self, umo: str = None) -> Provider:
|
||||
"""
|
||||
获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。通过 /provider 指令切换。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import warnings
|
||||
|
||||
from astrbot.core.star import StarMetadata, star_map
|
||||
|
||||
_warned_register_star = False
|
||||
|
||||
|
||||
@@ -37,6 +39,22 @@ def register_star(name: str, author: str, desc: str, version: str, repo: str = N
|
||||
)
|
||||
|
||||
def decorator(cls):
|
||||
if not star_map.get(cls.__module__):
|
||||
metadata = StarMetadata(
|
||||
name=name,
|
||||
author=author,
|
||||
desc=desc,
|
||||
version=version,
|
||||
repo=repo,
|
||||
)
|
||||
star_map[cls.__module__] = metadata
|
||||
else:
|
||||
star_map[cls.__module__].name = name
|
||||
star_map[cls.__module__].author = author
|
||||
star_map[cls.__module__].desc = desc
|
||||
star_map[cls.__module__].version = version
|
||||
star_map[cls.__module__].repo = repo
|
||||
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -56,7 +56,10 @@ class StarMetadata:
|
||||
"""插件支持的平台ID字典,key为平台ID,value为是否支持"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"StarMetadata({self.name}, {self.desc}, {self.version}, {self.repo})"
|
||||
return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
|
||||
|
||||
def update_platform_compatibility(self, plugin_enable_config: dict) -> None:
|
||||
"""更新插件支持的平台列表
|
||||
|
||||
+132
-127
@@ -36,12 +36,6 @@ except ImportError:
|
||||
if os.getenv("ASTRBOT_RELOAD", "0") == "1":
|
||||
logger.warning("未安装 watchfiles,无法实现插件的热重载。")
|
||||
|
||||
try:
|
||||
import nh3
|
||||
except ImportError:
|
||||
logger.warning("未安装 nh3 库,无法清理插件 README.md 中的 HTML 标签。")
|
||||
nh3 = None
|
||||
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, context: Context, config: AstrBotConfig):
|
||||
@@ -63,6 +57,8 @@ class PluginManager:
|
||||
"""保留插件的路径。在 packages 目录下"""
|
||||
self.conf_schema_fname = "_conf_schema.json"
|
||||
"""插件配置 Schema 文件名"""
|
||||
self._pm_lock = asyncio.Lock()
|
||||
"""StarManager操作互斥锁"""
|
||||
|
||||
self.failed_plugin_info = ""
|
||||
if os.getenv("ASTRBOT_RELOAD", "0") == "1":
|
||||
@@ -192,9 +188,9 @@ class PluginManager:
|
||||
|
||||
@staticmethod
|
||||
def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata:
|
||||
"""v3.4.0 以前的方式载入插件元数据
|
||||
"""先寻找 metadata.yaml 文件,如果不存在,则使用插件对象的 info() 函数获取元数据。
|
||||
|
||||
先寻找 metadata.yaml 文件,如果不存在,则使用插件对象的 info() 函数获取元数据。
|
||||
Notes: 旧版本 AstrBot 插件可能使用的是 info() 函数来获取元数据。
|
||||
"""
|
||||
metadata = None
|
||||
|
||||
@@ -206,11 +202,14 @@ class PluginManager:
|
||||
os.path.join(plugin_path, "metadata.yaml"), "r", encoding="utf-8"
|
||||
) as f:
|
||||
metadata = yaml.safe_load(f)
|
||||
elif plugin_obj:
|
||||
elif plugin_obj and hasattr(plugin_obj, "info"):
|
||||
# 使用 info() 函数
|
||||
metadata = plugin_obj.info()
|
||||
|
||||
if isinstance(metadata, dict):
|
||||
if "desc" not in metadata and "description" in metadata:
|
||||
metadata["desc"] = metadata["description"]
|
||||
|
||||
if (
|
||||
"name" not in metadata
|
||||
or "desc" not in metadata
|
||||
@@ -296,50 +295,51 @@ class PluginManager:
|
||||
- success (bool): 重载是否成功
|
||||
- error_message (str|None): 错误信息,成功时为 None
|
||||
"""
|
||||
specified_module_path = None
|
||||
if specified_plugin_name:
|
||||
for smd in star_registry:
|
||||
if smd.name == specified_plugin_name:
|
||||
specified_module_path = smd.module_path
|
||||
break
|
||||
async with self._pm_lock:
|
||||
specified_module_path = None
|
||||
if specified_plugin_name:
|
||||
for smd in star_registry:
|
||||
if smd.name == specified_plugin_name:
|
||||
specified_module_path = smd.module_path
|
||||
break
|
||||
|
||||
# 终止插件
|
||||
if not specified_module_path:
|
||||
# 重载所有插件
|
||||
for smd in star_registry:
|
||||
try:
|
||||
await self._terminate_plugin(smd)
|
||||
except Exception as e:
|
||||
logger.warning(traceback.format_exc())
|
||||
logger.warning(
|
||||
f"插件 {smd.name} 未被正常终止: {str(e)}, 可能会导致该插件运行不正常。"
|
||||
)
|
||||
# 终止插件
|
||||
if not specified_module_path:
|
||||
# 重载所有插件
|
||||
for smd in star_registry:
|
||||
try:
|
||||
await self._terminate_plugin(smd)
|
||||
except Exception as e:
|
||||
logger.warning(traceback.format_exc())
|
||||
logger.warning(
|
||||
f"插件 {smd.name} 未被正常终止: {str(e)}, 可能会导致该插件运行不正常。"
|
||||
)
|
||||
|
||||
await self._unbind_plugin(smd.name, smd.module_path)
|
||||
await self._unbind_plugin(smd.name, smd.module_path)
|
||||
|
||||
star_handlers_registry.clear()
|
||||
star_map.clear()
|
||||
star_registry.clear()
|
||||
else:
|
||||
# 只重载指定插件
|
||||
smd = star_map.get(specified_module_path)
|
||||
if smd:
|
||||
try:
|
||||
await self._terminate_plugin(smd)
|
||||
except Exception as e:
|
||||
logger.warning(traceback.format_exc())
|
||||
logger.warning(
|
||||
f"插件 {smd.name} 未被正常终止: {str(e)}, 可能会导致该插件运行不正常。"
|
||||
)
|
||||
star_handlers_registry.clear()
|
||||
star_map.clear()
|
||||
star_registry.clear()
|
||||
else:
|
||||
# 只重载指定插件
|
||||
smd = star_map.get(specified_module_path)
|
||||
if smd:
|
||||
try:
|
||||
await self._terminate_plugin(smd)
|
||||
except Exception as e:
|
||||
logger.warning(traceback.format_exc())
|
||||
logger.warning(
|
||||
f"插件 {smd.name} 未被正常终止: {str(e)}, 可能会导致该插件运行不正常。"
|
||||
)
|
||||
|
||||
await self._unbind_plugin(smd.name, specified_module_path)
|
||||
await self._unbind_plugin(smd.name, specified_module_path)
|
||||
|
||||
result = await self.load(specified_module_path)
|
||||
result = await self.load(specified_module_path)
|
||||
|
||||
# 更新所有插件的平台兼容性
|
||||
await self.update_all_platform_compatibility()
|
||||
# 更新所有插件的平台兼容性
|
||||
await self.update_all_platform_compatibility()
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
async def update_all_platform_compatibility(self):
|
||||
"""更新所有插件的平台兼容性设置"""
|
||||
@@ -438,7 +438,7 @@ class PluginManager:
|
||||
)
|
||||
|
||||
if path in star_map:
|
||||
# 通过__init__subclass__注册插件
|
||||
# 通过 __init__subclass__ 注册插件
|
||||
metadata = star_map[path]
|
||||
|
||||
try:
|
||||
@@ -452,8 +452,11 @@ class PluginManager:
|
||||
metadata.desc = metadata_yaml.desc
|
||||
metadata.version = metadata_yaml.version
|
||||
metadata.repo = metadata_yaml.repo
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"插件 {root_dir_name} 元数据载入失败: {str(e)}。使用默认元数据。"
|
||||
)
|
||||
logger.info(metadata)
|
||||
metadata.config = plugin_config
|
||||
if path not in inactivated_plugins:
|
||||
# 只有没有禁用插件时才实例化插件类
|
||||
@@ -507,8 +510,6 @@ class PluginManager:
|
||||
if func_tool.name in inactivated_llm_tools:
|
||||
func_tool.active = False
|
||||
|
||||
star_registry.append(metadata)
|
||||
|
||||
else:
|
||||
# v3.4.0 以前的方式注册插件
|
||||
logger.debug(
|
||||
@@ -627,43 +628,45 @@ class PluginManager:
|
||||
- readme: README.md 文件的内容(如果存在)
|
||||
如果找不到插件元数据则返回 None。
|
||||
"""
|
||||
plugin_path = await self.updator.install(repo_url, proxy)
|
||||
# reload the plugin
|
||||
dir_name = os.path.basename(plugin_path)
|
||||
await self.load(specified_dir_name=dir_name)
|
||||
async with self._pm_lock:
|
||||
plugin_path = await self.updator.install(repo_url, proxy)
|
||||
# reload the plugin
|
||||
dir_name = os.path.basename(plugin_path)
|
||||
await self.load(specified_dir_name=dir_name)
|
||||
|
||||
# Get the plugin metadata to return repo info
|
||||
plugin = self.context.get_registered_star(dir_name)
|
||||
if not plugin:
|
||||
# Try to find by other name if directory name doesn't match plugin name
|
||||
for star in self.context.get_all_stars():
|
||||
if star.root_dir_name == dir_name:
|
||||
plugin = star
|
||||
break
|
||||
# Get the plugin metadata to return repo info
|
||||
plugin = self.context.get_registered_star(dir_name)
|
||||
if not plugin:
|
||||
# Try to find by other name if directory name doesn't match plugin name
|
||||
for star in self.context.get_all_stars():
|
||||
if star.root_dir_name == dir_name:
|
||||
plugin = star
|
||||
break
|
||||
|
||||
# Extract README.md content if exists
|
||||
readme_content = None
|
||||
readme_path = os.path.join(plugin_path, "README.md")
|
||||
if not os.path.exists(readme_path):
|
||||
readme_path = os.path.join(plugin_path, "readme.md")
|
||||
# Extract README.md content if exists
|
||||
readme_content = None
|
||||
readme_path = os.path.join(plugin_path, "README.md")
|
||||
if not os.path.exists(readme_path):
|
||||
readme_path = os.path.join(plugin_path, "readme.md")
|
||||
|
||||
if os.path.exists(readme_path) and nh3:
|
||||
try:
|
||||
with open(readme_path, "r", encoding="utf-8") as f:
|
||||
readme_content = f.read()
|
||||
cleaned_content = nh3.clean(readme_content)
|
||||
except Exception as e:
|
||||
logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}")
|
||||
if os.path.exists(readme_path):
|
||||
try:
|
||||
with open(readme_path, "r", encoding="utf-8") as f:
|
||||
readme_content = f.read()
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}"
|
||||
)
|
||||
|
||||
plugin_info = None
|
||||
if plugin:
|
||||
plugin_info = {
|
||||
"repo": plugin.repo,
|
||||
"readme": cleaned_content,
|
||||
"name": plugin.name,
|
||||
}
|
||||
plugin_info = None
|
||||
if plugin:
|
||||
plugin_info = {
|
||||
"repo": plugin.repo,
|
||||
"readme": readme_content,
|
||||
"name": plugin.name,
|
||||
}
|
||||
|
||||
return plugin_info
|
||||
return plugin_info
|
||||
|
||||
async def uninstall_plugin(self, plugin_name: str):
|
||||
"""卸载指定的插件。
|
||||
@@ -674,32 +677,33 @@ class PluginManager:
|
||||
Raises:
|
||||
Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常
|
||||
"""
|
||||
plugin = self.context.get_registered_star(plugin_name)
|
||||
if not plugin:
|
||||
raise Exception("插件不存在。")
|
||||
if plugin.reserved:
|
||||
raise Exception("该插件是 AstrBot 保留插件,无法卸载。")
|
||||
root_dir_name = plugin.root_dir_name
|
||||
ppath = self.plugin_store_path
|
||||
async with self._pm_lock:
|
||||
plugin = self.context.get_registered_star(plugin_name)
|
||||
if not plugin:
|
||||
raise Exception("插件不存在。")
|
||||
if plugin.reserved:
|
||||
raise Exception("该插件是 AstrBot 保留插件,无法卸载。")
|
||||
root_dir_name = plugin.root_dir_name
|
||||
ppath = self.plugin_store_path
|
||||
|
||||
# 终止插件
|
||||
try:
|
||||
await self._terminate_plugin(plugin)
|
||||
except Exception as e:
|
||||
logger.warning(traceback.format_exc())
|
||||
logger.warning(
|
||||
f"插件 {plugin_name} 未被正常终止 {str(e)}, 可能会导致资源泄露等问题。"
|
||||
)
|
||||
# 终止插件
|
||||
try:
|
||||
await self._terminate_plugin(plugin)
|
||||
except Exception as e:
|
||||
logger.warning(traceback.format_exc())
|
||||
logger.warning(
|
||||
f"插件 {plugin_name} 未被正常终止 {str(e)}, 可能会导致资源泄露等问题。"
|
||||
)
|
||||
|
||||
# 从 star_registry 和 star_map 中删除
|
||||
await self._unbind_plugin(plugin_name, plugin.module_path)
|
||||
# 从 star_registry 和 star_map 中删除
|
||||
await self._unbind_plugin(plugin_name, plugin.module_path)
|
||||
|
||||
try:
|
||||
remove_dir(os.path.join(ppath, root_dir_name))
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"移除插件成功,但是删除插件文件夹失败: {str(e)}。您可以手动删除该文件夹,位于 addons/plugins/ 下。"
|
||||
)
|
||||
try:
|
||||
remove_dir(os.path.join(ppath, root_dir_name))
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"移除插件成功,但是删除插件文件夹失败: {str(e)}。您可以手动删除该文件夹,位于 addons/plugins/ 下。"
|
||||
)
|
||||
|
||||
async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str):
|
||||
"""解绑并移除一个插件。
|
||||
@@ -752,33 +756,34 @@ class PluginManager:
|
||||
将插件的 module_path 加入到 data/shared_preferences.json 的 inactivated_plugins 列表中。
|
||||
并且同时将插件启用的 llm_tool 禁用。
|
||||
"""
|
||||
plugin = self.context.get_registered_star(plugin_name)
|
||||
if not plugin:
|
||||
raise Exception("插件不存在。")
|
||||
async with self._pm_lock:
|
||||
plugin = self.context.get_registered_star(plugin_name)
|
||||
if not plugin:
|
||||
raise Exception("插件不存在。")
|
||||
|
||||
# 调用插件的终止方法
|
||||
await self._terminate_plugin(plugin)
|
||||
# 调用插件的终止方法
|
||||
await self._terminate_plugin(plugin)
|
||||
|
||||
# 加入到 shared_preferences 中
|
||||
inactivated_plugins: list = sp.get("inactivated_plugins", [])
|
||||
if plugin.module_path not in inactivated_plugins:
|
||||
inactivated_plugins.append(plugin.module_path)
|
||||
# 加入到 shared_preferences 中
|
||||
inactivated_plugins: list = sp.get("inactivated_plugins", [])
|
||||
if plugin.module_path not in inactivated_plugins:
|
||||
inactivated_plugins.append(plugin.module_path)
|
||||
|
||||
inactivated_llm_tools: list = list(
|
||||
set(sp.get("inactivated_llm_tools", []))
|
||||
) # 后向兼容
|
||||
inactivated_llm_tools: list = list(
|
||||
set(sp.get("inactivated_llm_tools", []))
|
||||
) # 后向兼容
|
||||
|
||||
# 禁用插件启用的 llm_tool
|
||||
for func_tool in llm_tools.func_list:
|
||||
if func_tool.handler_module_path == plugin.module_path:
|
||||
func_tool.active = False
|
||||
if func_tool.name not in inactivated_llm_tools:
|
||||
inactivated_llm_tools.append(func_tool.name)
|
||||
# 禁用插件启用的 llm_tool
|
||||
for func_tool in llm_tools.func_list:
|
||||
if func_tool.handler_module_path == plugin.module_path:
|
||||
func_tool.active = False
|
||||
if func_tool.name not in inactivated_llm_tools:
|
||||
inactivated_llm_tools.append(func_tool.name)
|
||||
|
||||
sp.put("inactivated_plugins", inactivated_plugins)
|
||||
sp.put("inactivated_llm_tools", inactivated_llm_tools)
|
||||
sp.put("inactivated_plugins", inactivated_plugins)
|
||||
sp.put("inactivated_llm_tools", inactivated_llm_tools)
|
||||
|
||||
plugin.activated = False
|
||||
plugin.activated = False
|
||||
|
||||
@staticmethod
|
||||
async def _terminate_plugin(star_metadata: StarMetadata):
|
||||
|
||||
@@ -166,15 +166,12 @@ class ChatRoute(Route):
|
||||
type = result.get("type")
|
||||
cid = result.get("cid")
|
||||
streaming = result.get("streaming", False)
|
||||
chain_type = result.get("chain_type")
|
||||
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
if streaming and type != "end":
|
||||
# If the result is still streaming, we continue to wait for more data
|
||||
continue
|
||||
|
||||
if result_text:
|
||||
if type == "end":
|
||||
break
|
||||
elif (streaming and type == "complete") or not streaming:
|
||||
# append bot message
|
||||
conversation = self.db.get_conversation_by_user_id(
|
||||
username, cid
|
||||
@@ -188,10 +185,6 @@ class ChatRoute(Route):
|
||||
self.db.update_conversation(
|
||||
username, cid, history=json.dumps(history)
|
||||
)
|
||||
if chain_type not in ["tool_call", "tool_call_result"]:
|
||||
# If the result is not a tool call or tool call result,
|
||||
# we can break the loop and end the stream
|
||||
break
|
||||
|
||||
except BaseException as _:
|
||||
logger.debug(f"用户 {username} 断开聊天长连接。")
|
||||
|
||||
@@ -166,7 +166,7 @@ class ConversationRoute(Route):
|
||||
|
||||
if not user_id or not cid:
|
||||
return Response().error("缺少必要参数: user_id 和 cid").__dict__
|
||||
self.core_lifecycle.conversation_manager.delete_conversation(
|
||||
await self.core_lifecycle.conversation_manager.delete_conversation(
|
||||
unified_msg_origin=user_id, conversation_id=cid
|
||||
)
|
||||
return Response().ok({"message": "对话删除成功"}).__dict__
|
||||
|
||||
@@ -18,12 +18,6 @@ from astrbot.core.star.filter.regex import RegexFilter
|
||||
from astrbot.core.star.star_handler import EventType
|
||||
from astrbot.core import DEMO_MODE
|
||||
|
||||
try:
|
||||
import nh3
|
||||
except ImportError:
|
||||
logger.warning("未安装 nh3 库,无法清理插件 README.md 中的 HTML 标签。")
|
||||
nh3 = None
|
||||
|
||||
|
||||
class PluginRoute(Route):
|
||||
def __init__(
|
||||
@@ -332,9 +326,6 @@ class PluginRoute(Route):
|
||||
return Response().error(str(e)).__dict__
|
||||
|
||||
async def get_plugin_readme(self):
|
||||
if not nh3:
|
||||
return Response().error("未安装 nh3 库").__dict__
|
||||
|
||||
plugin_name = request.args.get("name")
|
||||
logger.debug(f"正在获取插件 {plugin_name} 的README文件内容")
|
||||
|
||||
@@ -370,11 +361,9 @@ class PluginRoute(Route):
|
||||
with open(readme_path, "r", encoding="utf-8") as f:
|
||||
readme_content = f.read()
|
||||
|
||||
cleaned_content = nh3.clean(readme_content)
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok({"content": cleaned_content}, "成功获取README内容")
|
||||
.ok({"content": readme_content}, "成功获取README内容")
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -395,12 +384,14 @@ class PluginRoute(Route):
|
||||
platform_type = platform.get("type", "")
|
||||
platform_id = platform.get("id", "")
|
||||
|
||||
platforms.append({
|
||||
"name": platform_id, # 使用type作为name,这是系统内部使用的平台名称
|
||||
"id": platform_id, # 保留id字段以便前端可以显示
|
||||
"type": platform_type,
|
||||
"display_name": f"{platform_type}({platform_id})",
|
||||
})
|
||||
platforms.append(
|
||||
{
|
||||
"name": platform_id, # 使用type作为name,这是系统内部使用的平台名称
|
||||
"id": platform_id, # 保留id字段以便前端可以显示
|
||||
"type": platform_type,
|
||||
"display_name": f"{platform_type}({platform_id})",
|
||||
}
|
||||
)
|
||||
|
||||
adjusted_platform_enable = {}
|
||||
for platform_id, plugins in platform_enable.items():
|
||||
@@ -409,11 +400,13 @@ class PluginRoute(Route):
|
||||
# 获取所有插件,包括系统内部插件
|
||||
plugins = []
|
||||
for plugin in self.plugin_manager.context.get_all_stars():
|
||||
plugins.append({
|
||||
"name": plugin.name,
|
||||
"desc": plugin.desc,
|
||||
"reserved": plugin.reserved, # 添加reserved标志
|
||||
})
|
||||
plugins.append(
|
||||
{
|
||||
"name": plugin.name,
|
||||
"desc": plugin.desc,
|
||||
"reserved": plugin.reserved, # 添加reserved标志
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}"
|
||||
@@ -421,11 +414,13 @@ class PluginRoute(Route):
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok({
|
||||
"platforms": platforms,
|
||||
"plugins": plugins,
|
||||
"platform_enable": adjusted_platform_enable,
|
||||
})
|
||||
.ok(
|
||||
{
|
||||
"platforms": platforms,
|
||||
"plugins": plugins,
|
||||
"platform_enable": adjusted_platform_enable,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# What's Changed
|
||||
|
||||
1. 修复: 工具调用的结果错误地被当作消息发送
|
||||
2. 新增: 支持对引用消息中的图片进行理解(QQ, Telegram)
|
||||
3. 优化: QQ 主动消息发送逻辑,优化合并消息、文件、语音、图片等的处理
|
||||
4. 优化: 移除插件的 @register 插件注册装饰器(插件只需要继承 Star 类即可,AstrBot 会自动处理),简化插件代码开发
|
||||
@@ -0,0 +1,7 @@
|
||||
# What's Changed
|
||||
|
||||
1. 修复: WebChat 下图片、音频消息没有被正确渲染
|
||||
2. 修复: 部分情况下,插件信息无法正确显示
|
||||
3. 修复: WebChat 下开启分段回复后,消息错位
|
||||
4. 优化: 提高插件加载的性能和稳定性
|
||||
5. 修复: WebUI 对话数据库页中,无法真正删除对话
|
||||
@@ -0,0 +1,3 @@
|
||||
# What's Changed
|
||||
|
||||
1. 修复: 用户环境没有 Docker 时,可能导致死锁(表现为在初始化 AstrBot 的时候卡住)
|
||||
@@ -47,7 +47,6 @@
|
||||
contain
|
||||
width="120"
|
||||
height="120"
|
||||
class="rounded-circle"
|
||||
></v-img>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
@@ -171,14 +171,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 机器人消息 -->
|
||||
<!-- Bot Messages -->
|
||||
<div v-else class="bot-message">
|
||||
<v-avatar class="bot-avatar" size="36">
|
||||
<span class="text-h2">✨</span>
|
||||
</v-avatar>
|
||||
<div class="bot-message-content">
|
||||
<div class="message-bubble bot-bubble">
|
||||
<div v-html="md.render(msg.message)" class="markdown-content"></div>
|
||||
<!-- Text -->
|
||||
<div v-if="msg.message && msg.message.trim()"
|
||||
v-html="md.render(msg.message)"
|
||||
class="markdown-content"></div>
|
||||
|
||||
<!-- Image -->
|
||||
<div class="embedded-images" v-if="msg.embedded_images && msg.embedded_images.length > 0">
|
||||
<div v-for="(img, imgIndex) in msg.embedded_images" :key="imgIndex"
|
||||
class="embedded-image">
|
||||
<img :src="img" class="bot-embedded-image" @click="openImagePreview(img)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio -->
|
||||
<div class="embedded-audio" v-if="msg.embedded_audio">
|
||||
<audio controls class="audio-player">
|
||||
<source :src="msg.embedded_audio" type="audio/wav">
|
||||
{{ t('messages.errors.browser.audioNotSupported') }}
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-actions">
|
||||
<v-btn :icon="getCopyIcon(index)" size="small" variant="text"
|
||||
@@ -716,7 +735,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
axios.get('/api/chat/get_conversation?conversation_id=' + cid[0]).then(async response => {
|
||||
this.currCid = cid[0];
|
||||
let message = JSON.parse(response.data.data.history);
|
||||
@@ -724,21 +742,26 @@ export default {
|
||||
if (message[i].message.startsWith('[IMAGE]')) {
|
||||
let img = message[i].message.replace('[IMAGE]', '');
|
||||
const imageUrl = await this.getMediaFile(img);
|
||||
message[i].message = `<img src="${imageUrl}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
|
||||
if (!message[i].embedded_images) {
|
||||
message[i].embedded_images = [];
|
||||
}
|
||||
message[i].embedded_images.push(imageUrl);
|
||||
message[i].message = ''; // 清空message,避免显示标记文本
|
||||
}
|
||||
|
||||
if (message[i].message.startsWith('[RECORD]')) {
|
||||
let audio = message[i].message.replace('[RECORD]', '');
|
||||
const audioUrl = await this.getMediaFile(audio);
|
||||
message[i].message = `<audio controls class="audio-player">
|
||||
<source src="${audioUrl}" type="audio/wav">
|
||||
${this.t('messages.errors.browser.audioNotSupported')}
|
||||
</audio>`
|
||||
message[i].embedded_audio = audioUrl;
|
||||
message[i].message = ''; // 清空message,避免显示标记文本
|
||||
}
|
||||
|
||||
if (message[i].image_url && message[i].image_url.length > 0) {
|
||||
for (let j = 0; j < message[i].image_url.length; j++) {
|
||||
message[i].image_url[j] = await this.getMediaFile(message[i].image_url[j]);
|
||||
}
|
||||
}
|
||||
|
||||
if (message[i].audio_url) {
|
||||
message[i].audio_url = await this.getMediaFile(message[i].audio_url);
|
||||
}
|
||||
@@ -924,9 +947,6 @@ export default {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chunk_json.type === 'heartbeat') {
|
||||
continue; // 心跳包
|
||||
}
|
||||
if (chunk_json.type === 'error') {
|
||||
console.error('Error received:', chunk_json.data);
|
||||
continue;
|
||||
@@ -937,7 +957,8 @@ export default {
|
||||
const imageUrl = await this.getMediaFile(img);
|
||||
let bot_resp = {
|
||||
type: 'bot',
|
||||
message: `<img src="${imageUrl}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
|
||||
message: '',
|
||||
embedded_images: [imageUrl]
|
||||
}
|
||||
this.messages.push(bot_resp);
|
||||
} else if (chunk_json.type === 'record') {
|
||||
@@ -945,10 +966,8 @@ export default {
|
||||
const audioUrl = await this.getMediaFile(audio);
|
||||
let bot_resp = {
|
||||
type: 'bot',
|
||||
message: `<audio controls class="audio-player">
|
||||
<source src="${audioUrl}" type="audio/wav">
|
||||
${this.t('messages.errors.browser.audioNotSupported')}
|
||||
</audio>`
|
||||
message: '',
|
||||
embedded_audio: audioUrl
|
||||
}
|
||||
this.messages.push(bot_resp);
|
||||
} else if (chunk_json.type === 'plain') {
|
||||
@@ -962,20 +981,19 @@ export default {
|
||||
} else {
|
||||
message_obj.message.value += chunk_json.data;
|
||||
}
|
||||
} else if (chunk_json.type === 'end') {
|
||||
in_streaming = false;
|
||||
// 在消息流结束后初始化代码复制按钮和图片点击事件
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
continue;
|
||||
} else if (chunk_json.type === 'update_title') {
|
||||
// 更新对话标题
|
||||
const conversation = this.conversations.find(c => c.cid === chunk_json.cid);
|
||||
if (conversation) {
|
||||
conversation.title = chunk_json.data;
|
||||
}
|
||||
} else {
|
||||
console.warn('未知数据类型:', chunk_json.type);
|
||||
}
|
||||
if ((chunk_json.type === 'break' && chunk_json.streaming) || !chunk_json.streaming) {
|
||||
// break means a segment end
|
||||
in_streaming = false;
|
||||
// 在消息流结束后初始化代码复制按钮和图片点击事件
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
}
|
||||
this.scrollToBottom();
|
||||
}
|
||||
@@ -1077,19 +1095,43 @@ export default {
|
||||
|
||||
// 复制bot消息到剪贴板
|
||||
copyBotMessage(message, messageIndex) {
|
||||
// 移除HTML标签,获取纯文本
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = message;
|
||||
const plainText = tempDiv.textContent || tempDiv.innerText || message;
|
||||
// 获取对应的消息对象
|
||||
const msgObj = this.messages[messageIndex];
|
||||
let textToCopy = '';
|
||||
|
||||
// 如果有文本消息,添加到复制内容中
|
||||
if (message && message.trim()) {
|
||||
// 移除HTML标签,获取纯文本
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = message;
|
||||
textToCopy = tempDiv.textContent || tempDiv.innerText || message;
|
||||
}
|
||||
|
||||
// 如果有内嵌图片,添加说明
|
||||
if (msgObj && msgObj.embedded_images && msgObj.embedded_images.length > 0) {
|
||||
if (textToCopy) textToCopy += '\n\n';
|
||||
textToCopy += `[包含 ${msgObj.embedded_images.length} 张图片]`;
|
||||
}
|
||||
|
||||
// 如果有内嵌音频,添加说明
|
||||
if (msgObj && msgObj.embedded_audio) {
|
||||
if (textToCopy) textToCopy += '\n\n';
|
||||
textToCopy += '[包含音频内容]';
|
||||
}
|
||||
|
||||
// 如果没有任何内容,使用默认文本
|
||||
if (!textToCopy.trim()) {
|
||||
textToCopy = '[媒体内容]';
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(plainText).then(() => {
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
console.log('消息已复制到剪贴板');
|
||||
this.showCopySuccess(messageIndex);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 如果现代API失败,使用传统方法
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = plainText;
|
||||
textArea.value = textToCopy;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
@@ -1920,4 +1962,39 @@ export default {
|
||||
padding-right: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.embedded-images {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.embedded-image {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bot-embedded-image {
|
||||
max-width: 80%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.bot-embedded-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.embedded-audio {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.embedded-audio .audio-player {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -60,6 +60,7 @@
|
||||
title-field="id"
|
||||
enabled-field="enable"
|
||||
@toggle-enabled="providerStatusChange"
|
||||
:bglogo="getProviderIcon(provider.provider)"
|
||||
@delete="deleteProvider"
|
||||
@edit="configExistingProvider">
|
||||
<template v-slot:details="{ item }">
|
||||
@@ -199,7 +200,7 @@
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="provider-card-logo">
|
||||
<img :src="getProviderIcon(name)" v-if="getProviderIcon(name)" class="provider-logo-img">
|
||||
<img :src="getProviderIcon(template.provider)" v-if="getProviderIcon(template.provider)" class="provider-logo-img">
|
||||
<div v-else class="provider-logo-fallback">
|
||||
{{ name[0].toUpperCase() }}
|
||||
</div>
|
||||
@@ -541,34 +542,27 @@ export default {
|
||||
// 获取提供商类型对应的图标
|
||||
getProviderIcon(type) {
|
||||
const icons = {
|
||||
'OpenAI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
|
||||
'Azure OpenAI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
|
||||
'Whisper': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
|
||||
'xAI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/xai.svg',
|
||||
'Anthropic': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic.svg',
|
||||
'Ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
|
||||
'Gemini': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||||
'Gemini(OpenAI兼容)': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||||
'DeepSeek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
|
||||
'智谱 AI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
|
||||
'硅基流动': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
|
||||
'Kimi': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
|
||||
'PPIO派欧云': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ppio.svg',
|
||||
'Dify': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dify-color.svg',
|
||||
'阿里云百炼': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/alibabacloud-color.svg',
|
||||
'FastGPT': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fastgpt-color.svg',
|
||||
'LM Studio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/lmstudio.svg',
|
||||
'FishAudio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fishaudio.svg',
|
||||
'Azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg',
|
||||
'MiniMax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg',
|
||||
'302.AI': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.53.0/files/icons/ai302.svg',
|
||||
'openai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg',
|
||||
'azure': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/azure.svg',
|
||||
'xai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/xai.svg',
|
||||
'anthropic': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/anthropic.svg',
|
||||
'ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
|
||||
'google': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||||
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
|
||||
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
|
||||
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
|
||||
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
|
||||
'ppio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ppio.svg',
|
||||
'dify': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dify-color.svg',
|
||||
'dashscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/alibabacloud-color.svg',
|
||||
'fastgpt': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fastgpt-color.svg',
|
||||
'lm_studio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/lmstudio.svg',
|
||||
'fishaudio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/fishaudio.svg',
|
||||
'minimax': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg',
|
||||
'302ai': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/1.53.0/files/icons/ai302-color.svg',
|
||||
'microsoft': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/microsoft.svg',
|
||||
};
|
||||
for (const key in icons) {
|
||||
if (type.startsWith(key)) {
|
||||
return icons[key];
|
||||
}
|
||||
}
|
||||
return ''
|
||||
return icons[type] || '';
|
||||
},
|
||||
|
||||
// 获取Tab类型的中文名称
|
||||
|
||||
@@ -135,7 +135,7 @@ class LongTermMemory:
|
||||
return
|
||||
|
||||
if event.get_result() and event.get_result().is_llm_result():
|
||||
final_message = f"[AstrBot/{datetime.datetime.now().strftime('%H:%M:%S')}]: {event.get_result().get_plain_text()}"
|
||||
final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {event.get_result().get_plain_text()}"
|
||||
logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}")
|
||||
self.session_chats[event.unified_msg_origin].append(final_message)
|
||||
if len(self.session_chats[event.unified_msg_origin]) > self.max_cnt:
|
||||
|
||||
+31
-13
@@ -229,9 +229,7 @@ class Main(star.Star):
|
||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = None):
|
||||
"""禁用插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法禁用插件。")
|
||||
)
|
||||
event.set_result(MessageEventResult().message("演示模式下无法禁用插件。"))
|
||||
return
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
@@ -246,9 +244,7 @@ class Main(star.Star):
|
||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = None):
|
||||
"""启用插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法启用插件。")
|
||||
)
|
||||
event.set_result(MessageEventResult().message("演示模式下无法启用插件。"))
|
||||
return
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
@@ -263,9 +259,7 @@ class Main(star.Star):
|
||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = None):
|
||||
"""安装插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(
|
||||
MessageEventResult().message("演示模式下无法安装插件。")
|
||||
)
|
||||
event.set_result(MessageEventResult().message("演示模式下无法安装插件。"))
|
||||
return
|
||||
if not plugin_repo:
|
||||
event.set_result(
|
||||
@@ -1304,12 +1298,36 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
) and not req.contexts:
|
||||
req.contexts[:0] = begin_dialogs
|
||||
|
||||
if quote and quote.message_str:
|
||||
if quote:
|
||||
sender_info = ""
|
||||
if quote.sender_nickname:
|
||||
sender_info = f"(Sent by {quote.sender_nickname})"
|
||||
else:
|
||||
sender_info = ""
|
||||
req.system_prompt += f"\nUser is quoting the message{sender_info}: {quote.message_str}, please consider the context."
|
||||
message_str = quote.message_str or "[Empty Text]"
|
||||
req.system_prompt += (
|
||||
f"\nUser is quoting a message{sender_info}.\n"
|
||||
f"Here are the information of the quoted message: Text Content: {message_str}.\n"
|
||||
)
|
||||
image_seg = None
|
||||
if quote.chain:
|
||||
for comp in quote.chain:
|
||||
if isinstance(comp, Image):
|
||||
image_seg = comp
|
||||
break
|
||||
if image_seg:
|
||||
try:
|
||||
if prov := self.context.get_using_provider(
|
||||
event.unified_msg_origin
|
||||
):
|
||||
llm_resp = await prov.text_chat(
|
||||
prompt="Please describe the image content.",
|
||||
image_urls=[await image_seg.convert_to_file_path()],
|
||||
)
|
||||
if llm_resp.completion_text:
|
||||
req.system_prompt += (
|
||||
f"Image Caption: {llm_resp.completion_text}\n"
|
||||
)
|
||||
except BaseException as e:
|
||||
logger.error(f"处理引用图片失败: {e}")
|
||||
|
||||
if self.ltm:
|
||||
try:
|
||||
|
||||
@@ -129,9 +129,9 @@ class Main(star.Star):
|
||||
logger.info(
|
||||
"Docker 不可用,代码解释器将无法使用,astrbot-python-interpreter 将自动禁用。"
|
||||
)
|
||||
await self.context._star_manager.turn_off_plugin(
|
||||
"astrbot-python-interpreter"
|
||||
)
|
||||
# await self.context._star_manager.turn_off_plugin(
|
||||
# "astrbot-python-interpreter"
|
||||
# )
|
||||
|
||||
async def file_upload(self, file_path: str):
|
||||
"""
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "3.5.19"
|
||||
version = "3.5.22"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -27,7 +27,6 @@ dependencies = [
|
||||
"lark-oapi>=1.4.15",
|
||||
"lxml-html-clean>=0.4.2",
|
||||
"mcp>=1.8.0",
|
||||
"nh3>=0.2.21",
|
||||
"openai>=1.78.0",
|
||||
"ormsgpack>=1.9.1",
|
||||
"pillow>=11.2.1",
|
||||
|
||||
Reference in New Issue
Block a user