Merge branch 'master' of https://github.com/kterna/AstrBot
This commit is contained in:
@@ -41,7 +41,7 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用
|
||||
|
||||
1. **大语言模型对话**。支持各种大语言模型,包括 OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM 等,支持接入本地部署的大模型,通过 Ollama、LLMTuner。具有多轮对话、人格情境、多模态能力,支持图片理解、语音转文字(Whisper)。
|
||||
2. **多消息平台接入**。支持接入 QQ(OneBot)、QQ 频道、微信(Gewechat)、飞书、Telegram。后续将支持钉钉、Discord、WhatsApp、小爱音响。支持速率限制、白名单、关键词过滤、百度内容审核。
|
||||
3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://astrbot.app/others/dify.html),便捷接入 Dify 智能助手、知识库和 Dify 工作流。
|
||||
3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://dify.ai/),便捷接入 Dify 智能助手、知识库和 Dify 工作流。
|
||||
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,极简开发。已支持安装多个插件。
|
||||
5. **可视化管理面板**。支持可视化修改配置、插件管理、日志查看等功能,降低配置难度。集成 WebChat,可在面板上与大模型对话。
|
||||
6. **高稳定性、高模块化**。基于事件总线和流水线的架构设计,高度模块化,低耦合。
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ AstrBot is a loosely coupled, asynchronous chatbot and development framework tha
|
||||
|
||||
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.
|
||||
3. **Agent Capabilities** - Native support for code execution, natural language TODO lists, web search. Integrates with [Dify Platform](https://dify.ai/) 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.
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ AstrBot は、疎結合、非同期、複数のメッセージプラットフォ
|
||||
|
||||
1. **大規模言語モデルの対話**。OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM など、さまざまな大規模言語モデルをサポートし、Ollama、LLMTuner を介してローカルにデプロイされた大規模モデルをサポートします。多輪対話、人格シナリオ、多モーダル機能を備え、画像理解、音声からテキストへの変換(Whisper)をサポートします。
|
||||
2. **複数のメッセージプラットフォームの接続**。QQ(OneBot)、QQ チャンネル、WeChat(Gewechat)、Feishu、Telegram への接続をサポートします。今後、DingTalk、Discord、WhatsApp、Xiaoai 音響をサポートする予定です。レート制限、ホワイトリスト、キーワードフィルタリング、Baidu コンテンツ監査をサポートします。
|
||||
3. **エージェント**。一部のエージェント機能をネイティブにサポートし、コードエグゼキューター、自然言語タスク、ウェブ検索などを提供します。[Dify プラットフォーム](https://astrbot.app/others/dify.html)と連携し、Dify スマートアシスタント、ナレッジベース、Dify ワークフローを簡単に接続できます。
|
||||
3. **エージェント**。一部のエージェント機能をネイティブにサポートし、コードエグゼキューター、自然言語タスク、ウェブ検索などを提供します。[Dify プラットフォーム](https://dify.ai/)と連携し、Dify スマートアシスタント、ナレッジベース、Dify ワークフローを簡単に接続できます。
|
||||
4. **プラグインの拡張**。深く最適化されたプラグインメカニズムを備え、[プラグインの開発](https://astrbot.app/dev/plugin.html)をサポートし、機能を拡張できます。複数のプラグインのインストールをサポートします。
|
||||
5. **ビジュアル管理パネル**。設定の視覚的な変更、プラグイン管理、ログの表示などをサポートし、設定の難易度を低減します。WebChat を統合し、パネル上で大規模モデルと対話できます。
|
||||
6. **高い安定性と高いモジュール性**。イベントバスとパイプラインに基づくアーキテクチャ設計により、高度にモジュール化され、低結合です。
|
||||
|
||||
@@ -2,11 +2,7 @@ from astrbot.core.star.register import (
|
||||
register_star as register, # 注册插件(Star)
|
||||
)
|
||||
|
||||
from astrbot.core.star import Context, Star
|
||||
from astrbot.core.star import Context, Star, StarTools
|
||||
from astrbot.core.star.config import *
|
||||
|
||||
__all__ = [
|
||||
"register",
|
||||
"Context",
|
||||
"Star",
|
||||
]
|
||||
__all__ = ["register", "Context", "Star", "StarTools"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。
|
||||
"""
|
||||
|
||||
VERSION = "3.5.1"
|
||||
VERSION = "3.5.2"
|
||||
DB_PATH = "data/data_v3.db"
|
||||
|
||||
# 默认配置
|
||||
@@ -98,6 +98,7 @@ DEFAULT_CONFIG = {
|
||||
"plugin_repo_mirror": "",
|
||||
"knowledge_db": {},
|
||||
"persona": [],
|
||||
"timezone": "",
|
||||
}
|
||||
|
||||
|
||||
@@ -522,6 +523,12 @@ CONFIG_METADATA_2 = {
|
||||
"model": "gemini-2.0-flash-exp",
|
||||
},
|
||||
"gm_resp_image_modal": False,
|
||||
"gm_safety_settings": {
|
||||
"harassment": "BLOCK_MEDIUM_AND_ABOVE",
|
||||
"hate_speech": "BLOCK_MEDIUM_AND_ABOVE",
|
||||
"sexually_explicit": "BLOCK_MEDIUM_AND_ABOVE",
|
||||
"dangerous_content": "BLOCK_MEDIUM_AND_ABOVE",
|
||||
},
|
||||
},
|
||||
"DeepSeek": {
|
||||
"id": "deepseek_default",
|
||||
@@ -671,17 +678,82 @@ CONFIG_METADATA_2 = {
|
||||
"fishaudio-tts-character": "可莉",
|
||||
"timeout": "20",
|
||||
},
|
||||
"阿里云百炼_TTS(API)": {
|
||||
"id": "dashscope_tts",
|
||||
"type": "dashscope_tts",
|
||||
"enable": False,
|
||||
"api_key": "",
|
||||
"model": "cosyvoice-v1",
|
||||
"dashscope_tts_voice": "loongstella",
|
||||
"timeout": "20",
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
"dashscope_tts_voice": {
|
||||
"description": "语音合成模型",
|
||||
"type": "string",
|
||||
"hint": "阿里云百炼语音合成模型名称。具体可参考 https://help.aliyun.com/zh/model-studio/developer-reference/cosyvoice-python-api 等内容",
|
||||
},
|
||||
"gm_resp_image_modal": {
|
||||
"description": "启用图片模态",
|
||||
"type": "bool",
|
||||
"hint": "启用后,将支持返回图片内容。需要模型支持,否则会报错。具体支持模型请查看 Google Gemini 官方网站。温馨提示,如果您需要生成图片,请关闭 `启用群员识别` 配置获得更好的效果。",
|
||||
},
|
||||
"gm_safety_settings": {
|
||||
"description": "安全过滤器",
|
||||
"type": "object",
|
||||
"hint": "设置模型输入的内容安全过滤级别。过滤级别分类为NONE(不屏蔽)、HIGH(高风险时屏蔽)、MEDIUM_AND_ABOVE(中等风险及以上屏蔽)、LOW_AND_ABOVE(低风险及以上时屏蔽),具体参见Gemini API文档。",
|
||||
"items": {
|
||||
"harassment": {
|
||||
"description": "骚扰内容",
|
||||
"type": "string",
|
||||
"hint": "负面或有害评论",
|
||||
"options": [
|
||||
"BLOCK_NONE",
|
||||
"BLOCK_ONLY_HIGH",
|
||||
"BLOCK_MEDIUM_AND_ABOVE",
|
||||
"BLOCK_LOW_AND_ABOVE",
|
||||
],
|
||||
},
|
||||
"hate_speech": {
|
||||
"description": "仇恨言论",
|
||||
"type": "string",
|
||||
"hint": "粗鲁、无礼或亵渎性质内容",
|
||||
"options": [
|
||||
"BLOCK_NONE",
|
||||
"BLOCK_ONLY_HIGH",
|
||||
"BLOCK_MEDIUM_AND_ABOVE",
|
||||
"BLOCK_LOW_AND_ABOVE",
|
||||
],
|
||||
},
|
||||
"sexually_explicit": {
|
||||
"description": "露骨色情内容",
|
||||
"type": "string",
|
||||
"hint": "包含性行为或其他淫秽内容的引用",
|
||||
"options": [
|
||||
"BLOCK_NONE",
|
||||
"BLOCK_ONLY_HIGH",
|
||||
"BLOCK_MEDIUM_AND_ABOVE",
|
||||
"BLOCK_LOW_AND_ABOVE",
|
||||
],
|
||||
},
|
||||
"dangerous_content": {
|
||||
"description": "危险内容",
|
||||
"type": "string",
|
||||
"hint": "宣扬、助长或鼓励有害行为的信息",
|
||||
"options": [
|
||||
"BLOCK_NONE",
|
||||
"BLOCK_ONLY_HIGH",
|
||||
"BLOCK_MEDIUM_AND_ABOVE",
|
||||
"BLOCK_LOW_AND_ABOVE",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"rag_options": {
|
||||
"description": "RAG 选项",
|
||||
"type": "object",
|
||||
"hint": "检索知识库设置, 非必填。仅 Agent 应用类型支持(智能体应用, 包括 RAG 应用)",
|
||||
"hint": "检索知识库设置, 非必填。仅 Agent 应用类型支持(智能体应用, 包括 RAG 应用)。阿里云百炼应用开启此功能后将无法多轮对话。",
|
||||
"items": {
|
||||
"pipeline_ids": {
|
||||
"description": "知识库 ID 列表",
|
||||
@@ -1101,6 +1173,12 @@ CONFIG_METADATA_2 = {
|
||||
"type": "string",
|
||||
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
|
||||
},
|
||||
"timezone": {
|
||||
"description": "时区",
|
||||
"type": "string",
|
||||
"obvious_hint": True,
|
||||
"hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab",
|
||||
},
|
||||
"log_level": {
|
||||
"description": "控制台日志级别",
|
||||
"type": "string",
|
||||
|
||||
@@ -190,7 +190,6 @@ class AstrBotCoreLifecycle:
|
||||
task.cancel()
|
||||
|
||||
for plugin in self.plugin_manager.context.get_all_stars():
|
||||
logger.info(f"正在终止插件 {plugin.name} ...")
|
||||
try:
|
||||
await self.plugin_manager._terminate_plugin(plugin)
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import json
|
||||
import aiosqlite
|
||||
import os
|
||||
from typing import Any
|
||||
from .plugin_storage import PluginStorage
|
||||
|
||||
DBPATH = "data/plugin_data/sqlite/plugin_data.db"
|
||||
|
||||
|
||||
class SQLitePluginStorage(PluginStorage):
|
||||
"""插件数据的 SQLite 存储实现类。
|
||||
|
||||
该类提供异步方式将插件数据存储到 SQLite 数据库中,支持数据的增删改查操作。
|
||||
所有数据以 (plugin, key) 作为复合主键进行索引。
|
||||
"""
|
||||
|
||||
_instance = None # Standalone instance of the class
|
||||
_db_conn = None
|
||||
db_path = None
|
||||
|
||||
def __new__(cls):
|
||||
"""
|
||||
创建或获取 SQLitePluginStorage 的单例实例。
|
||||
如果实例已存在,则返回现有实例;否则创建一个新实例。
|
||||
数据在 `data/plugin_data/sqlite/plugin_data.db` 下。
|
||||
"""
|
||||
os.makedirs(os.path.dirname(DBPATH), exist_ok=True)
|
||||
if cls._instance is None:
|
||||
cls._instance = super(SQLitePluginStorage, cls).__new__(cls)
|
||||
cls._instance.db_path = DBPATH
|
||||
return cls._instance
|
||||
|
||||
async def _init_db(self):
|
||||
"""初始化数据库连接(只执行一次)"""
|
||||
if SQLitePluginStorage._db_conn is None:
|
||||
SQLitePluginStorage._db_conn = await aiosqlite.connect(self.db_path)
|
||||
await self._setup_db()
|
||||
|
||||
async def _setup_db(self):
|
||||
"""
|
||||
异步初始化数据库。
|
||||
|
||||
创建插件数据表,如果表不存在则创建,表结构包含 plugin、key 和 value 字段,
|
||||
其中 plugin 和 key 组合作为主键。
|
||||
"""
|
||||
await self._db_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS plugin_data (
|
||||
plugin TEXT,
|
||||
key TEXT,
|
||||
value TEXT,
|
||||
PRIMARY KEY (plugin, key)
|
||||
)
|
||||
""")
|
||||
await self._db_conn.commit()
|
||||
|
||||
async def set(self, plugin: str, key: str, value: Any):
|
||||
"""
|
||||
异步存储数据。
|
||||
|
||||
将指定插件的键值对存入数据库,如果键已存在则更新值。
|
||||
值会被序列化为 JSON 字符串后存储。
|
||||
|
||||
Args:
|
||||
plugin: 插件标识符
|
||||
key: 数据键名
|
||||
value: 要存储的数据值(任意类型,将被 JSON 序列化)
|
||||
"""
|
||||
await self._init_db()
|
||||
await self._db_conn.execute(
|
||||
"INSERT INTO plugin_data (plugin, key, value) VALUES (?, ?, ?) "
|
||||
"ON CONFLICT(plugin, key) DO UPDATE SET value = excluded.value",
|
||||
(plugin, key, json.dumps(value)),
|
||||
)
|
||||
await self._db_conn.commit()
|
||||
|
||||
async def get(self, plugin: str, key: str) -> Any:
|
||||
"""
|
||||
异步获取数据。
|
||||
|
||||
从数据库中获取指定插件和键名对应的值,
|
||||
返回的值会从 JSON 字符串反序列化为原始数据类型。
|
||||
|
||||
Args:
|
||||
plugin: 插件标识符
|
||||
key: 数据键名
|
||||
|
||||
Returns:
|
||||
Any: 存储的数据值,如果未找到则返回 None
|
||||
"""
|
||||
await self._init_db()
|
||||
async with self._db_conn.execute(
|
||||
"SELECT value FROM plugin_data WHERE plugin = ? AND key = ?",
|
||||
(plugin, key),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
return json.loads(row[0]) if row else None
|
||||
|
||||
async def delete(self, plugin: str, key: str):
|
||||
"""
|
||||
异步删除数据。
|
||||
|
||||
从数据库中删除指定插件和键名对应的数据项。
|
||||
|
||||
Args:
|
||||
plugin: 插件标识符
|
||||
key: 要删除的数据键名
|
||||
"""
|
||||
await self._init_db()
|
||||
await self._db_conn.execute(
|
||||
"DELETE FROM plugin_data WHERE plugin = ? AND key = ?", (plugin, key)
|
||||
)
|
||||
await self._db_conn.commit()
|
||||
+8
-3
@@ -108,11 +108,12 @@ class LogBroker:
|
||||
"""
|
||||
self.subscribers.remove(q)
|
||||
|
||||
def publish(self, log_entry: str):
|
||||
def publish(self, log_entry: dict):
|
||||
"""发布新日志到所有订阅者, 使用非阻塞方式投递, 避免一个订阅者阻塞整个系统
|
||||
|
||||
Args:
|
||||
log_entry (str): 日志消息, 可以是字符串或字典
|
||||
log_entry (dict): 日志消息, 包含日志级别和日志内容.
|
||||
example: {"level": "INFO", "data": "This is a log message.", "time": "2023-10-01 12:00:00"}
|
||||
"""
|
||||
self.log_cache.append(log_entry)
|
||||
for q in self.subscribers:
|
||||
@@ -140,7 +141,11 @@ class LogQueueHandler(logging.Handler):
|
||||
record (logging.LogRecord): 日志记录对象, 包含日志信息
|
||||
"""
|
||||
log_entry = self.format(record)
|
||||
self.log_broker.publish(log_entry)
|
||||
self.log_broker.publish({
|
||||
"level": record.levelname,
|
||||
"time": record.asctime,
|
||||
"data": log_entry,
|
||||
})
|
||||
|
||||
|
||||
class LogManager:
|
||||
|
||||
@@ -58,9 +58,9 @@ class LLMRequestSubStage(Stage):
|
||||
|
||||
if event.get_extra("provider_request"):
|
||||
req = event.get_extra("provider_request")
|
||||
assert isinstance(
|
||||
req, ProviderRequest
|
||||
), "provider_request 必须是 ProviderRequest 类型。"
|
||||
assert isinstance(req, ProviderRequest), (
|
||||
"provider_request 必须是 ProviderRequest 类型。"
|
||||
)
|
||||
|
||||
if req.conversation:
|
||||
req.contexts = json.loads(req.conversation.history)
|
||||
@@ -80,7 +80,6 @@ class LLMRequestSubStage(Stage):
|
||||
conversation_id = await self.conv_manager.get_curr_conversation_id(
|
||||
event.unified_msg_origin
|
||||
)
|
||||
req.session_id = event.unified_msg_origin
|
||||
if not conversation_id:
|
||||
conversation_id = await self.conv_manager.new_conversation(
|
||||
event.unified_msg_origin
|
||||
@@ -134,6 +133,10 @@ class LLMRequestSubStage(Stage):
|
||||
logger.debug("上下文长度超过限制,将截断。")
|
||||
req.contexts = req.contexts[-self.max_context_length * 2 :]
|
||||
|
||||
# session_id
|
||||
if not req.session_id:
|
||||
req.session_id = event.unified_msg_origin
|
||||
|
||||
try:
|
||||
need_loop = True
|
||||
while need_loop:
|
||||
|
||||
@@ -2,6 +2,7 @@ import random
|
||||
import asyncio
|
||||
import math
|
||||
import traceback
|
||||
import astrbot.core.message.components as Comp
|
||||
from typing import Union, AsyncGenerator
|
||||
from ..stage import register_stage, Stage
|
||||
from ..context import PipelineContext
|
||||
@@ -11,11 +12,42 @@ from astrbot.core import logger
|
||||
from astrbot.core.message.message_event_result import BaseMessageComponent
|
||||
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
||||
from astrbot.core.star.star import star_map
|
||||
from astrbot.core.message.components import Plain, Reply, At
|
||||
|
||||
|
||||
@register_stage
|
||||
class RespondStage(Stage):
|
||||
# 组件类型到其非空判断函数的映射
|
||||
_component_validators = {
|
||||
Comp.Plain: lambda comp: bool(comp.text and comp.text.strip()), # 纯文本消息需要strip
|
||||
Comp.Face: lambda comp: comp.id is not None, # QQ表情
|
||||
Comp.Record: lambda comp: bool(comp.file), # 语音
|
||||
Comp.Video: lambda comp: bool(comp.file), # 视频
|
||||
Comp.At: lambda comp: bool(comp.qq) or bool(comp.name), # @
|
||||
Comp.AtAll: lambda comp: True, # @所有人
|
||||
Comp.RPS: lambda comp: True, # 不知道是啥(未完成)
|
||||
Comp.Dice: lambda comp: True, # 骰子(未完成)
|
||||
Comp.Shake: lambda comp: True, # 摇一摇(未完成)
|
||||
Comp.Anonymous: lambda comp: True, # 匿名(未完成)
|
||||
Comp.Share: lambda comp: bool(comp.url) and bool(comp.title), # 分享
|
||||
Comp.Contact: lambda comp: True, # 联系人(未完成)
|
||||
Comp.Location: lambda comp: bool(comp.lat and comp.lon), # 位置
|
||||
Comp.Music: lambda comp: bool(comp._type) and bool(comp.url) and bool(comp.audio), # 音乐
|
||||
Comp.Image: lambda comp: bool(comp.file), # 图片
|
||||
Comp.Reply: lambda comp: bool(comp.id) and comp.sender_id is not None, # 回复
|
||||
Comp.RedBag: lambda comp: bool(comp.title), # 红包
|
||||
Comp.Poke: lambda comp: comp.id != 0 and comp.qq != 0, # 戳一戳
|
||||
Comp.Forward: lambda comp: bool(comp.id and comp.id.strip()), # 转发
|
||||
Comp.Node: lambda comp: bool(comp.name) and comp.uin != 0 and bool(comp.content), # 一个转发节点
|
||||
Comp.Nodes: lambda comp: bool(comp.nodes), # 多个转发节点
|
||||
Comp.Xml: lambda comp: bool(comp.data and comp.data.strip()), # XML
|
||||
Comp.Json: lambda comp: bool(comp.data), # JSON
|
||||
Comp.CardImage: lambda comp: bool(comp.file), # 卡片图片
|
||||
Comp.TTS: lambda comp: bool(comp.text and comp.text.strip()), # 语音合成
|
||||
Comp.Unknown: lambda comp: bool(comp.text and comp.text.strip()), # 未知消息
|
||||
Comp.File: lambda comp: bool(comp.file), # 文件
|
||||
Comp.WechatEmoji: lambda comp: bool(comp.md5), # 微信表情
|
||||
}
|
||||
|
||||
async def initialize(self, ctx: PipelineContext):
|
||||
self.ctx = ctx
|
||||
|
||||
@@ -62,7 +94,7 @@ class RespondStage(Stage):
|
||||
async def _calc_comp_interval(self, comp: BaseMessageComponent) -> float:
|
||||
"""分段回复 计算间隔时间"""
|
||||
if self.interval_method == "log":
|
||||
if isinstance(comp, Plain):
|
||||
if isinstance(comp, Comp.Plain):
|
||||
wc = await self._word_cnt(comp.text)
|
||||
i = math.log(wc + 1, self.log_base)
|
||||
return random.uniform(i, i + 0.5)
|
||||
@@ -72,6 +104,28 @@ class RespondStage(Stage):
|
||||
# random
|
||||
return random.uniform(self.interval[0], self.interval[1])
|
||||
|
||||
async def _is_empty_message_chain(self, chain: list[BaseMessageComponent]):
|
||||
"""检查消息链是否为空
|
||||
|
||||
Args:
|
||||
chain (list[BaseMessageComponent]): 包含消息对象的列表
|
||||
"""
|
||||
if not chain:
|
||||
return True
|
||||
|
||||
for comp in chain:
|
||||
comp_type = type(comp)
|
||||
|
||||
# 检查组件类型是否在字典中
|
||||
if comp_type in self._component_validators:
|
||||
if self._component_validators[comp_type](comp):
|
||||
return False
|
||||
else:
|
||||
logger.info(f"空内容检查: 无法识别的组件类型: {comp_type.__name__}")
|
||||
|
||||
# 如果所有组件都为空
|
||||
return True
|
||||
|
||||
async def process(
|
||||
self, event: AstrMessageEvent
|
||||
) -> Union[None, AsyncGenerator[None, None]]:
|
||||
@@ -82,6 +136,16 @@ class RespondStage(Stage):
|
||||
if len(result.chain) > 0:
|
||||
await event._pre_send()
|
||||
|
||||
# 检查消息链是否为空
|
||||
try:
|
||||
if await self._is_empty_message_chain(result.chain):
|
||||
logger.info("消息为空,跳过发送阶段")
|
||||
event.clear_result()
|
||||
event.stop_event()
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning(f"空内容检查异常: {e}")
|
||||
|
||||
if self.enable_seg and (
|
||||
(self.only_llm_result and result.is_llm_result())
|
||||
or not self.only_llm_result
|
||||
@@ -89,13 +153,13 @@ class RespondStage(Stage):
|
||||
decorated_comps = []
|
||||
if self.reply_with_mention:
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, At):
|
||||
if isinstance(comp, Comp.At):
|
||||
decorated_comps.append(comp)
|
||||
result.chain.remove(comp)
|
||||
break
|
||||
if self.reply_with_quote:
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Reply):
|
||||
if isinstance(comp, Comp.Reply):
|
||||
decorated_comps.append(comp)
|
||||
result.chain.remove(comp)
|
||||
break
|
||||
|
||||
@@ -156,9 +156,7 @@ class ResultDecorateStage(Stage):
|
||||
self.ctx.astrbot_config["provider_tts_settings"]["enable"]
|
||||
and result.is_llm_result()
|
||||
):
|
||||
tts_provider = (
|
||||
self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst
|
||||
)
|
||||
tts_provider = self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst
|
||||
new_chain = []
|
||||
for comp in result.chain:
|
||||
if isinstance(comp, Plain) and len(comp.text) > 1:
|
||||
|
||||
@@ -15,6 +15,9 @@ class WhitelistCheckStage(Stage):
|
||||
"enable_id_white_list"
|
||||
]
|
||||
self.whitelist = ctx.astrbot_config["platform_settings"]["id_whitelist"]
|
||||
self.whitelist = [
|
||||
str(i).strip() for i in self.whitelist if str(i).strip() != ""
|
||||
]
|
||||
self.wl_ignore_admin_on_group = ctx.astrbot_config["platform_settings"][
|
||||
"wl_ignore_admin_on_group"
|
||||
]
|
||||
@@ -53,7 +56,7 @@ class WhitelistCheckStage(Stage):
|
||||
return
|
||||
if (
|
||||
event.unified_msg_origin not in self.whitelist
|
||||
and event.get_group_id() not in self.whitelist
|
||||
and str(event.get_group_id()).strip() not in self.whitelist
|
||||
):
|
||||
if self.wl_log:
|
||||
logger.info(
|
||||
|
||||
@@ -24,7 +24,7 @@ class DingtalkMessageEvent(AstrMessageEvent):
|
||||
if isinstance(segment, Comp.Plain):
|
||||
segment.text = segment.text.strip()
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None, client.reply_text, segment.text, self.message_obj.raw_message
|
||||
None, client.reply_markdown, "AstrBot", segment.text, self.message_obj.raw_message
|
||||
)
|
||||
elif isinstance(segment, Comp.Image):
|
||||
markdown_str = ""
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import telegramify_markdown
|
||||
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, File, Record
|
||||
from telegram.ext import ExtBot
|
||||
from astrbot.core.utils.io import download_file
|
||||
from astrbot import logger
|
||||
|
||||
|
||||
class TelegramPlatformEvent(AstrMessageEvent):
|
||||
@@ -49,7 +51,17 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
||||
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)
|
||||
text = i.text
|
||||
try:
|
||||
text = telegramify_markdown.markdownify(
|
||||
i.text, max_line_length=None, normalize_whitespace=False
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"MarkdownV2 conversion failed: {e}. Using plain text instead."
|
||||
)
|
||||
return
|
||||
await client.send_message(text=text, parse_mode="MarkdownV2", **payload)
|
||||
elif isinstance(i, Image):
|
||||
image_path = await i.convert_to_file_path()
|
||||
await client.send_photo(photo=image_path, **payload)
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
import base64
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.message_components import Plain, Image
|
||||
from astrbot.api.message_components import Plain, Image, Record
|
||||
from astrbot.core.utils.io import download_image_by_url
|
||||
from astrbot.core import web_chat_back_queue
|
||||
|
||||
@@ -47,6 +47,22 @@ class WebChatMessageEvent(AstrMessageEvent):
|
||||
with open(comp.file, "rb") as f2:
|
||||
f.write(f2.read())
|
||||
web_chat_back_queue.put_nowait((f"[IMAGE]{filename}", cid))
|
||||
elif isinstance(comp, Record):
|
||||
# save record to local
|
||||
filename = str(uuid.uuid4()) + ".wav"
|
||||
path = os.path.join(imgs_dir, filename)
|
||||
if comp.file and comp.file.startswith("file:///"):
|
||||
ph = comp.file[8:]
|
||||
with open(path, "wb") as f:
|
||||
with open(ph, "rb") as f2:
|
||||
f.write(f2.read())
|
||||
elif comp.file and comp.file.startswith("http"):
|
||||
await download_image_by_url(comp.file, path=path)
|
||||
else:
|
||||
with open(path, "wb") as f:
|
||||
with open(comp.file, "rb") as f2:
|
||||
f.write(f2.read())
|
||||
web_chat_back_queue.put_nowait((f"[RECORD]{filename}", cid))
|
||||
else:
|
||||
logger.debug(f"webchat 忽略: {comp.type}")
|
||||
web_chat_back_queue.put_nowait(None)
|
||||
|
||||
@@ -198,6 +198,10 @@ class ProviderManager:
|
||||
from .sources.fishaudio_tts_api_source import (
|
||||
ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI,
|
||||
)
|
||||
case "dashscope_tts":
|
||||
from .sources.dashscope_tts import (
|
||||
ProviderDashscopeTTSAPI as ProviderDashscopeTTSAPI,
|
||||
)
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.critical(
|
||||
f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。"
|
||||
|
||||
@@ -51,10 +51,14 @@ class ProviderDashscope(ProviderOpenAIOfficial):
|
||||
self.timeout = int(self.timeout)
|
||||
|
||||
def has_rag_options(self):
|
||||
if (
|
||||
self.rag_options
|
||||
and self.rag_options.get("pipeline_ids", None)
|
||||
and self.rag_options.get("file_ids", None)
|
||||
"""判断是否有 RAG 选项
|
||||
|
||||
Returns:
|
||||
bool: 是否有 RAG 选项
|
||||
"""
|
||||
if self.rag_options and (
|
||||
len(self.rag_options.get("pipeline_ids", [])) > 0
|
||||
or len(self.rag_options.get("file_ids", [])) > 0
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@@ -78,7 +82,7 @@ class ProviderDashscope(ProviderOpenAIOfficial):
|
||||
|
||||
if (
|
||||
self.dashscope_app_type in ["agent", "dialog-workflow"]
|
||||
and self.has_rag_options()
|
||||
and not self.has_rag_options()
|
||||
):
|
||||
# 支持多轮对话的
|
||||
new_record = {"role": "user", "content": prompt}
|
||||
@@ -92,12 +96,15 @@ class ProviderDashscope(ProviderOpenAIOfficial):
|
||||
if "_no_save" in part:
|
||||
del part["_no_save"]
|
||||
# 调用阿里云百炼 API
|
||||
payload = {
|
||||
"app_id": self.app_id,
|
||||
"api_key": self.api_key,
|
||||
"messages": context_query,
|
||||
"biz_params": payload_vars or None,
|
||||
}
|
||||
partial = functools.partial(
|
||||
Application.call,
|
||||
app_id=self.app_id,
|
||||
api_key=self.api_key,
|
||||
messages=context_query,
|
||||
biz_params=payload_vars or None,
|
||||
**payload,
|
||||
)
|
||||
response = await asyncio.get_event_loop().run_in_executor(None, partial)
|
||||
else:
|
||||
@@ -134,7 +141,8 @@ class ProviderDashscope(ProviderOpenAIOfficial):
|
||||
if self.output_reference and response.output.get("doc_references", None):
|
||||
ref_str = ""
|
||||
for ref in response.output.get("doc_references", []):
|
||||
ref_str += f"{ref['index_id']}. {ref['title']}\n"
|
||||
ref_title = ref.get("title", "") if ref.get("title") else ref.get("doc_name", "")
|
||||
ref_str += f"{ref['index_id']}. {ref_title}\n"
|
||||
output_text += f"\n\n回答来源:\n{ref_str}"
|
||||
|
||||
return LLMResponse(role="assistant", completion_text=output_text)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import dashscope
|
||||
import uuid
|
||||
import asyncio
|
||||
from dashscope.audio.tts_v2 import *
|
||||
from ..provider import TTSProvider
|
||||
from ..entites import ProviderType
|
||||
from ..register import register_provider_adapter
|
||||
|
||||
|
||||
@register_provider_adapter(
|
||||
"dashscope_tts", "Dashscope TTS API", provider_type=ProviderType.TEXT_TO_SPEECH
|
||||
)
|
||||
class ProviderDashscopeTTSAPI(TTSProvider):
|
||||
def __init__(
|
||||
self,
|
||||
provider_config: dict,
|
||||
provider_settings: dict,
|
||||
) -> None:
|
||||
super().__init__(provider_config, provider_settings)
|
||||
self.chosen_api_key: str = provider_config.get("api_key", "")
|
||||
self.voice: str = provider_config.get("dashscope_tts_voice", "loongstella")
|
||||
self.set_model(provider_config.get("model", None))
|
||||
self.timeout_ms = float(provider_config.get("timeout", 20))*1000
|
||||
|
||||
dashscope.api_key = self.chosen_api_key
|
||||
self.synthesizer = SpeechSynthesizer(
|
||||
model=self.get_model(),
|
||||
voice=self.voice,
|
||||
format=AudioFormat.WAV_24000HZ_MONO_16BIT,
|
||||
)
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
path = f"data/temp/dashscope_tts_{uuid.uuid4()}.wav"
|
||||
audio = await asyncio.get_event_loop().run_in_executor(
|
||||
None, self.synthesizer.call, text, self.timeout_ms
|
||||
)
|
||||
with open(path, "wb") as f:
|
||||
f.write(audio)
|
||||
return path
|
||||
@@ -63,9 +63,7 @@ class ProviderEdgeTTS(TTSProvider):
|
||||
ff = FFmpeg()
|
||||
ff.convert(input=mp3_path, output=wav_path)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换"
|
||||
)
|
||||
logger.debug(f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换")
|
||||
# use ffmpeg command line
|
||||
|
||||
# 使用ffmpeg将MP3转换为标准WAV格式
|
||||
|
||||
@@ -43,6 +43,7 @@ class SimpleGoogleGenAIClient:
|
||||
system_instruction: str = "",
|
||||
tools: dict = None,
|
||||
modalities: List[str] = ["Text"],
|
||||
safety_settings: List[dict] = [],
|
||||
):
|
||||
payload = {}
|
||||
if system_instruction:
|
||||
@@ -53,6 +54,10 @@ class SimpleGoogleGenAIClient:
|
||||
payload["generationConfig"] = {
|
||||
"responseModalities": modalities,
|
||||
}
|
||||
payload["safetySettings"] = [
|
||||
{"category": s["category"], "threshold": s["threshold"]}
|
||||
for s in safety_settings
|
||||
]
|
||||
logger.debug(f"payload: {payload}")
|
||||
request_url = (
|
||||
f"{self.api_base}/v1beta/models/{model}:generateContent?key={self.api_key}"
|
||||
@@ -106,6 +111,21 @@ class ProviderGoogleGenAI(Provider):
|
||||
)
|
||||
self.set_model(provider_config["model_config"]["model"])
|
||||
|
||||
safety_mapping = {
|
||||
"harassment": "HARM_CATEGORY_HARASSMENT",
|
||||
"hate_speech": "HARM_CATEGORY_HATE_SPEECH",
|
||||
"sexually_explicit": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"dangerous_content": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
}
|
||||
|
||||
self.safety_settings = []
|
||||
user_safety_config = self.provider_config.get("gm_safety_settings", {})
|
||||
for config_key, harm_category in safety_mapping.items():
|
||||
if threshold := user_safety_config.get(config_key):
|
||||
self.safety_settings.append(
|
||||
{"category": harm_category, "threshold": threshold}
|
||||
)
|
||||
|
||||
async def get_models(self):
|
||||
return await self.client.models_list()
|
||||
|
||||
@@ -127,7 +147,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
if message["role"] == "user":
|
||||
if isinstance(message["content"], str):
|
||||
if not message["content"]:
|
||||
message["content"] = "<empty_content>"
|
||||
message["content"] = ""
|
||||
|
||||
google_genai_conversation.append(
|
||||
{"role": "user", "parts": [{"text": message["content"]}]}
|
||||
@@ -138,7 +158,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
for part in message["content"]:
|
||||
if part["type"] == "text":
|
||||
if not part["text"]:
|
||||
part["text"] = "<empty_content>"
|
||||
part["text"] = ""
|
||||
parts.append({"text": part["text"]})
|
||||
elif part["type"] == "image_url":
|
||||
parts.append(
|
||||
@@ -156,7 +176,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
elif message["role"] == "assistant":
|
||||
if "content" in message:
|
||||
if not message["content"]:
|
||||
message["content"] = "<empty_content>"
|
||||
message["content"] = ""
|
||||
google_genai_conversation.append(
|
||||
{"role": "model", "parts": [{"text": message["content"]}]}
|
||||
)
|
||||
@@ -205,6 +225,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
system_instruction=system_instruction,
|
||||
tools=tool,
|
||||
modalities=modalites,
|
||||
safety_settings=self.safety_settings,
|
||||
)
|
||||
logger.debug(f"result: {result}")
|
||||
|
||||
|
||||
@@ -221,8 +221,8 @@ class ProviderOpenAIOfficial(Provider):
|
||||
payloads["messages"] = new_contexts
|
||||
elif (
|
||||
"Function calling is not enabled" in str(e)
|
||||
or ("tool" in str(e) and "support" in str(e).lower())
|
||||
or ("function" in str(e) and "support" in str(e).lower())
|
||||
or ("tool" in str(e).lower() and "support" in str(e).lower())
|
||||
or ("function" in str(e).lower() and "support" in str(e).lower())
|
||||
):
|
||||
# openai, ollama, gemini openai, siliconcloud 的错误提示与 code 不统一,只能通过字符串匹配
|
||||
logger.info(
|
||||
|
||||
@@ -4,12 +4,14 @@ from .context import Context
|
||||
from astrbot.core.provider import Provider
|
||||
from astrbot.core.utils.command_parser import CommandParserMixin
|
||||
from astrbot.core import html_renderer
|
||||
from astrbot.core.star.star_tools import StarTools
|
||||
|
||||
|
||||
class Star(CommandParserMixin):
|
||||
"""所有插件(Star)的父类,所有插件都应该继承于这个类"""
|
||||
|
||||
def __init__(self, context: Context):
|
||||
StarTools.initialize(context)
|
||||
self.context = context
|
||||
|
||||
async def text_to_image(self, text: str, return_url=True) -> str:
|
||||
@@ -27,4 +29,4 @@ class Star(CommandParserMixin):
|
||||
pass
|
||||
|
||||
|
||||
__all__ = ["Star", "StarMetadata", "PluginManager", "Context", "Provider"]
|
||||
__all__ = ["Star", "StarMetadata", "PluginManager", "Context", "Provider", "StarTools"]
|
||||
|
||||
@@ -596,7 +596,7 @@ class PluginManager:
|
||||
asyncio.get_event_loop().run_in_executor(
|
||||
None, star_metadata.star_cls.__del__
|
||||
)
|
||||
else:
|
||||
elif hasattr(star_metadata.star_cls, "terminate"):
|
||||
await star_metadata.star_cls.terminate()
|
||||
|
||||
async def turn_on_plugin(self, plugin_name: str):
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
from typing import Union, Awaitable, List, Optional, ClassVar
|
||||
from astrbot.core.message.components import BaseMessageComponent
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.api.platform import MessageMember, AstrBotMessage
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.star.context import Context
|
||||
|
||||
|
||||
class StarTools:
|
||||
"""
|
||||
提供给插件使用的便捷工具函数集合
|
||||
这些方法封装了一些常用操作,使插件开发更加简单便捷!
|
||||
"""
|
||||
|
||||
_context: ClassVar[Optional[Context]] = None
|
||||
|
||||
@classmethod
|
||||
def initialize(cls, context: Context) -> None:
|
||||
"""
|
||||
初始化StarTools,设置context引用
|
||||
|
||||
Args:
|
||||
context: 暴露给插件的上下文
|
||||
"""
|
||||
cls._context = context
|
||||
|
||||
@classmethod
|
||||
async def send_message(
|
||||
cls, session: Union[str, MessageSesion], message_chain: MessageChain
|
||||
) -> bool:
|
||||
"""
|
||||
根据session(unified_msg_origin)主动发送消息
|
||||
|
||||
Args:
|
||||
session: 消息会话。通过event.session或者event.unified_msg_origin获取
|
||||
message_chain: 消息链
|
||||
|
||||
Returns:
|
||||
bool: 是否找到匹配的平台
|
||||
|
||||
Raises:
|
||||
ValueError: 当session为字符串且解析失败时抛出
|
||||
|
||||
Note:
|
||||
qq_official(QQ官方API平台)不支持此方法
|
||||
"""
|
||||
return await cls._context.send_message(session, message_chain)
|
||||
|
||||
@classmethod
|
||||
async def create_message(
|
||||
cls,
|
||||
type: str,
|
||||
self_id: str,
|
||||
session_id: str,
|
||||
message_id: str,
|
||||
sender: MessageMember,
|
||||
message: List[BaseMessageComponent],
|
||||
message_str: str,
|
||||
raw_message: object,
|
||||
group_id: str = "",
|
||||
):
|
||||
"""
|
||||
创建一个AstrBot消息对象
|
||||
|
||||
Args:
|
||||
type (str): 消息类型
|
||||
self_id (str): 机器人自身ID
|
||||
session_id (str): 会话ID(通常为用户ID)(QQ号, 群号等)
|
||||
message_id (str): 消息ID
|
||||
sender (MessageMember): 发送者信息
|
||||
message (List[BaseMessageComponent]): 消息组件列表
|
||||
message_str (str): 消息字符串
|
||||
raw_message (object): 原始消息对象
|
||||
group_id (str, optional): 群组ID, 如果为私聊则为空. Defaults to "".
|
||||
|
||||
Returns:
|
||||
AstrBotMessage: 创建的消息对象
|
||||
"""
|
||||
abm = AstrBotMessage()
|
||||
abm.type = type
|
||||
abm.self_id = self_id
|
||||
abm.session_id = session_id
|
||||
abm.message_id = message_id
|
||||
abm.sender = sender
|
||||
abm.message = message
|
||||
abm.message_str = message_str
|
||||
abm.raw_message = raw_message
|
||||
abm.group_id = group_id
|
||||
return abm
|
||||
|
||||
# todo: 添加构造事件的方法
|
||||
# async def create_event(
|
||||
# self, platform: str, umo: str, sender_id: str, session_id: str
|
||||
# ):
|
||||
# platform = self._context.get_platform(platform)
|
||||
|
||||
# todo: 添加找到对应平台并提交对应事件的方法
|
||||
|
||||
@classmethod
|
||||
def activate_llm_tool(cls, name: str) -> bool:
|
||||
"""
|
||||
激活一个已经注册的函数调用工具
|
||||
注册的工具默认是激活状态
|
||||
|
||||
Args:
|
||||
name (str): 工具名称
|
||||
"""
|
||||
return cls._context.activate_llm_tool(name)
|
||||
|
||||
@classmethod
|
||||
def deactivate_llm_tool(cls, name: str) -> bool:
|
||||
"""
|
||||
停用一个已经注册的函数调用工具
|
||||
|
||||
Args:
|
||||
name (str): 工具名称
|
||||
"""
|
||||
return cls._context.deactivate_llm_tool(name)
|
||||
|
||||
@classmethod
|
||||
def register_llm_tool(
|
||||
cls, name: str, func_args: list, desc: str, func_obj: Awaitable
|
||||
) -> None:
|
||||
"""
|
||||
为函数调用(function-calling/tools-use)添加工具
|
||||
|
||||
Args:
|
||||
name (str): 工具名称
|
||||
func_args (list): 函数参数列表
|
||||
desc (str): 工具描述
|
||||
func_obj (Awaitable): 函数对象,必须是异步函数
|
||||
"""
|
||||
cls._context.register_llm_tool(name, func_args, desc, func_obj)
|
||||
|
||||
@classmethod
|
||||
def unregister_llm_tool(cls, name: str) -> None:
|
||||
"""
|
||||
删除一个函数调用工具
|
||||
如果再要启用,需要重新注册
|
||||
|
||||
Args:
|
||||
name (str): 工具名称
|
||||
"""
|
||||
cls._context.unregister_llm_tool(name)
|
||||
@@ -103,7 +103,7 @@ async def download_image_by_url(
|
||||
with open(path, "wb") as f:
|
||||
f.write(await resp.read())
|
||||
return path
|
||||
except aiohttp.client.ClientConnectorSSLError:
|
||||
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
|
||||
# 关闭SSL验证
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.set_ciphers("DEFAULT")
|
||||
@@ -152,7 +152,7 @@ async def download_file(url: str, path: str, show_progress: bool = False):
|
||||
f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s",
|
||||
end="",
|
||||
)
|
||||
except aiohttp.client.ClientConnectorSSLError:
|
||||
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
|
||||
# 关闭SSL验证
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.set_ciphers("DEFAULT")
|
||||
|
||||
@@ -20,7 +20,7 @@ class LogRoute(Route):
|
||||
message = await queue.get()
|
||||
payload = {
|
||||
"type": "log",
|
||||
"data": message,
|
||||
**message # see astrbot/core/log.py
|
||||
}
|
||||
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||||
except asyncio.CancelledError:
|
||||
|
||||
@@ -21,7 +21,7 @@ class StaticFileRoute(Route):
|
||||
"/about",
|
||||
"/extension-marketplace",
|
||||
"/conversation",
|
||||
"/tool-use"
|
||||
"/tool-use",
|
||||
]
|
||||
for i in index_:
|
||||
self.app.add_url_rule(i, view_func=self.index)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# What's Changed
|
||||
|
||||
> 📢 在升级前,请完整阅读本次更新日志。
|
||||
|
||||
## ✨ 新增的功能
|
||||
|
||||
1. 安装完插件后自动弹出插件仓库 README 对话框 @zhx8702
|
||||
4. 支持阿里云百炼 TTS@Soulter
|
||||
5. 支持 Telegram MarkdownV2 渲染 @Soulter
|
||||
6. 支持 钉钉 Markdown 渲染 @Soulter
|
||||
6. 增加对 Gemini 系列模型的输入安全设置参数支持 @AliveGh0st
|
||||
7. 支持手动设置时区以应对容器、国外用户的时区问题 @anka-afk @Raven95676 @Soulter
|
||||
8. 插件市场显示帮助按钮 @Soulter
|
||||
|
||||
## 🎈 功能性优化
|
||||
|
||||
1. WebUI 的日志通信使用 SSE 替代 Websockets @Soulter
|
||||
2. 在发送消息之前统一检查消息内容是否为空, 不允许发送空消息, 以解决该消息内容不支持查看以及 Gemini 返回 `<empty content>` 问题 @anka-afk
|
||||
3. 更新 Dify 平台链接为官方域名 by @Captain-Slacker-OwO
|
||||
4. 人格 prompt 输入框支持调节高度 @Soulter
|
||||
|
||||
## 🐛 修复的 Bug
|
||||
|
||||
1. 将最多携带对话数量修改回 `-1` 时出现报错 #1074 @anka-afk
|
||||
2. 修复无法识别到函数调用异常的问题 by @Soulter
|
||||
3. 修复 aiocqhttp 适配器下空白 plain 导致的 `the object is not a proper segment chain` 报错问题 @Soulter
|
||||
4. 修复阿里百炼应用无法多轮会话的问题 @Soulter
|
||||
|
||||
## 🧩 新增的插件
|
||||
|
||||
待补充
|
||||
@@ -94,7 +94,6 @@
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'text' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
variant="outlined"
|
||||
auto-grow
|
||||
rows="3"
|
||||
class="config-field"
|
||||
hide-details
|
||||
|
||||
@@ -3,9 +3,20 @@ import { useCommonStore } from '@/stores/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="term"
|
||||
style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto">
|
||||
<div>
|
||||
<!-- 添加筛选级别控件 -->
|
||||
<div class="filter-controls mb-2">
|
||||
<v-chip-group v-model="selectedLevels" column multiple>
|
||||
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter
|
||||
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'">
|
||||
{{ level }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
</div>
|
||||
|
||||
<div id="term" style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto; height: 100%">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -25,7 +36,16 @@ export default {
|
||||
'default': 'color: #FFFFFF;'
|
||||
},
|
||||
logCache: useCommonStore().getLogCache(),
|
||||
historyNum_: -1
|
||||
historyNum_: -1,
|
||||
logLevels: ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
selectedLevels: [0, 1, 2, 3, 4], // 默认选中所有级别
|
||||
levelColors: {
|
||||
'DEBUG': 'grey',
|
||||
'INFO': 'blue-lighten-3',
|
||||
'WARNING': 'amber',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'purple'
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
@@ -37,7 +57,16 @@ export default {
|
||||
watch: {
|
||||
logCache: {
|
||||
handler(val) {
|
||||
this.printLog(val[this.logCache.length - 1])
|
||||
const lastLog = val[this.logCache.length - 1];
|
||||
if (lastLog && this.isLevelSelected(lastLog.level)) {
|
||||
this.printLog(lastLog.data);
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
selectedLevels: {
|
||||
handler() {
|
||||
this.refreshDisplay();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
@@ -50,6 +79,31 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLevelColor(level) {
|
||||
return this.levelColors[level] || 'grey';
|
||||
},
|
||||
|
||||
isLevelSelected(level) {
|
||||
for (let i = 0; i < this.selectedLevels.length; ++i) {
|
||||
let level_ = this.logLevels[this.selectedLevels[i]]
|
||||
if (level_ === level) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
refreshDisplay() {
|
||||
// 清空现有的显示
|
||||
const termElement = document.getElementById('term');
|
||||
if (termElement) {
|
||||
termElement.innerHTML = '';
|
||||
}
|
||||
|
||||
// 重新显示符合筛选条件的日志
|
||||
this.init();
|
||||
},
|
||||
|
||||
delayInit() {
|
||||
if (this.logCache.length === 0) {
|
||||
setTimeout(() => {
|
||||
@@ -64,18 +118,21 @@ export default {
|
||||
this.historyNum_ = parseInt(this.historyNum)
|
||||
let i = 0
|
||||
for (let log of this.logCache) {
|
||||
if (this.isLevelSelected(log.level)) { // 只显示选中级别的日志
|
||||
if (this.historyNum_ != -1 && i >= this.logCache.length - this.historyNum_) {
|
||||
this.printLog(log)
|
||||
this.printLog(log.data)
|
||||
++i
|
||||
} else if (this.historyNum_ == -1) {
|
||||
this.printLog(log)
|
||||
this.printLog(log.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
toggleAutoScroll() {
|
||||
this.autoScroll = !this.autoScroll;
|
||||
},
|
||||
|
||||
printLog(log) {
|
||||
// append 一个 span 标签到 term,block 的方式
|
||||
let ele = document.getElementById('term')
|
||||
@@ -88,14 +145,38 @@ export default {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
span.style = style + 'display: block; font-size: 12px; font-family: Consolas, monospace; white-space: pre-wrap;'
|
||||
span.classList.add('fade-in')
|
||||
span.innerText = log
|
||||
span.innerText = `${log}`;
|
||||
ele.appendChild(span)
|
||||
if (this.autoScroll) {
|
||||
if (this.autoScroll ) {
|
||||
ele.scrollTop = ele.scrollHeight
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,12 +3,36 @@
|
||||
<v-list dense style="background-color: transparent;max-height: 300px; overflow-y: auto;">
|
||||
<v-list-item v-for="(item, index) in items" :key="index">
|
||||
<v-list-item-content style="display: flex; justify-content: space-between;">
|
||||
<v-list-item-title>
|
||||
<v-list-item-title v-if="editIndex !== index">
|
||||
<v-chip size="small" label color="primary">{{ item }}</v-chip>
|
||||
</v-list-item-title>
|
||||
<v-btn @click="removeItem(index)" variant="plain">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-text-field
|
||||
v-else
|
||||
v-model="editItem"
|
||||
dense
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@keyup.enter="saveEdit"
|
||||
@keyup.esc="cancelEdit"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
<div v-if="editIndex !== index">
|
||||
<v-btn @click="startEdit(index, item)" variant="plain" class="edit-btn" icon size="small">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn @click="removeItem(index)" variant="plain" icon size="small">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-btn @click="saveEdit" variant="plain" color="success" icon size="small">
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
<v-btn @click="cancelEdit" variant="plain" color="error" icon size="small">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -41,6 +65,8 @@ export default {
|
||||
return {
|
||||
newItem: '',
|
||||
items: this.value,
|
||||
editIndex: -1,
|
||||
editItem: '',
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -58,6 +84,20 @@ export default {
|
||||
removeItem(index) {
|
||||
this.items.splice(index, 1);
|
||||
},
|
||||
startEdit(index, item) {
|
||||
this.editIndex = index;
|
||||
this.editItem = item;
|
||||
},
|
||||
saveEdit() {
|
||||
if (this.editItem.trim() !== '') {
|
||||
this.items[this.editIndex] = this.editItem.trim();
|
||||
this.cancelEdit();
|
||||
}
|
||||
},
|
||||
cancelEdit() {
|
||||
this.editIndex = -1;
|
||||
this.editItem = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -82,4 +122,8 @@ export default {
|
||||
.v-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
margin-right: -8px;
|
||||
}
|
||||
</style>
|
||||
@@ -20,7 +20,7 @@ export const useCommonStore = defineStore({
|
||||
"gewechat": "https://astrbot.app/deploy/platform/gewechat.html",
|
||||
"lark": "https://astrbot.app/deploy/platform/lark.html",
|
||||
"telegram": "https://astrbot.app/deploy/platform/telegram.html",
|
||||
"dingtalk": "https: //astrbot.app/deploy/platform/dingtalk.html",
|
||||
"dingtalk": "https://astrbot.app/deploy/platform/dingtalk.html",
|
||||
},
|
||||
|
||||
pluginMarketData: [],
|
||||
@@ -62,17 +62,26 @@ export const useCommonStore = defineStore({
|
||||
}
|
||||
|
||||
const text = decoder.decode(value);
|
||||
const lines = text.split('\n');
|
||||
const lines = text.split('\n\n');
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('data:')) {
|
||||
const data = line.substring(5).trim();
|
||||
|
||||
// {"type":"log","data":"[2021-08-01 00:00:00] INFO: Hello, world!"}
|
||||
|
||||
let data_json = JSON.parse(data)
|
||||
let data_json = {}
|
||||
try {
|
||||
data_json = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON:', data);
|
||||
data_json = {
|
||||
type: 'log',
|
||||
data: data,
|
||||
level: 'INFO',
|
||||
time: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
if (data_json.type === 'log') {
|
||||
let log = data_json.data
|
||||
this.log_cache.push(log);
|
||||
// let log = data_json.data
|
||||
this.log_cache.push(data_json);
|
||||
if (this.log_cache.length > this.log_cache_max_len) {
|
||||
this.log_cache.shift();
|
||||
}
|
||||
|
||||
@@ -289,6 +289,16 @@ export default {
|
||||
message: `<img src="/api/chat/get_file?filename=${img}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
|
||||
}
|
||||
this.messages.push(bot_resp);
|
||||
} else if (chunk.startsWith('[RECORD]')) {
|
||||
let audio = chunk.replace('[RECORD]', '');
|
||||
let bot_resp = {
|
||||
type: 'bot',
|
||||
message: `<audio controls class="audio-player">
|
||||
<source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
|
||||
您的浏览器不支持音频播放。
|
||||
</audio>`
|
||||
}
|
||||
this.messages.push(bot_resp);
|
||||
} else {
|
||||
let bot_resp = {
|
||||
type: 'bot',
|
||||
@@ -407,6 +417,13 @@ export default {
|
||||
let img = message[i].message.replace('[IMAGE]', '');
|
||||
message[i].message = `<img src="/api/chat/get_file?filename=${img}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
|
||||
}
|
||||
if (message[i].message.startsWith('[RECORD]')) {
|
||||
let audio = message[i].message.replace('[RECORD]', '');
|
||||
message[i].message = `<audio controls class="audio-player">
|
||||
<source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
|
||||
您的浏览器不支持音频播放。
|
||||
</audio>`
|
||||
}
|
||||
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] = `/api/chat/get_file?filename=${message[i].image_url[j]}`;
|
||||
@@ -846,7 +863,6 @@ export default {
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import axios from 'axios';
|
||||
</v-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<ConsoleDisplayer ref="consoleDisplayer" style="height: calc(100vh - 160px); " />
|
||||
<ConsoleDisplayer ref="consoleDisplayer" style="height: calc(100vh - 220px); " />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@@ -63,14 +63,13 @@ import 'highlight.js/styles/github.css';
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div v-if="isListView" class="mt-4">
|
||||
<h2>📦 全部插件</h2>
|
||||
<v-col cols="12" md="12" style="padding: 0px;">
|
||||
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name"
|
||||
:loading="loading_" v-model:search="marketSearch"
|
||||
:filter-keys="filterKeys">
|
||||
:loading="loading_" v-model:search="marketSearch" :filter-keys="filterKeys">
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<img v-if="item.logo" :src="item.logo"
|
||||
@@ -86,7 +85,7 @@ import 'highlight.js/styles/github.css';
|
||||
</template>
|
||||
<template v-slot:item.author="{ item }">
|
||||
<span v-if="item?.social_link"><a :href="item?.social_link">{{ item.author
|
||||
}}</a></span>
|
||||
}}</a></span>
|
||||
<span v-else>{{ item.author }}</span>
|
||||
</template>
|
||||
<template v-slot:item.stars="{ item }">
|
||||
@@ -100,14 +99,16 @@ import 'highlight.js/styles/github.css';
|
||||
<template v-slot:item.tags="{ item }">
|
||||
<span v-if="item.tags.length === 0">无</span>
|
||||
<v-chip v-for="tag in item.tags" :key="tag" color="primary" size="small">{{ tag
|
||||
}}</v-chip>
|
||||
}}</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn v-if="!item.installed" class="text-none mr-2" size="small" text="Read"
|
||||
<v-btn v-if="!item.installed" class="text-none mr-2" size="small"
|
||||
variant="flat" border
|
||||
@click="extension_url = item.repo; newExtension()">安装</v-btn>
|
||||
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border
|
||||
<v-btn v-else class="text-none mr-2" size="small" variant="flat" border
|
||||
disabled>已安装</v-btn>
|
||||
<v-btn class="text-none mr-2" size="small" variant="flat" border
|
||||
@click="open(item.repo)">查看帮助</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
@@ -186,7 +187,7 @@ import 'highlight.js/styles/github.css';
|
||||
</v-snackbar>
|
||||
|
||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||
|
||||
|
||||
<!-- README Dialog -->
|
||||
<v-dialog v-model="readmeDialog.show" width="800" persistent>
|
||||
<v-card>
|
||||
@@ -198,16 +199,13 @@ import 'highlight.js/styles/github.css';
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="height: 70vh; overflow-y: auto;">
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-open-in-new"
|
||||
@click="openReadmeInNewTab()"
|
||||
class="mt-4"
|
||||
>
|
||||
<v-btn color="primary" prepend-icon="mdi-open-in-new" @click="openReadmeInNewTab()" class="mt-4">
|
||||
在GitHub中查看文档
|
||||
</v-btn>
|
||||
<div v-if="readmeDialog.content" class="markdown-body" v-html="renderMarkdown(readmeDialog.content)"></div>
|
||||
<div v-else-if="readmeDialog.error" class="d-flex flex-column align-center justify-center" style="height: 100%;">
|
||||
<div v-if="readmeDialog.content" class="markdown-body" v-html="renderMarkdown(readmeDialog.content)">
|
||||
</div>
|
||||
<div v-else-if="readmeDialog.error" class="d-flex flex-column align-center justify-center"
|
||||
style="height: 100%;">
|
||||
<v-icon size="64" color="error" class="mb-4">mdi-alert-circle-outline</v-icon>
|
||||
<p class="text-body-1 text-center mb-4">{{ readmeDialog.error }}</p>
|
||||
</div>
|
||||
@@ -291,8 +289,8 @@ export default {
|
||||
const search = this.marketSearch.toLowerCase();
|
||||
return this.pluginMarketData.filter(plugin =>
|
||||
this.filterKeys.some(key =>
|
||||
plugin[key]?.toLowerCase().includes(search)
|
||||
));
|
||||
plugin[key]?.toLowerCase().includes(search)
|
||||
));
|
||||
},
|
||||
pinnedPlugins() {
|
||||
return this.pluginMarketData.filter(plugin => plugin?.pinned);
|
||||
@@ -319,6 +317,12 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
open(link) {
|
||||
if (link) {
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
},
|
||||
|
||||
jumpToPluginMarket() {
|
||||
window.open('https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json', '_blank');
|
||||
},
|
||||
@@ -387,21 +391,21 @@ export default {
|
||||
async getReadmeUrl(repoUrl) {
|
||||
// 去掉 repoUrl 末尾的斜杠
|
||||
repoUrl = repoUrl.replace(/\/+$/, '');
|
||||
|
||||
|
||||
const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
||||
if (!match) {
|
||||
throw new Error("无效的 GitHub 仓库地址");
|
||||
}
|
||||
|
||||
|
||||
const owner = match[1];
|
||||
const repo = match[2];
|
||||
|
||||
|
||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
||||
|
||||
|
||||
try {
|
||||
const res = await fetch(apiUrl);
|
||||
const data = await res.json();
|
||||
|
||||
|
||||
const branch = data?.default_branch || 'master';
|
||||
return `${repoUrl}/blob/${branch}/README.md`;
|
||||
} catch (error) {
|
||||
@@ -458,7 +462,7 @@ export default {
|
||||
this.onLoadingDialogResult(1, res.data.message);
|
||||
this.dialog = false;
|
||||
this.getExtensions();
|
||||
|
||||
|
||||
await this.showReadmeDialog(res);
|
||||
}).catch((err) => {
|
||||
this.loading_ = false;
|
||||
@@ -523,7 +527,7 @@ export default {
|
||||
if (!content) return '';
|
||||
// Configure marked with highlight.js for syntax highlighting
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
@@ -553,11 +557,11 @@ export default {
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@@ -3,6 +3,7 @@ import datetime
|
||||
import builtins
|
||||
import traceback
|
||||
import re
|
||||
import zoneinfo
|
||||
import astrbot.api.star as star
|
||||
import astrbot.api.event.filter as filter
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
@@ -22,7 +23,6 @@ from astrbot.core.config.default import VERSION
|
||||
from .long_term_memory import LongTermMemory
|
||||
from astrbot.core import logger
|
||||
from astrbot.api.message_components import Plain, Image, Reply
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
||||
@@ -39,7 +39,12 @@ class Main(star.Star):
|
||||
self.prompt_prefix = cfg["provider_settings"]["prompt_prefix"]
|
||||
self.identifier = cfg["provider_settings"]["identifier"]
|
||||
self.enable_datetime = cfg["provider_settings"]["datetime_system_prompt"]
|
||||
|
||||
self.timezone = cfg.get("timezone")
|
||||
if not self.timezone:
|
||||
# 系统默认时区
|
||||
self.timezone = None
|
||||
else:
|
||||
logger.info(f"Timezone set to: {self.timezone}")
|
||||
self.ltm = None
|
||||
if (
|
||||
self.context.get_config()["provider_ltm_settings"]["group_icl_enable"]
|
||||
@@ -969,7 +974,8 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
if len(l) == 1:
|
||||
message.set_result(
|
||||
MessageEventResult()
|
||||
.message(f"""[Persona]
|
||||
.message(
|
||||
f"""[Persona]
|
||||
|
||||
- 人格情景列表: `/persona list`
|
||||
- 设置人格情景: `/persona 人格`
|
||||
@@ -980,7 +986,8 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
当前对话 {curr_cid_title} 的人格情景: {curr_persona_name}
|
||||
|
||||
配置人格情景请前往管理面板-配置页
|
||||
""")
|
||||
"""
|
||||
)
|
||||
.use_t2i(False)
|
||||
)
|
||||
elif l[1] == "list":
|
||||
@@ -1190,11 +1197,20 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
user_info = f"\n[User ID: {user_id}, Nickname: {user_nickname}]\n"
|
||||
req.prompt = user_info + req.prompt
|
||||
|
||||
# 启用附加时间戳
|
||||
if self.enable_datetime:
|
||||
# Including timezone
|
||||
current_time = (
|
||||
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
|
||||
)
|
||||
current_time = None
|
||||
if self.timezone:
|
||||
# 启用时区
|
||||
try:
|
||||
now = datetime.datetime.now(zoneinfo.ZoneInfo(self.timezone))
|
||||
current_time = now.strftime("%Y-%m-%d %H:%M (%Z)")
|
||||
except Exception as e:
|
||||
logger.error(f"时区设置错误: {e}, 使用本地时区")
|
||||
if not current_time:
|
||||
current_time = (
|
||||
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
|
||||
)
|
||||
req.system_prompt += f"\nCurrent datetime: {current_time}\n"
|
||||
|
||||
if req.conversation:
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
import json
|
||||
import datetime
|
||||
import uuid
|
||||
import zoneinfo
|
||||
import astrbot.api.star as star
|
||||
from astrbot.api.event import filter
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
@@ -17,7 +18,15 @@ class Main(star.Star):
|
||||
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
self.context = context
|
||||
self.scheduler = AsyncIOScheduler(timezone="Asia/Shanghai")
|
||||
self.timezone = self.context.get_config().get("timezone")
|
||||
if not self.timezone:
|
||||
self.timezone = None
|
||||
try:
|
||||
self.timezone = zoneinfo.ZoneInfo(self.timezone) if self.timezone else None
|
||||
except Exception as e:
|
||||
logger.error(f"时区设置错误: {e}, 使用本地时区")
|
||||
self.timezone = None
|
||||
self.scheduler = AsyncIOScheduler(timezone=self.timezone)
|
||||
|
||||
# set and load config
|
||||
if not os.path.exists("data/astrbot-reminder.json"):
|
||||
@@ -65,10 +74,10 @@ class Main(star.Star):
|
||||
def check_is_outdated(self, reminder: dict):
|
||||
"""Check if the reminder is outdated."""
|
||||
if "datetime" in reminder:
|
||||
return (
|
||||
datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M")
|
||||
< datetime.datetime.now()
|
||||
)
|
||||
reminder_time = datetime.datetime.strptime(
|
||||
reminder["datetime"], "%Y-%m-%d %H:%M"
|
||||
).replace(tzinfo=self.timezone)
|
||||
return reminder_time < datetime.datetime.now(self.timezone)
|
||||
return False
|
||||
|
||||
async def _save_data(self):
|
||||
@@ -171,12 +180,15 @@ class Main(star.Star):
|
||||
reminders = self.reminder_data.get(unified_msg_origin, [])
|
||||
if not reminders:
|
||||
return []
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(self.timezone)
|
||||
upcoming_reminders = [
|
||||
reminder
|
||||
for reminder in reminders
|
||||
if "datetime" not in reminder
|
||||
or datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M") >= now
|
||||
or datetime.datetime.strptime(
|
||||
reminder["datetime"], "%Y-%m-%d %H:%M"
|
||||
).replace(tzinfo=self.timezone)
|
||||
>= now
|
||||
]
|
||||
return upcoming_reminders
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import astrbot.api.message_components as Comp
|
||||
import copy
|
||||
import json
|
||||
from astrbot.api import logger
|
||||
from astrbot.api.event import AstrMessageEvent, filter
|
||||
from astrbot.api.star import Context, Star, register
|
||||
@@ -64,17 +63,11 @@ class Waiter(Star):
|
||||
event.unified_msg_origin
|
||||
)
|
||||
conversation = None
|
||||
context = []
|
||||
|
||||
if curr_cid:
|
||||
conversation = await self.context.conversation_manager.get_conversation(
|
||||
event.unified_msg_origin, curr_cid
|
||||
)
|
||||
context = (
|
||||
json.loads(conversation.history)
|
||||
if conversation.history
|
||||
else []
|
||||
)
|
||||
else:
|
||||
# 创建新对话
|
||||
curr_cid = await self.context.conversation_manager.new_conversation(
|
||||
|
||||
@@ -35,6 +35,7 @@ dependencies = [
|
||||
"quart>=0.20.0",
|
||||
"readability-lxml>=0.8.1",
|
||||
"silk-python>=0.2.6",
|
||||
"telegramify-markdown>=0.5.0",
|
||||
"wechatpy>=1.8.18",
|
||||
]
|
||||
|
||||
|
||||
+2
-1
@@ -28,4 +28,5 @@ dingtalk-stream
|
||||
defusedxml
|
||||
mcp
|
||||
certifi
|
||||
pip
|
||||
pip
|
||||
telegramify-markdown
|
||||
@@ -225,6 +225,7 @@ dependencies = [
|
||||
{ name = "quart" },
|
||||
{ name = "readability-lxml" },
|
||||
{ name = "silk-python" },
|
||||
{ name = "telegramify-markdown" },
|
||||
{ name = "wechatpy" },
|
||||
]
|
||||
|
||||
@@ -260,6 +261,7 @@ requires-dist = [
|
||||
{ name = "quart", specifier = ">=0.20.0" },
|
||||
{ name = "readability-lxml", specifier = ">=0.8.1" },
|
||||
{ name = "silk-python", specifier = ">=0.2.6" },
|
||||
{ name = "telegramify-markdown", specifier = ">=0.5.0" },
|
||||
{ name = "wechatpy", specifier = ">=1.8.18" },
|
||||
]
|
||||
|
||||
@@ -1059,6 +1061,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/d1/3ff566ecf322077d861f1a68a1ff025cad337417bd66ad22a7c6f7dfcfaf/mcp-1.5.0-py3-none-any.whl", hash = "sha256:51c3f35ce93cb702f7513c12406bbea9665ef75a08db909200b07da9db641527", size = 73734 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mistletoe"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/96/ea46a376a7c4cd56955ecdfff0ea68de43996a4e6d1aee4599729453bd11/mistletoe-1.4.0.tar.gz", hash = "sha256:1630f906e5e4bbe66fdeb4d29d277e2ea515d642bb18a9b49b136361a9818c9d", size = 107203 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/0f/b5e545f0c7962be90366af3418989b12cf441d9da1e5d89d88f2f3e5cf8f/mistletoe-1.4.0-py3-none-any.whl", hash = "sha256:44a477803861de1237ba22e375c6b617690a31d2902b47279d1f8f7ed498a794", size = 51304 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.2.0"
|
||||
@@ -1792,6 +1803,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "telegramify-markdown"
|
||||
version = "0.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mistletoe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/b7/a9b56f856f87e1b4d743932353f71811844a413561be180a22d667ef6f5a/telegramify_markdown-0.5.0.tar.gz", hash = "sha256:70e6eff7e341e6e9c8818fa1ec53a4e25e4f5e3ef50856d7772760fc6b7a4066", size = 36017 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/87/4a39dde5b4aea91b874cbbe057edea19334e62651ce0f1f74f5a1f721439/telegramify_markdown-0.5.0-py3-none-any.whl", hash = "sha256:6f66b7029c0eba268fed5f9daf9216f56c588c6202dd591ff572f7df0d318f2f", size = 32389 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.2.1"
|
||||
|
||||
Reference in New Issue
Block a user