Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9564166297 | ||
|
|
f5cf3c3c8e | ||
|
|
18f919fb6b | ||
|
|
0924835253 | ||
|
|
20d2e5c578 | ||
|
|
907801605c | ||
|
|
93bc684e8c | ||
|
|
a76c98d57e | ||
|
|
d937a800d0 | ||
|
|
d16f3a227f | ||
|
|
80c9a3eeda | ||
|
|
e68173b451 | ||
|
|
40c27d87f5 | ||
|
|
3c13b5049d | ||
|
|
8288d5e51f | ||
|
|
4ffbb18ab4 | ||
|
|
b27271b7a3 | ||
|
|
ebb6665f64 | ||
|
|
e4e5731ffd | ||
|
|
2ab5810f13 | ||
|
|
af934c5d09 | ||
|
|
1e0cf7c112 | ||
|
|
46859c93c9 | ||
|
|
1641549016 | ||
|
|
716a5dbb8a | ||
|
|
af98cb11c5 | ||
|
|
9a4c2cf341 | ||
|
|
2bc3bcd102 | ||
|
|
d6c663f79d | ||
|
|
2b4ee13b5e | ||
|
|
6959f86632 | ||
|
|
537d373e10 | ||
|
|
cceadf222c | ||
|
|
cf5a4af623 | ||
|
|
39aea11c22 | ||
|
|
c2f1227700 | ||
|
|
900f14d37c | ||
|
|
598249b1d6 | ||
|
|
7ed15bdf04 | ||
|
|
2fc0ec0f72 | ||
|
|
5e9c2a669b | ||
|
|
b310521884 | ||
|
|
288945bf7e | ||
|
|
4fc07cff36 | ||
|
|
b884fe0e86 | ||
|
|
855858c236 | ||
|
|
c11a2a5419 | ||
|
|
773a6572af | ||
|
|
88ad373c9b | ||
|
|
51666464b9 | ||
|
|
5af9cf2f52 | ||
|
|
12c4ae4b10 | ||
|
|
4e1bef414a | ||
|
|
e896c18644 | ||
|
|
c852685e74 | ||
|
|
1e99797df8 | ||
|
|
52a4c986a8 | ||
|
|
c501728204 | ||
|
|
6b067fa6a7 | ||
|
|
a1cd5c53a9 | ||
|
|
a46d487e03 | ||
|
|
3deb6d3ab3 | ||
|
|
af34cdd5d2 | ||
|
|
6e1393235a | ||
|
|
343e0b54b9 | ||
|
|
ecb70cb6f7 | ||
|
|
ca50618af6 | ||
|
|
29c07ba83e | ||
|
|
45fbb83a9f | ||
|
|
ae7ba2df25 | ||
|
|
c3ef57cc32 | ||
|
|
7bb4ca5a14 | ||
|
|
063783d81d | ||
|
|
42116c9b65 | ||
|
|
a36e11973d | ||
|
|
5125568ea2 | ||
|
|
0fa164e50d | ||
|
|
cf814e81ee | ||
|
|
43a45f18ce | ||
|
|
ad51381063 | ||
|
|
0b0e4ce904 | ||
|
|
6a3e04d688 | ||
|
|
4107a17370 | ||
|
|
06b4d8f169 | ||
|
|
1c0c820746 | ||
|
|
d061403a28 | ||
|
|
5c092321a6 | ||
|
|
bdd3f61c1f | ||
|
|
8023557d6e | ||
|
|
074b0ced7a | ||
|
|
3864b1ac9b | ||
|
|
6e9b43457d | ||
|
|
ca1aec8920 | ||
|
|
acac580862 | ||
|
|
673e1b2980 | ||
|
|
f62157be72 | ||
|
|
f894ecf3b6 | ||
|
|
66dd4e28ad | ||
|
|
939dc1b0fb | ||
|
|
56bf5d38a1 | ||
|
|
d09b70b295 | ||
|
|
205180387a | ||
|
|
39c8cfeda5 | ||
|
|
f38a329be5 | ||
|
|
a0cd069539 | ||
|
|
bf306a2f01 | ||
|
|
c31f93a8d1 | ||
|
|
4730ab6309 | ||
|
|
1ae78ca98c | ||
|
|
d2379da478 | ||
|
|
0f64981b20 | ||
|
|
0002e49bb5 | ||
|
|
db13a60274 | ||
|
|
db0f11a359 | ||
|
|
ac7f43520b | ||
|
|
f67b9f5f6e | ||
|
|
c75156c4ce | ||
|
|
10270b5595 | ||
|
|
f7458572ed | ||
|
|
d57b7222b2 | ||
|
|
62e70a673a | ||
|
|
5e9eba6478 | ||
|
|
c5ccc1a084 | ||
|
|
6439917cbe | ||
|
|
d21c18f657 | ||
|
|
e6981290bc | ||
|
|
75c3d8abbd | ||
|
|
d88683f498 | ||
|
|
40b9aa3a4c |
@@ -3,7 +3,6 @@ import tempfile
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import yaml
|
import yaml
|
||||||
import re
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -59,7 +58,16 @@ def get_git_repo(url: str, target_path: Path, proxy: str | None = None):
|
|||||||
proxy=proxy if proxy else None, follow_redirects=True
|
proxy=proxy if proxy else None, follow_redirects=True
|
||||||
) as client:
|
) as client:
|
||||||
resp = client.get(download_url)
|
resp = client.get(download_url)
|
||||||
resp.raise_for_status()
|
if (
|
||||||
|
resp.status_code == 404
|
||||||
|
and "archive/refs/heads/master.zip" in download_url
|
||||||
|
):
|
||||||
|
alt_url = download_url.replace("master.zip", "main.zip")
|
||||||
|
click.echo("master 分支不存在,尝试下载 main 分支")
|
||||||
|
resp = client.get(alt_url)
|
||||||
|
resp.raise_for_status()
|
||||||
|
else:
|
||||||
|
resp.raise_for_status()
|
||||||
zip_content = BytesIO(resp.content)
|
zip_content = BytesIO(resp.content)
|
||||||
with ZipFile(zip_content) as z:
|
with ZipFile(zip_content) as z:
|
||||||
z.extractall(temp_dir)
|
z.extractall(temp_dir)
|
||||||
@@ -91,39 +99,6 @@ def load_yaml_metadata(plugin_dir: Path) -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def extract_py_metadata(plugin_dir: Path) -> dict:
|
|
||||||
"""从 Python 文件中提取插件元数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
plugin_dir: 插件目录路径
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 包含元数据的字典,如果提取失败则返回空字典
|
|
||||||
"""
|
|
||||||
# 检查 main.py 或与目录同名的 py 文件
|
|
||||||
for pattern in ["main.py", f"{plugin_dir.name}.py"]:
|
|
||||||
for py_file in plugin_dir.glob(pattern):
|
|
||||||
try:
|
|
||||||
content = py_file.read_text(encoding="utf-8")
|
|
||||||
register_match = re.search(
|
|
||||||
r'@register_star\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"(?:\s*,\s*"?([^")]+)"?)?\s*\)',
|
|
||||||
content,
|
|
||||||
)
|
|
||||||
if register_match:
|
|
||||||
# 映射匹配组到元数据键
|
|
||||||
metadata = {}
|
|
||||||
keys = ["name", "author", "desc", "version", "repo"]
|
|
||||||
for i, key in enumerate(keys):
|
|
||||||
if i + 1 <= len(
|
|
||||||
register_match.groups()
|
|
||||||
) and register_match.group(i + 1):
|
|
||||||
metadata[key] = register_match.group(i + 1)
|
|
||||||
return metadata
|
|
||||||
except Exception as e:
|
|
||||||
click.echo(f"读取 {py_file} 失败: {e}", err=True)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def build_plug_list(plugins_dir: Path) -> list:
|
def build_plug_list(plugins_dir: Path) -> list:
|
||||||
"""构建插件列表,包含本地和在线插件信息
|
"""构建插件列表,包含本地和在线插件信息
|
||||||
|
|
||||||
@@ -139,31 +114,22 @@ def build_plug_list(plugins_dir: Path) -> list:
|
|||||||
for plugin_name in [d.name for d in plugins_dir.glob("*") if d.is_dir()]:
|
for plugin_name in [d.name for d in plugins_dir.glob("*") if d.is_dir()]:
|
||||||
plugin_dir = plugins_dir / plugin_name
|
plugin_dir = plugins_dir / plugin_name
|
||||||
|
|
||||||
# 从不同来源加载元数据
|
# 从 metadata.yaml 加载元数据
|
||||||
metadata = load_yaml_metadata(plugin_dir)
|
metadata = load_yaml_metadata(plugin_dir)
|
||||||
|
|
||||||
# 如果元数据不完整,尝试从 Python 文件提取
|
# 如果成功加载元数据,添加到结果列表
|
||||||
if not metadata or not all(
|
if metadata and all(
|
||||||
k in metadata for k in ["name", "desc", "version", "author", "repo"]
|
k in metadata for k in ["name", "desc", "version", "author", "repo"]
|
||||||
):
|
):
|
||||||
py_metadata = extract_py_metadata(plugin_dir)
|
result.append({
|
||||||
# 合并元数据,保留已有的值
|
"name": str(metadata.get("name", "")),
|
||||||
for key, value in py_metadata.items():
|
"desc": str(metadata.get("desc", "")),
|
||||||
if key not in metadata or not metadata[key]:
|
"version": str(metadata.get("version", "")),
|
||||||
metadata[key] = value
|
"author": str(metadata.get("author", "")),
|
||||||
# 如果成功提取元数据,添加到结果列表
|
"repo": str(metadata.get("repo", "")),
|
||||||
if metadata:
|
"status": PluginStatus.INSTALLED,
|
||||||
result.append(
|
"local_path": str(plugin_dir),
|
||||||
{
|
})
|
||||||
"name": str(metadata.get("name", "")),
|
|
||||||
"desc": str(metadata.get("desc", "")),
|
|
||||||
"version": str(metadata.get("version", "")),
|
|
||||||
"author": str(metadata.get("author", "")),
|
|
||||||
"repo": str(metadata.get("repo", "")),
|
|
||||||
"status": PluginStatus.INSTALLED,
|
|
||||||
"local_path": str(plugin_dir),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 获取在线插件列表
|
# 获取在线插件列表
|
||||||
online_plugins = []
|
online_plugins = []
|
||||||
@@ -173,17 +139,15 @@ def build_plug_list(plugins_dir: Path) -> list:
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
for plugin_id, plugin_info in data.items():
|
for plugin_id, plugin_info in data.items():
|
||||||
online_plugins.append(
|
online_plugins.append({
|
||||||
{
|
"name": str(plugin_id),
|
||||||
"name": str(plugin_id),
|
"desc": str(plugin_info.get("desc", "")),
|
||||||
"desc": str(plugin_info.get("desc", "")),
|
"version": str(plugin_info.get("version", "")),
|
||||||
"version": str(plugin_info.get("version", "")),
|
"author": str(plugin_info.get("author", "")),
|
||||||
"author": str(plugin_info.get("author", "")),
|
"repo": str(plugin_info.get("repo", "")),
|
||||||
"repo": str(plugin_info.get("repo", "")),
|
"status": PluginStatus.NOT_INSTALLED,
|
||||||
"status": PluginStatus.NOT_INSTALLED,
|
"local_path": None,
|
||||||
"local_path": None,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(f"获取在线插件列表失败: {e}", err=True)
|
click.echo(f"获取在线插件列表失败: {e}", err=True)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import os
|
import os
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
VERSION = "3.5.10"
|
VERSION = "3.5.13"
|
||||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
|
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
|
||||||
|
|
||||||
# 默认配置
|
# 默认配置
|
||||||
@@ -66,6 +66,7 @@ DEFAULT_CONFIG = {
|
|||||||
"enable": False,
|
"enable": False,
|
||||||
"provider_id": "",
|
"provider_id": "",
|
||||||
"dual_output": False,
|
"dual_output": False,
|
||||||
|
"use_file_service": False,
|
||||||
},
|
},
|
||||||
"provider_ltm_settings": {
|
"provider_ltm_settings": {
|
||||||
"group_icl_enable": False,
|
"group_icl_enable": False,
|
||||||
@@ -91,6 +92,7 @@ DEFAULT_CONFIG = {
|
|||||||
"t2i_word_threshold": 150,
|
"t2i_word_threshold": 150,
|
||||||
"t2i_strategy": "remote",
|
"t2i_strategy": "remote",
|
||||||
"t2i_endpoint": "",
|
"t2i_endpoint": "",
|
||||||
|
"t2i_use_file_service": False,
|
||||||
"http_proxy": "",
|
"http_proxy": "",
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"enable": True,
|
"enable": True,
|
||||||
@@ -176,6 +178,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"api_base_url": "https://api.weixin.qq.com/cgi-bin/",
|
"api_base_url": "https://api.weixin.qq.com/cgi-bin/",
|
||||||
"callback_server_host": "0.0.0.0",
|
"callback_server_host": "0.0.0.0",
|
||||||
"port": 6194,
|
"port": 6194,
|
||||||
|
"active_send_mode": False,
|
||||||
},
|
},
|
||||||
"wecom(企业微信)": {
|
"wecom(企业微信)": {
|
||||||
"id": "wecom",
|
"id": "wecom",
|
||||||
@@ -220,20 +223,25 @@ CONFIG_METADATA_2 = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
|
"active_send_mode": {
|
||||||
|
"description": "是否换用主动发送接口",
|
||||||
|
"type": "bool",
|
||||||
|
"desc": "只有企业认证的公众号才能主动发送。主动发送接口的限制会少一些。",
|
||||||
|
},
|
||||||
"wpp_active_message_poll": {
|
"wpp_active_message_poll": {
|
||||||
"description": "是否启用主动消息轮询",
|
"description": "是否启用主动消息轮询",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"hint": "只有当你发现微信消息没有按时同步到 AstrBot 时,才需要启用这个功能,默认不启用。"
|
"hint": "只有当你发现微信消息没有按时同步到 AstrBot 时,才需要启用这个功能,默认不启用。",
|
||||||
},
|
},
|
||||||
"wpp_active_message_poll_interval": {
|
"wpp_active_message_poll_interval": {
|
||||||
"description": "主动消息轮询间隔",
|
"description": "主动消息轮询间隔",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"hint": "主动消息轮询间隔,单位为秒,默认 3 秒,最大不要超过 60 秒,否则可能被认为是旧消息。"
|
"hint": "主动消息轮询间隔,单位为秒,默认 3 秒,最大不要超过 60 秒,否则可能被认为是旧消息。",
|
||||||
},
|
},
|
||||||
"kf_name": {
|
"kf_name": {
|
||||||
"description": "微信客服账号名",
|
"description": "微信客服账号名",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"hint": "可选。微信客服账号名(不是 ID)。可在 https://kf.weixin.qq.com/kf/frame#/accounts 获取"
|
"hint": "可选。微信客服账号名(不是 ID)。可在 https://kf.weixin.qq.com/kf/frame#/accounts 获取",
|
||||||
},
|
},
|
||||||
"telegram_token": {
|
"telegram_token": {
|
||||||
"description": "Bot Token",
|
"description": "Bot Token",
|
||||||
@@ -256,10 +264,10 @@ CONFIG_METADATA_2 = {
|
|||||||
"hint": "Telegram 命令自动刷新间隔,单位为秒。",
|
"hint": "Telegram 命令自动刷新间隔,单位为秒。",
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"description": "ID",
|
"description": "机器人名称",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"obvious_hint": True,
|
"obvious_hint": True,
|
||||||
"hint": "ID 不能和其它的平台适配器重复,否则将发生严重冲突。",
|
"hint": "机器人名称(ID)不能和其它的平台适配器重复。",
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"description": "适配器类型",
|
"description": "适配器类型",
|
||||||
@@ -818,7 +826,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"azure_tts_rate": "1",
|
"azure_tts_rate": "1",
|
||||||
"azure_tts_volume": "100",
|
"azure_tts_volume": "100",
|
||||||
"azure_tts_subscription_key": "",
|
"azure_tts_subscription_key": "",
|
||||||
"azure_tts_region": "eastus"
|
"azure_tts_region": "eastus",
|
||||||
},
|
},
|
||||||
"MiniMax TTS(API)": {
|
"MiniMax TTS(API)": {
|
||||||
"id": "minimax_tts",
|
"id": "minimax_tts",
|
||||||
@@ -841,44 +849,144 @@ CONFIG_METADATA_2 = {
|
|||||||
"minimax-voice-english-normalization": False,
|
"minimax-voice-english-normalization": False,
|
||||||
"timeout": 20,
|
"timeout": 20,
|
||||||
},
|
},
|
||||||
|
"火山引擎_TTS(API)": {
|
||||||
|
"id": "volcengine_tts",
|
||||||
|
"type": "volcengine_tts",
|
||||||
|
"provider_type": "text_to_speech",
|
||||||
|
"enable": False,
|
||||||
|
"api_key": "",
|
||||||
|
"appid": "",
|
||||||
|
"volcengine_cluster": "volcano_tts",
|
||||||
|
"volcengine_voice_type": "",
|
||||||
|
"volcengine_speed_ratio": 1.0,
|
||||||
|
"api_base": "https://openspeech.bytedance.com/api/v1/tts",
|
||||||
|
"timeout": 20,
|
||||||
|
},
|
||||||
|
"OpenAI Embedding": {
|
||||||
|
"id": "openai_embedding",
|
||||||
|
"type": "openai_embedding",
|
||||||
|
"provider_type": "embedding",
|
||||||
|
"enable": True,
|
||||||
|
"embedding_api_key": "",
|
||||||
|
"embedding_api_base": "",
|
||||||
|
"embedding_model": "",
|
||||||
|
"embedding_dimensions": 1536,
|
||||||
|
"timeout": 20,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
|
"embedding_dimensions": {
|
||||||
|
"description": "嵌入维度",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "嵌入向量的维度。根据模型不同,可能需要调整,请参考具体模型的文档。此配置项请务必填写正确,否则将导致向量数据库无法正常工作。",
|
||||||
|
},
|
||||||
|
"embedding_model": {
|
||||||
|
"description": "嵌入模型",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "嵌入模型名称。",
|
||||||
|
},
|
||||||
|
"embedding_api_key": {
|
||||||
|
"description": "API Key",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "API Key",
|
||||||
|
},
|
||||||
|
"volcengine_cluster": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "火山引擎集群",
|
||||||
|
"hint": "若使用语音复刻大模型,可选volcano_icl或volcano_icl_concurr,默认使用volcano_tts",
|
||||||
|
},
|
||||||
|
"volcengine_voice_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "火山引擎音色",
|
||||||
|
"hint": "输入声音id(Voice_type)",
|
||||||
|
},
|
||||||
|
"volcengine_speed_ratio": {
|
||||||
|
"type": "float",
|
||||||
|
"description": "语速设置",
|
||||||
|
"hint": "语速设置,范围为 0.2 到 3.0,默认值为 1.0",
|
||||||
|
},
|
||||||
|
"volcengine_volume_ratio": {
|
||||||
|
"type": "float",
|
||||||
|
"description": "音量设置",
|
||||||
|
"hint": "音量设置,范围为 0.0 到 2.0,默认值为 1.0",
|
||||||
|
},
|
||||||
"azure_tts_voice": {
|
"azure_tts_voice": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "音色设置",
|
"description": "音色设置",
|
||||||
"hint": "API 音色"
|
"hint": "API 音色",
|
||||||
},
|
},
|
||||||
"azure_tts_style": {
|
"azure_tts_style": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "风格设置",
|
"description": "风格设置",
|
||||||
"hint": "声音特定的讲话风格。 可以表达快乐、同情和平静等情绪。"
|
"hint": "声音特定的讲话风格。 可以表达快乐、同情和平静等情绪。",
|
||||||
},
|
},
|
||||||
"azure_tts_role": {
|
"azure_tts_role": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "模仿设置(可选)",
|
"description": "模仿设置(可选)",
|
||||||
"hint": "讲话角色扮演。 声音可以模仿不同的年龄和性别,但声音名称不会更改。 例如,男性语音可以提高音调和改变语调来模拟女性语音,但语音名称不会更改。 如果角色缺失或不受声音的支持,则会忽略此属性。",
|
"hint": "讲话角色扮演。 声音可以模仿不同的年龄和性别,但声音名称不会更改。 例如,男性语音可以提高音调和改变语调来模拟女性语音,但语音名称不会更改。 如果角色缺失或不受声音的支持,则会忽略此属性。",
|
||||||
"options": ["Boy","Girl","YoungAdultFemale","YoungAdultMale","OlderAdultFemale","OlderAdultMale","SeniorFemale","SeniorMale","禁用"]
|
"options": [
|
||||||
|
"Boy",
|
||||||
|
"Girl",
|
||||||
|
"YoungAdultFemale",
|
||||||
|
"YoungAdultMale",
|
||||||
|
"OlderAdultFemale",
|
||||||
|
"OlderAdultMale",
|
||||||
|
"SeniorFemale",
|
||||||
|
"SeniorMale",
|
||||||
|
"禁用",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"azure_tts_rate": {
|
"azure_tts_rate": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "语速设置",
|
"description": "语速设置",
|
||||||
"hint": "指示文本的讲出速率。可在字词或句子层面应用语速。 速率变化应为原始音频的 0.5 到 2 倍。"
|
"hint": "指示文本的讲出速率。可在字词或句子层面应用语速。 速率变化应为原始音频的 0.5 到 2 倍。",
|
||||||
},
|
},
|
||||||
"azure_tts_volume": {
|
"azure_tts_volume": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "语音音量设置",
|
"description": "语音音量设置",
|
||||||
"hint": "指示语音的音量级别。 可在句子层面应用音量的变化。以从 0.0 到 100.0(从最安静到最大声,例如 75)的数字表示。 默认值为 100.0。"
|
"hint": "指示语音的音量级别。 可在句子层面应用音量的变化。以从 0.0 到 100.0(从最安静到最大声,例如 75)的数字表示。 默认值为 100.0。",
|
||||||
},
|
},
|
||||||
"azure_tts_region": {
|
"azure_tts_region": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "API 地区",
|
"description": "API 地区",
|
||||||
"hint": "Azure_TTS 处理数据所在区域,具体参考 https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/regions",
|
"hint": "Azure_TTS 处理数据所在区域,具体参考 https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/regions",
|
||||||
"options": ["southafricanorth", "eastasia", "southeastasia", "australiaeast", "centralindia", "japaneast", "japanwest", "koreacentral", "canadacentral", "northeurope", "westeurope", "francecentral", "germanywestcentral", "norwayeast", "swedencentral", "switzerlandnorth", "switzerlandwest", "uksouth", "uaenorth", "brazilsouth", "qatarcentral", "centralus", "eastus", "eastus2", "northcentralus", "southcentralus", "westcentralus", "westus", "westus2", "westus3"]
|
"options": [
|
||||||
|
"southafricanorth",
|
||||||
|
"eastasia",
|
||||||
|
"southeastasia",
|
||||||
|
"australiaeast",
|
||||||
|
"centralindia",
|
||||||
|
"japaneast",
|
||||||
|
"japanwest",
|
||||||
|
"koreacentral",
|
||||||
|
"canadacentral",
|
||||||
|
"northeurope",
|
||||||
|
"westeurope",
|
||||||
|
"francecentral",
|
||||||
|
"germanywestcentral",
|
||||||
|
"norwayeast",
|
||||||
|
"swedencentral",
|
||||||
|
"switzerlandnorth",
|
||||||
|
"switzerlandwest",
|
||||||
|
"uksouth",
|
||||||
|
"uaenorth",
|
||||||
|
"brazilsouth",
|
||||||
|
"qatarcentral",
|
||||||
|
"centralus",
|
||||||
|
"eastus",
|
||||||
|
"eastus2",
|
||||||
|
"northcentralus",
|
||||||
|
"southcentralus",
|
||||||
|
"westcentralus",
|
||||||
|
"westus",
|
||||||
|
"westus2",
|
||||||
|
"westus3",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"azure_tts_subscription_key": {
|
"azure_tts_subscription_key": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "服务订阅密钥",
|
"description": "服务订阅密钥",
|
||||||
"hint": "Azure_TTS 服务的订阅密钥(注意不是令牌)"
|
"hint": "Azure_TTS 服务的订阅密钥(注意不是令牌)",
|
||||||
},
|
},
|
||||||
"dashscope_tts_voice": {
|
"dashscope_tts_voice": {
|
||||||
"description": "语音合成模型",
|
"description": "语音合成模型",
|
||||||
@@ -973,7 +1081,33 @@ CONFIG_METADATA_2 = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "指定语言/方言",
|
"description": "指定语言/方言",
|
||||||
"hint": "增强对指定的小语种和方言的识别能力,设置后可以提升在指定小语种/方言场景下的语音表现",
|
"hint": "增强对指定的小语种和方言的识别能力,设置后可以提升在指定小语种/方言场景下的语音表现",
|
||||||
"options": [ "Chinese","Chinese,Yue","English","Arabic","Russian","Spanish","French","Portuguese","German","Turkish","Dutch","Ukrainian","Vietnamese","Indonesian","Japanese","Italian","Korean","Thai","Polish","Romanian","Greek","Czech","Finnish","Hindi","auto",],
|
"options": [
|
||||||
|
"Chinese",
|
||||||
|
"Chinese,Yue",
|
||||||
|
"English",
|
||||||
|
"Arabic",
|
||||||
|
"Russian",
|
||||||
|
"Spanish",
|
||||||
|
"French",
|
||||||
|
"Portuguese",
|
||||||
|
"German",
|
||||||
|
"Turkish",
|
||||||
|
"Dutch",
|
||||||
|
"Ukrainian",
|
||||||
|
"Vietnamese",
|
||||||
|
"Indonesian",
|
||||||
|
"Japanese",
|
||||||
|
"Italian",
|
||||||
|
"Korean",
|
||||||
|
"Thai",
|
||||||
|
"Polish",
|
||||||
|
"Romanian",
|
||||||
|
"Greek",
|
||||||
|
"Czech",
|
||||||
|
"Finnish",
|
||||||
|
"Hindi",
|
||||||
|
"auto",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"minimax-voice-speed": {
|
"minimax-voice-speed": {
|
||||||
"type": "float",
|
"type": "float",
|
||||||
@@ -1010,7 +1144,15 @@ CONFIG_METADATA_2 = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "情绪",
|
"description": "情绪",
|
||||||
"hint": "控制合成语音的情绪",
|
"hint": "控制合成语音的情绪",
|
||||||
"options": ["happy","sad","angry","fearful","disgusted","surprised","neutral",],
|
"options": [
|
||||||
|
"happy",
|
||||||
|
"sad",
|
||||||
|
"angry",
|
||||||
|
"fearful",
|
||||||
|
"disgusted",
|
||||||
|
"surprised",
|
||||||
|
"neutral",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"minimax-voice-latex": {
|
"minimax-voice-latex": {
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
@@ -1365,6 +1507,11 @@ CONFIG_METADATA_2 = {
|
|||||||
"hint": "启用后,Bot 将同时输出语音和文字消息。",
|
"hint": "启用后,Bot 将同时输出语音和文字消息。",
|
||||||
"obvious_hint": True,
|
"obvious_hint": True,
|
||||||
},
|
},
|
||||||
|
"use_file_service": {
|
||||||
|
"description": "使用文件服务提供 TTS 语音文件",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,如已配置 callback_api_base ,将会使用文件服务提供TTS语音文件",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"provider_ltm_settings": {
|
"provider_ltm_settings": {
|
||||||
@@ -1481,7 +1628,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"description": "对外可达的回调接口地址",
|
"description": "对外可达的回调接口地址",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"obvious_hint": True,
|
"obvious_hint": True,
|
||||||
"hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定 “外部服务如何访问 AstrBot” 的地址。如 http://localhost:6185,https://example.com 等。"
|
"hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定 “外部服务如何访问 AstrBot” 的地址。如 http://localhost:6185,https://example.com 等。",
|
||||||
},
|
},
|
||||||
"log_level": {
|
"log_level": {
|
||||||
"description": "控制台日志级别",
|
"description": "控制台日志级别",
|
||||||
@@ -1500,6 +1647,11 @@ CONFIG_METADATA_2 = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"hint": "当 t2i_strategy 为 remote 时生效。为空时使用 AstrBot API 服务",
|
"hint": "当 t2i_strategy 为 remote 时生效。为空时使用 AstrBot API 服务",
|
||||||
},
|
},
|
||||||
|
"t2i_use_file_service": {
|
||||||
|
"description": "本地文本转图像使用文件服务提供文件",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "当 t2i_strategy 为 local 并且配置 callback_api_base 时生效。是否使用文件服务提供文件。",
|
||||||
|
},
|
||||||
"pip_install_arg": {
|
"pip_install_arg": {
|
||||||
"description": "pip 安装参数",
|
"description": "pip 安装参数",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
Astrbot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作。
|
Astrbot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作。
|
||||||
该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、KnowledgeDBManager、ConversationManager、PluginManager、PipelineScheduler、EventBus等。
|
该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus等。
|
||||||
该类还负责加载和执行插件, 以及处理事件总线的分发。
|
该类还负责加载和执行插件, 以及处理事件总线的分发。
|
||||||
|
|
||||||
工作流程:
|
工作流程:
|
||||||
@@ -28,7 +28,6 @@ from astrbot.core.db import BaseDatabase
|
|||||||
from astrbot.core.updator import AstrBotUpdator
|
from astrbot.core.updator import AstrBotUpdator
|
||||||
from astrbot.core import logger
|
from astrbot.core import logger
|
||||||
from astrbot.core.config.default import VERSION
|
from astrbot.core.config.default import VERSION
|
||||||
from astrbot.core.rag.knowledge_db_mgr import KnowledgeDBManager
|
|
||||||
from astrbot.core.conversation_mgr import ConversationManager
|
from astrbot.core.conversation_mgr import ConversationManager
|
||||||
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
||||||
from astrbot.core.star.star_handler import star_map
|
from astrbot.core.star.star_handler import star_map
|
||||||
@@ -37,7 +36,7 @@ from astrbot.core.star.star_handler import star_map
|
|||||||
class AstrBotCoreLifecycle:
|
class AstrBotCoreLifecycle:
|
||||||
"""
|
"""
|
||||||
AstrBot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作。
|
AstrBot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作。
|
||||||
该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、KnowledgeDBManager、ConversationManager、PluginManager、PipelineScheduler、
|
该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、
|
||||||
EventBus 等。
|
EventBus 等。
|
||||||
该类还负责加载和执行插件, 以及处理事件总线的分发。
|
该类还负责加载和执行插件, 以及处理事件总线的分发。
|
||||||
"""
|
"""
|
||||||
@@ -54,7 +53,7 @@ class AstrBotCoreLifecycle:
|
|||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""
|
"""
|
||||||
初始化 AstrBot 核心生命周期管理类, 负责初始化各个组件, 包括 ProviderManager、PlatformManager、KnowledgeDBManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdator等。
|
初始化 AstrBot 核心生命周期管理类, 负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdator等。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 初始化日志代理
|
# 初始化日志代理
|
||||||
@@ -73,9 +72,6 @@ class AstrBotCoreLifecycle:
|
|||||||
# 初始化平台管理器
|
# 初始化平台管理器
|
||||||
self.platform_manager = PlatformManager(self.astrbot_config, self.event_queue)
|
self.platform_manager = PlatformManager(self.astrbot_config, self.event_queue)
|
||||||
|
|
||||||
# 初始化知识库管理器
|
|
||||||
self.knowledge_db_manager = KnowledgeDBManager(self.astrbot_config)
|
|
||||||
|
|
||||||
# 初始化对话管理器
|
# 初始化对话管理器
|
||||||
self.conversation_manager = ConversationManager(self.db)
|
self.conversation_manager = ConversationManager(self.db)
|
||||||
|
|
||||||
@@ -87,7 +83,6 @@ class AstrBotCoreLifecycle:
|
|||||||
self.provider_manager,
|
self.provider_manager,
|
||||||
self.platform_manager,
|
self.platform_manager,
|
||||||
self.conversation_manager,
|
self.conversation_manager,
|
||||||
self.knowledge_db_manager,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 初始化插件管理器
|
# 初始化插件管理器
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import json
|
|
||||||
import aiosqlite
|
|
||||||
import os
|
|
||||||
from typing import Any
|
|
||||||
from .plugin_storage import PluginStorage
|
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
||||||
|
|
||||||
DBPATH = os.path.join(get_astrbot_data_path(), "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()
|
|
||||||
46
astrbot/core/db/vec_db/base.py
Normal file
46
astrbot/core/db/vec_db/base.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import abc
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Result:
|
||||||
|
similarity: float
|
||||||
|
data: dict
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVecDB:
|
||||||
|
async def initialize(self):
|
||||||
|
"""
|
||||||
|
初始化向量数据库
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def insert(self, content: str, metadata: dict = None, id: str = None) -> int:
|
||||||
|
"""
|
||||||
|
插入一条文本和其对应向量,自动生成 ID 并保持一致性。
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def retrieve(self, query: str, top_k: int = 5) -> list[Result]:
|
||||||
|
"""
|
||||||
|
搜索最相似的文档。
|
||||||
|
Args:
|
||||||
|
query (str): 查询文本
|
||||||
|
top_k (int): 返回的最相似文档的数量
|
||||||
|
Returns:
|
||||||
|
List[Result]: 查询结果
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def delete(self, doc_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
删除指定文档。
|
||||||
|
Args:
|
||||||
|
doc_id (str): 要删除的文档 ID
|
||||||
|
Returns:
|
||||||
|
bool: 删除是否成功
|
||||||
|
"""
|
||||||
|
...
|
||||||
3
astrbot/core/db/vec_db/faiss_impl/__init__.py
Normal file
3
astrbot/core/db/vec_db/faiss_impl/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .vec_db import FaissVecDB
|
||||||
|
|
||||||
|
__all__ = ["FaissVecDB"]
|
||||||
121
astrbot/core/db/vec_db/faiss_impl/document_storage.py
Normal file
121
astrbot/core/db/vec_db/faiss_impl/document_storage.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import aiosqlite
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentStorage:
|
||||||
|
def __init__(self, db_path: str):
|
||||||
|
self.db_path = db_path
|
||||||
|
self.connection = None
|
||||||
|
self.sqlite_init_path = os.path.join(
|
||||||
|
os.path.dirname(__file__), "sqlite_init.sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
"""Initialize the SQLite database and create the documents table if it doesn't exist."""
|
||||||
|
if not os.path.exists(self.db_path):
|
||||||
|
await self.connect()
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
with open(self.sqlite_init_path, "r", encoding="utf-8") as f:
|
||||||
|
sql_script = f.read()
|
||||||
|
await cursor.executescript(sql_script)
|
||||||
|
await self.connection.commit()
|
||||||
|
else:
|
||||||
|
await self.connect()
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""Connect to the SQLite database."""
|
||||||
|
self.connection = await aiosqlite.connect(self.db_path)
|
||||||
|
|
||||||
|
async def get_documents(self, metadata_filters: dict, ids: list = None):
|
||||||
|
"""Retrieve documents by metadata filters and ids.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metadata_filters (dict): The metadata filters to apply.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The list of document IDs(primary key, not doc_id) that match the filters.
|
||||||
|
"""
|
||||||
|
# metadata filter -> SQL WHERE clause
|
||||||
|
where_clauses = []
|
||||||
|
values = []
|
||||||
|
for key, val in metadata_filters.items():
|
||||||
|
where_clauses.append(f"json_extract(metadata, '$.{key}') = ?")
|
||||||
|
values.append(val)
|
||||||
|
if ids is not None and len(ids) > 0:
|
||||||
|
ids = [str(i) for i in ids if i != -1]
|
||||||
|
where_clauses.append("id IN ({})".format(",".join("?" * len(ids))))
|
||||||
|
values.extend(ids)
|
||||||
|
where_sql = " AND ".join(where_clauses) or "1=1"
|
||||||
|
|
||||||
|
result = []
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
sql = "SELECT * FROM documents WHERE " + where_sql
|
||||||
|
await cursor.execute(sql, values)
|
||||||
|
for row in await cursor.fetchall():
|
||||||
|
result.append(await self.tuple_to_dict(row))
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_document_by_doc_id(self, doc_id: str):
|
||||||
|
"""Retrieve a document by its doc_id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc_id (str): The doc_id of the document to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The document data.
|
||||||
|
"""
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
await cursor.execute("SELECT * FROM documents WHERE doc_id = ?", (doc_id,))
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return await self.tuple_to_dict(row)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def update_document_by_doc_id(self, doc_id: str, new_text: str):
|
||||||
|
"""Retrieve a document by its doc_id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc_id (str): The doc_id.
|
||||||
|
new_text (str): The new text to update the document with.
|
||||||
|
"""
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"UPDATE documents SET text = ? WHERE doc_id = ?", (new_text, doc_id)
|
||||||
|
)
|
||||||
|
await self.connection.commit()
|
||||||
|
|
||||||
|
async def get_user_ids(self) -> list[str]:
|
||||||
|
"""Retrieve all user IDs from the documents table.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of user IDs.
|
||||||
|
"""
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
await cursor.execute("SELECT DISTINCT user_id FROM documents")
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return [row[0] for row in rows]
|
||||||
|
|
||||||
|
async def tuple_to_dict(self, row):
|
||||||
|
"""Convert a tuple to a dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row (tuple): The row to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The converted dictionary.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"id": row[0],
|
||||||
|
"doc_id": row[1],
|
||||||
|
"text": row[2],
|
||||||
|
"metadata": row[3],
|
||||||
|
"created_at": row[4],
|
||||||
|
"updated_at": row[5],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Close the connection to the SQLite database."""
|
||||||
|
if self.connection:
|
||||||
|
await self.connection.close()
|
||||||
|
self.connection = None
|
||||||
59
astrbot/core/db/vec_db/faiss_impl/embedding_storage.py
Normal file
59
astrbot/core/db/vec_db/faiss_impl/embedding_storage.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
try:
|
||||||
|
import faiss
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
raise ImportError(
|
||||||
|
"faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。"
|
||||||
|
)
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddingStorage:
|
||||||
|
def __init__(self, dimension: int, path: str = None):
|
||||||
|
self.dimension = dimension
|
||||||
|
self.path = path
|
||||||
|
self.index = None
|
||||||
|
if path and os.path.exists(path):
|
||||||
|
self.index = faiss.read_index(path)
|
||||||
|
else:
|
||||||
|
base_index = faiss.IndexFlatL2(dimension)
|
||||||
|
self.index = faiss.IndexIDMap(base_index)
|
||||||
|
self.storage = {}
|
||||||
|
|
||||||
|
async def insert(self, vector: np.ndarray, id: int):
|
||||||
|
"""插入向量
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vector (np.ndarray): 要插入的向量
|
||||||
|
id (int): 向量的ID
|
||||||
|
Raises:
|
||||||
|
ValueError: 如果向量的维度与存储的维度不匹配
|
||||||
|
"""
|
||||||
|
if vector.shape[0] != self.dimension:
|
||||||
|
raise ValueError(
|
||||||
|
f"向量维度不匹配, 期望: {self.dimension}, 实际: {vector.shape[0]}"
|
||||||
|
)
|
||||||
|
self.index.add_with_ids(vector.reshape(1, -1), np.array([id]))
|
||||||
|
self.storage[id] = vector
|
||||||
|
await self.save_index()
|
||||||
|
|
||||||
|
async def search(self, vector: np.ndarray, k: int) -> tuple:
|
||||||
|
"""搜索最相似的向量
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vector (np.ndarray): 查询向量
|
||||||
|
k (int): 返回的最相似向量的数量
|
||||||
|
Returns:
|
||||||
|
tuple: (距离, 索引)
|
||||||
|
"""
|
||||||
|
faiss.normalize_L2(vector)
|
||||||
|
distances, indices = self.index.search(vector, k)
|
||||||
|
return distances, indices
|
||||||
|
|
||||||
|
async def save_index(self):
|
||||||
|
"""保存索引
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): 保存索引的路径
|
||||||
|
"""
|
||||||
|
faiss.write_index(self.index, self.path)
|
||||||
17
astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql
Normal file
17
astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- 创建文档存储表,包含 faiss 中文档的 id,文档文本,create_at,updated_at
|
||||||
|
CREATE TABLE documents (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
doc_id TEXT NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
metadata TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE documents
|
||||||
|
ADD COLUMN group_id TEXT GENERATED ALWAYS AS (json_extract(metadata, '$.group_id')) STORED;
|
||||||
|
ALTER TABLE documents
|
||||||
|
ADD COLUMN user_id TEXT GENERATED ALWAYS AS (json_extract(metadata, '$.user_id')) STORED;
|
||||||
|
|
||||||
|
CREATE INDEX idx_documents_user_id ON documents(user_id);
|
||||||
|
CREATE INDEX idx_documents_group_id ON documents(group_id);
|
||||||
117
astrbot/core/db/vec_db/faiss_impl/vec_db.py
Normal file
117
astrbot/core/db/vec_db/faiss_impl/vec_db.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
from .document_storage import DocumentStorage
|
||||||
|
from .embedding_storage import EmbeddingStorage
|
||||||
|
from ..base import Result, BaseVecDB
|
||||||
|
from astrbot.core.provider.provider import EmbeddingProvider
|
||||||
|
|
||||||
|
|
||||||
|
class FaissVecDB(BaseVecDB):
|
||||||
|
"""
|
||||||
|
A class to represent a vector database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
doc_store_path: str,
|
||||||
|
index_store_path: str,
|
||||||
|
embedding_provider: EmbeddingProvider,
|
||||||
|
):
|
||||||
|
self.doc_store_path = doc_store_path
|
||||||
|
self.index_store_path = index_store_path
|
||||||
|
self.embedding_provider = embedding_provider
|
||||||
|
self.document_storage = DocumentStorage(doc_store_path)
|
||||||
|
self.embedding_storage = EmbeddingStorage(
|
||||||
|
embedding_provider.get_dim(), index_store_path
|
||||||
|
)
|
||||||
|
self.embedding_provider = embedding_provider
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
await self.document_storage.initialize()
|
||||||
|
|
||||||
|
async def insert(self, content: str, metadata: dict = None, id: str = None) -> int:
|
||||||
|
"""
|
||||||
|
插入一条文本和其对应向量,自动生成 ID 并保持一致性。
|
||||||
|
"""
|
||||||
|
metadata = metadata or {}
|
||||||
|
str_id = id or str(uuid.uuid4()) # 使用 UUID 作为原始 ID
|
||||||
|
|
||||||
|
vector = await self.embedding_provider.get_embedding(content)
|
||||||
|
vector = np.array(vector, dtype=np.float32)
|
||||||
|
async with self.document_storage.connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"INSERT INTO documents (doc_id, text, metadata) VALUES (?, ?, ?)",
|
||||||
|
(str_id, content, json.dumps(metadata)),
|
||||||
|
)
|
||||||
|
await self.document_storage.connection.commit()
|
||||||
|
result = await self.document_storage.get_document_by_doc_id(str_id)
|
||||||
|
int_id = result["id"]
|
||||||
|
|
||||||
|
# 插入向量到 FAISS
|
||||||
|
await self.embedding_storage.insert(vector, int_id)
|
||||||
|
return int_id
|
||||||
|
|
||||||
|
async def retrieve(
|
||||||
|
self, query: str, k: int = 5, fetch_k: int = 20, metadata_filters: dict = None
|
||||||
|
) -> list[Result]:
|
||||||
|
"""
|
||||||
|
搜索最相似的文档。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): 查询文本
|
||||||
|
k (int): 返回的最相似文档的数量
|
||||||
|
fetch_k (int): 在根据 metadata 过滤前从 FAISS 中获取的数量
|
||||||
|
metadata_filters (dict): 元数据过滤器
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Result]: 查询结果
|
||||||
|
"""
|
||||||
|
embedding = await self.embedding_provider.get_embedding(query)
|
||||||
|
scores, indices = await self.embedding_storage.search(
|
||||||
|
vector=np.array([embedding]).astype("float32"),
|
||||||
|
k=fetch_k if metadata_filters else k,
|
||||||
|
)
|
||||||
|
# TODO: rerank
|
||||||
|
if len(indices[0]) == 0 or indices[0][0] == -1:
|
||||||
|
return []
|
||||||
|
# normalize scores
|
||||||
|
scores[0] = 1.0 - (scores[0] / 2.0)
|
||||||
|
# NOTE: maybe the size is less than k.
|
||||||
|
fetched_docs = await self.document_storage.get_documents(
|
||||||
|
metadata_filters=metadata_filters or {}, ids=indices[0]
|
||||||
|
)
|
||||||
|
if not fetched_docs:
|
||||||
|
return []
|
||||||
|
result_docs = []
|
||||||
|
|
||||||
|
idx_pos = {fetch_doc["id"]: idx for idx, fetch_doc in enumerate(fetched_docs)}
|
||||||
|
for i, indice_idx in enumerate(indices[0]):
|
||||||
|
pos = idx_pos.get(indice_idx)
|
||||||
|
if pos is None:
|
||||||
|
continue
|
||||||
|
fetch_doc = fetched_docs[pos]
|
||||||
|
score = scores[0][i]
|
||||||
|
result_docs.append(Result(similarity=float(score), data=fetch_doc))
|
||||||
|
return result_docs[:k]
|
||||||
|
|
||||||
|
async def delete(self, doc_id: int):
|
||||||
|
"""
|
||||||
|
删除一条文档
|
||||||
|
"""
|
||||||
|
await self.document_storage.connection.execute(
|
||||||
|
"DELETE FROM documents WHERE doc_id = ?", (doc_id,)
|
||||||
|
)
|
||||||
|
await self.document_storage.connection.commit()
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self.document_storage.close()
|
||||||
|
|
||||||
|
async def count_documents(self) -> int:
|
||||||
|
"""
|
||||||
|
计算文档数量
|
||||||
|
"""
|
||||||
|
async with self.document_storage.connection.cursor() as cursor:
|
||||||
|
await cursor.execute("SELECT COUNT(*) FROM documents")
|
||||||
|
count = await cursor.fetchone()
|
||||||
|
return count[0] if count else 0
|
||||||
@@ -26,13 +26,14 @@ class InitialLoader:
|
|||||||
async def start(self):
|
async def start(self):
|
||||||
core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db)
|
core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db)
|
||||||
|
|
||||||
core_task = []
|
|
||||||
try:
|
try:
|
||||||
await core_lifecycle.initialize()
|
await core_lifecycle.initialize()
|
||||||
core_task = core_lifecycle.start()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(traceback.format_exc())
|
logger.critical(traceback.format_exc())
|
||||||
logger.critical(f"😭 初始化 AstrBot 失败:{e} !!!")
|
logger.critical(f"😭 初始化 AstrBot 失败:{e} !!!")
|
||||||
|
return
|
||||||
|
|
||||||
|
core_task = core_lifecycle.start()
|
||||||
|
|
||||||
self.dashboard_server = AstrBotDashboard(
|
self.dashboard_server = AstrBotDashboard(
|
||||||
core_lifecycle, self.db, core_lifecycle.dashboard_shutdown_event
|
core_lifecycle, self.db, core_lifecycle.dashboard_shutdown_event
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ class BaseMessageComponent(BaseModel):
|
|||||||
data[k] = v
|
data[k] = v
|
||||||
return {"type": self.type.lower(), "data": data}
|
return {"type": self.type.lower(), "data": data}
|
||||||
|
|
||||||
|
async def to_dict(self) -> dict:
|
||||||
|
# 默认情况下,回退到旧的同步 toDict()
|
||||||
|
return self.toDict()
|
||||||
|
|
||||||
|
|
||||||
class Plain(BaseMessageComponent):
|
class Plain(BaseMessageComponent):
|
||||||
type: ComponentType = "Plain"
|
type: ComponentType = "Plain"
|
||||||
@@ -118,6 +122,9 @@ class Plain(BaseMessageComponent):
|
|||||||
self.text.replace("&", "&").replace("[", "[").replace("]", "]")
|
self.text.replace("&", "&").replace("[", "[").replace("]", "]")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
return {"type": "text", "data": {"text": self.text.strip()}}
|
||||||
|
|
||||||
|
|
||||||
class Face(BaseMessageComponent):
|
class Face(BaseMessageComponent):
|
||||||
type: ComponentType = "Face"
|
type: ComponentType = "Face"
|
||||||
@@ -235,9 +242,6 @@ class Video(BaseMessageComponent):
|
|||||||
path: T.Optional[str] = ""
|
path: T.Optional[str] = ""
|
||||||
|
|
||||||
def __init__(self, file: str, **_):
|
def __init__(self, file: str, **_):
|
||||||
# for k in _.keys():
|
|
||||||
# if k == "c" and _[k] not in [2, 3]:
|
|
||||||
# logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
|
|
||||||
super().__init__(file=file, **_)
|
super().__init__(file=file, **_)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -250,6 +254,70 @@ class Video(BaseMessageComponent):
|
|||||||
return Video(file=url, **_)
|
return Video(file=url, **_)
|
||||||
raise Exception("not a valid url")
|
raise Exception("not a valid url")
|
||||||
|
|
||||||
|
async def convert_to_file_path(self) -> str:
|
||||||
|
"""将这个视频统一转换为本地文件路径。这个方法避免了手动判断视频数据类型,直接返回视频数据的本地路径(如果是网络 URL,则会自动进行下载)。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 视频的本地路径,以绝对路径表示。
|
||||||
|
"""
|
||||||
|
url = self.file
|
||||||
|
if url and url.startswith("file:///"):
|
||||||
|
return url[8:]
|
||||||
|
elif url and url.startswith("http"):
|
||||||
|
download_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||||
|
video_file_path = os.path.join(download_dir, f"{uuid.uuid4().hex}")
|
||||||
|
await download_file(url, video_file_path)
|
||||||
|
if os.path.exists(video_file_path):
|
||||||
|
return os.path.abspath(video_file_path)
|
||||||
|
else:
|
||||||
|
raise Exception(f"download failed: {url}")
|
||||||
|
elif os.path.exists(url):
|
||||||
|
return os.path.abspath(url)
|
||||||
|
else:
|
||||||
|
raise Exception(f"not a valid file: {url}")
|
||||||
|
|
||||||
|
async def register_to_file_service(self):
|
||||||
|
"""
|
||||||
|
将视频注册到文件服务。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 注册后的URL
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: 如果未配置 callback_api_base
|
||||||
|
"""
|
||||||
|
callback_host = astrbot_config.get("callback_api_base")
|
||||||
|
|
||||||
|
if not callback_host:
|
||||||
|
raise Exception("未配置 callback_api_base,文件服务不可用")
|
||||||
|
|
||||||
|
file_path = await self.convert_to_file_path()
|
||||||
|
|
||||||
|
token = await file_token_service.register_file(file_path)
|
||||||
|
|
||||||
|
logger.debug(f"已注册:{callback_host}/api/file/{token}")
|
||||||
|
|
||||||
|
return f"{callback_host}/api/file/{token}"
|
||||||
|
|
||||||
|
async def to_dict(self):
|
||||||
|
"""需要和 toDict 区分开,toDict 是同步方法"""
|
||||||
|
url_or_path = self.file
|
||||||
|
if url_or_path.startswith("http"):
|
||||||
|
payload_file = url_or_path
|
||||||
|
elif callback_host := astrbot_config.get("callback_api_base"):
|
||||||
|
callback_host = str(callback_host).removesuffix("/")
|
||||||
|
token = await file_token_service.register_file(url_or_path)
|
||||||
|
payload_file = f"{callback_host}/api/file/{token}"
|
||||||
|
logger.debug(f"Generated video file callback link: {payload_file}")
|
||||||
|
else:
|
||||||
|
payload_file = url_or_path
|
||||||
|
return {
|
||||||
|
"type": "video",
|
||||||
|
"data": {
|
||||||
|
"file": payload_file,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class At(BaseMessageComponent):
|
class At(BaseMessageComponent):
|
||||||
type: ComponentType = "At"
|
type: ComponentType = "At"
|
||||||
@@ -259,6 +327,12 @@ class At(BaseMessageComponent):
|
|||||||
def __init__(self, **_):
|
def __init__(self, **_):
|
||||||
super().__init__(**_)
|
super().__init__(**_)
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
return {
|
||||||
|
"type": "at",
|
||||||
|
"data": {"qq": str(self.qq)},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AtAll(At):
|
class AtAll(At):
|
||||||
qq: str = "all"
|
qq: str = "all"
|
||||||
@@ -514,27 +588,47 @@ class Node(BaseMessageComponent):
|
|||||||
id: T.Optional[int] = 0 # 忽略
|
id: T.Optional[int] = 0 # 忽略
|
||||||
name: T.Optional[str] = "" # qq昵称
|
name: T.Optional[str] = "" # qq昵称
|
||||||
uin: T.Optional[str] = "0" # qq号
|
uin: T.Optional[str] = "0" # qq号
|
||||||
content: T.Optional[T.Union[str, list, dict]] = "" # 子消息段列表
|
content: T.Optional[list[BaseMessageComponent]] = []
|
||||||
seq: T.Optional[T.Union[str, list]] = "" # 忽略
|
seq: T.Optional[T.Union[str, list]] = "" # 忽略
|
||||||
time: T.Optional[int] = 0 # 忽略
|
time: T.Optional[int] = 0 # 忽略
|
||||||
|
|
||||||
def __init__(self, content: T.Union[str, list, dict, "Node", T.List["Node"]], **_):
|
def __init__(self, content: list[BaseMessageComponent], **_):
|
||||||
if isinstance(content, list):
|
if isinstance(content, Node):
|
||||||
_content = None
|
# back
|
||||||
if all(isinstance(item, Node) for item in content):
|
content = [content]
|
||||||
_content = [node.toDict() for node in content]
|
|
||||||
else:
|
|
||||||
_content = ""
|
|
||||||
for chain in content:
|
|
||||||
_content += chain.toString()
|
|
||||||
content = _content
|
|
||||||
elif isinstance(content, Node):
|
|
||||||
content = content.toDict()
|
|
||||||
super().__init__(content=content, **_)
|
super().__init__(content=content, **_)
|
||||||
|
|
||||||
def toString(self):
|
async def to_dict(self):
|
||||||
# logger.warn("Protocol: node doesn't support stringify")
|
data_content = []
|
||||||
return ""
|
for comp in self.content:
|
||||||
|
if isinstance(comp, (Image, Record)):
|
||||||
|
# For Image and Record segments, we convert them to base64
|
||||||
|
bs64 = await comp.convert_to_base64()
|
||||||
|
data_content.append(
|
||||||
|
{
|
||||||
|
"type": comp.type.lower(),
|
||||||
|
"data": {"file": f"base64://{bs64}"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif isinstance(comp, File):
|
||||||
|
# For File segments, we need to handle the file differently
|
||||||
|
d = await comp.to_dict()
|
||||||
|
data_content.append(d)
|
||||||
|
elif isinstance(comp, (Node, Nodes)):
|
||||||
|
# For Node segments, we recursively convert them to dict
|
||||||
|
d = await comp.to_dict()
|
||||||
|
data_content.append(d)
|
||||||
|
else:
|
||||||
|
d = comp.toDict()
|
||||||
|
data_content.append(d)
|
||||||
|
return {
|
||||||
|
"type": "node",
|
||||||
|
"data": {
|
||||||
|
"user_id": str(self.uin),
|
||||||
|
"nickname": self.name,
|
||||||
|
"content": data_content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Nodes(BaseMessageComponent):
|
class Nodes(BaseMessageComponent):
|
||||||
@@ -545,12 +639,20 @@ class Nodes(BaseMessageComponent):
|
|||||||
super().__init__(nodes=nodes, **_)
|
super().__init__(nodes=nodes, **_)
|
||||||
|
|
||||||
def toDict(self):
|
def toDict(self):
|
||||||
|
"""Deprecated. Use to_dict instead"""
|
||||||
ret = {
|
ret = {
|
||||||
"messages": [],
|
"messages": [],
|
||||||
}
|
}
|
||||||
for node in self.nodes:
|
for node in self.nodes:
|
||||||
d = node.toDict()
|
d = node.toDict()
|
||||||
d["data"]["uin"] = str(node.uin) # 转为字符串
|
ret["messages"].append(d)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def to_dict(self):
|
||||||
|
"""将 Nodes 转换为字典格式,适用于 OneBot JSON 格式"""
|
||||||
|
ret = {"messages": []}
|
||||||
|
for node in self.nodes:
|
||||||
|
d = await node.to_dict()
|
||||||
ret["messages"].append(d)
|
ret["messages"].append(d)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -723,6 +825,26 @@ class File(BaseMessageComponent):
|
|||||||
|
|
||||||
return f"{callback_host}/api/file/{token}"
|
return f"{callback_host}/api/file/{token}"
|
||||||
|
|
||||||
|
async def to_dict(self):
|
||||||
|
"""需要和 toDict 区分开,toDict 是同步方法"""
|
||||||
|
url_or_path = await self.get_file(allow_return_url=True)
|
||||||
|
if url_or_path.startswith("http"):
|
||||||
|
payload_file = url_or_path
|
||||||
|
elif callback_host := astrbot_config.get("callback_api_base"):
|
||||||
|
callback_host = str(callback_host).removesuffix("/")
|
||||||
|
token = await file_token_service.register_file(url_or_path)
|
||||||
|
payload_file = f"{callback_host}/api/file/{token}"
|
||||||
|
logger.debug(f"Generated file callback link: {payload_file}")
|
||||||
|
else:
|
||||||
|
payload_file = url_or_path
|
||||||
|
return {
|
||||||
|
"type": "file",
|
||||||
|
"data": {
|
||||||
|
"name": self.name,
|
||||||
|
"file": payload_file,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class WechatEmoji(BaseMessageComponent):
|
class WechatEmoji(BaseMessageComponent):
|
||||||
type: ComponentType = "WechatEmoji"
|
type: ComponentType = "WechatEmoji"
|
||||||
|
|||||||
@@ -46,28 +46,29 @@ class PreProcessStage(Stage):
|
|||||||
stt_provider = (
|
stt_provider = (
|
||||||
self.plugin_manager.context.provider_manager.curr_stt_provider_inst
|
self.plugin_manager.context.provider_manager.curr_stt_provider_inst
|
||||||
)
|
)
|
||||||
if stt_provider:
|
if not stt_provider:
|
||||||
message_chain = event.get_messages()
|
return
|
||||||
for idx, component in enumerate(message_chain):
|
message_chain = event.get_messages()
|
||||||
if isinstance(component, Record) and component.url:
|
for idx, component in enumerate(message_chain):
|
||||||
path = component.url.removeprefix("file://")
|
if isinstance(component, Record) and component.url:
|
||||||
retry = 5
|
path = component.url.removeprefix("file://")
|
||||||
for i in range(retry):
|
retry = 5
|
||||||
try:
|
for i in range(retry):
|
||||||
result = await stt_provider.get_text(audio_url=path)
|
try:
|
||||||
if result:
|
result = await stt_provider.get_text(audio_url=path)
|
||||||
logger.info("语音转文本结果: " + result)
|
if result:
|
||||||
message_chain[idx] = Plain(result)
|
logger.info("语音转文本结果: " + result)
|
||||||
event.message_str += result
|
message_chain[idx] = Plain(result)
|
||||||
event.message_obj.message_str += result
|
event.message_str += result
|
||||||
break
|
event.message_obj.message_str += result
|
||||||
except FileNotFoundError as e:
|
break
|
||||||
# napcat workaround
|
except FileNotFoundError as e:
|
||||||
logger.warning(e)
|
# napcat workaround
|
||||||
logger.warning(f"重试中: {i + 1}/{retry}")
|
logger.warning(e)
|
||||||
await asyncio.sleep(0.5)
|
logger.warning(f"重试中: {i + 1}/{retry}")
|
||||||
continue
|
await asyncio.sleep(0.5)
|
||||||
except BaseException as e:
|
continue
|
||||||
logger.error(traceback.format_exc())
|
except BaseException as e:
|
||||||
logger.error(f"语音转文本失败: {e}")
|
logger.error(traceback.format_exc())
|
||||||
break
|
logger.error(f"语音转文本失败: {e}")
|
||||||
|
break
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ class LLMRequestSubStage(Stage):
|
|||||||
) -> Union[None, AsyncGenerator[None, None]]:
|
) -> Union[None, AsyncGenerator[None, None]]:
|
||||||
req: ProviderRequest = None
|
req: ProviderRequest = None
|
||||||
|
|
||||||
|
if not self.ctx.astrbot_config["provider_settings"]["enable"]:
|
||||||
|
logger.debug("未启用 LLM 能力,跳过处理。")
|
||||||
|
return
|
||||||
|
|
||||||
provider = self.ctx.plugin_manager.context.get_using_provider()
|
provider = self.ctx.plugin_manager.context.get_using_provider()
|
||||||
if provider is None:
|
if provider is None:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ class RespondStage(Stage):
|
|||||||
Comp.Image: lambda comp: bool(comp.file), # 图片
|
Comp.Image: lambda comp: bool(comp.file), # 图片
|
||||||
Comp.Reply: lambda comp: bool(comp.id) and comp.sender_id is not None, # 回复
|
Comp.Reply: lambda comp: bool(comp.id) and comp.sender_id is not None, # 回复
|
||||||
Comp.Poke: lambda comp: comp.id != 0 and comp.qq != 0, # 戳一戳
|
Comp.Poke: lambda comp: comp.id != 0 and comp.qq != 0, # 戳一戳
|
||||||
Comp.Node: lambda comp: bool(comp.name)
|
Comp.Node: lambda comp: bool(comp.content), # 转发节点
|
||||||
and comp.uin != 0
|
|
||||||
and bool(comp.content), # 一个转发节点
|
|
||||||
Comp.Nodes: lambda comp: bool(comp.nodes), # 多个转发节点
|
Comp.Nodes: lambda comp: bool(comp.nodes), # 多个转发节点
|
||||||
Comp.File: lambda comp: bool(comp.file_ or comp.url),
|
Comp.File: lambda comp: bool(comp.file_ or comp.url),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import time
|
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Union, AsyncGenerator
|
from typing import AsyncGenerator, Union
|
||||||
from ..stage import Stage, register_stage, registered_stages
|
|
||||||
from ..context import PipelineContext
|
from astrbot.core import html_renderer, logger, file_token_service
|
||||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
from astrbot.core.message.components import At, File, Image, Node, Plain, Record, Reply
|
||||||
from astrbot.core.message.message_event_result import ResultContentType
|
from astrbot.core.message.message_event_result import ResultContentType
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
from astrbot.core.platform.message_type import MessageType
|
from astrbot.core.platform.message_type import MessageType
|
||||||
from astrbot.core import logger
|
|
||||||
from astrbot.core.message.components import Plain, Image, At, Reply, Record, File, Node
|
|
||||||
from astrbot.core import html_renderer
|
|
||||||
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
|
||||||
from astrbot.core.star.star import star_map
|
from astrbot.core.star.star import star_map
|
||||||
|
from astrbot.core.star.star_handler import EventType, star_handlers_registry
|
||||||
|
|
||||||
|
from ..context import PipelineContext
|
||||||
|
from ..stage import Stage, register_stage, registered_stages
|
||||||
|
|
||||||
|
|
||||||
@register_stage
|
@register_stage
|
||||||
@@ -168,30 +169,55 @@ class ResultDecorateStage(Stage):
|
|||||||
result.chain = new_chain
|
result.chain = new_chain
|
||||||
|
|
||||||
# TTS
|
# TTS
|
||||||
|
tts_provider = (
|
||||||
|
self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
self.ctx.astrbot_config["provider_tts_settings"]["enable"]
|
self.ctx.astrbot_config["provider_tts_settings"]["enable"]
|
||||||
and result.is_llm_result()
|
and result.is_llm_result()
|
||||||
|
and tts_provider
|
||||||
):
|
):
|
||||||
tts_provider = self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst
|
|
||||||
new_chain = []
|
new_chain = []
|
||||||
for comp in result.chain:
|
for comp in result.chain:
|
||||||
if isinstance(comp, Plain) and len(comp.text) > 1:
|
if isinstance(comp, Plain) and len(comp.text) > 1:
|
||||||
try:
|
try:
|
||||||
logger.info("TTS 请求: " + comp.text)
|
logger.info(f"TTS 请求: {comp.text}")
|
||||||
audio_path = await tts_provider.get_audio(comp.text)
|
audio_path = await tts_provider.get_audio(comp.text)
|
||||||
logger.info("TTS 结果: " + audio_path)
|
logger.info(f"TTS 结果: {audio_path}")
|
||||||
if audio_path:
|
if not audio_path:
|
||||||
new_chain.append(
|
|
||||||
Record(file=audio_path, url=audio_path)
|
|
||||||
)
|
|
||||||
if(self.ctx.astrbot_config["provider_tts_settings"]["dual_output"]):
|
|
||||||
new_chain.append(comp)
|
|
||||||
else:
|
|
||||||
logger.error(
|
logger.error(
|
||||||
f"由于 TTS 音频文件没找到,消息段转语音失败: {comp.text}"
|
f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}"
|
||||||
)
|
)
|
||||||
new_chain.append(comp)
|
new_chain.append(comp)
|
||||||
except BaseException:
|
continue
|
||||||
|
|
||||||
|
use_file_service = self.ctx.astrbot_config[
|
||||||
|
"provider_tts_settings"
|
||||||
|
]["use_file_service"]
|
||||||
|
callback_api_base = self.ctx.astrbot_config[
|
||||||
|
"callback_api_base"
|
||||||
|
]
|
||||||
|
dual_output = self.ctx.astrbot_config[
|
||||||
|
"provider_tts_settings"
|
||||||
|
]["dual_output"]
|
||||||
|
|
||||||
|
url = None
|
||||||
|
if use_file_service and callback_api_base:
|
||||||
|
token = await file_token_service.register_file(
|
||||||
|
audio_path
|
||||||
|
)
|
||||||
|
url = f"{callback_api_base}/api/file/{token}"
|
||||||
|
logger.debug(f"已注册:{url}")
|
||||||
|
|
||||||
|
new_chain.append(
|
||||||
|
Record(
|
||||||
|
file=url or audio_path,
|
||||||
|
url=url or audio_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if dual_output:
|
||||||
|
new_chain.append(comp)
|
||||||
|
except Exception:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
logger.error("TTS 失败,使用文本发送。")
|
logger.error("TTS 失败,使用文本发送。")
|
||||||
new_chain.append(comp)
|
new_chain.append(comp)
|
||||||
@@ -225,6 +251,14 @@ class ResultDecorateStage(Stage):
|
|||||||
if url:
|
if url:
|
||||||
if url.startswith("http"):
|
if url.startswith("http"):
|
||||||
result.chain = [Image.fromURL(url)]
|
result.chain = [Image.fromURL(url)]
|
||||||
|
elif (
|
||||||
|
self.ctx.astrbot_config["t2i_use_file_service"]
|
||||||
|
and self.ctx.astrbot_config["callback_api_base"]
|
||||||
|
):
|
||||||
|
token = await file_token_service.register_file(url)
|
||||||
|
url = f"{self.ctx.astrbot_config['callback_api_base']}/api/file/{token}"
|
||||||
|
logger.debug(f"已注册:{url}")
|
||||||
|
result.chain = [Image.fromURL(url)]
|
||||||
else:
|
else:
|
||||||
result.chain = [Image.fromFileSystem(url)]
|
result.chain = [Image.fromFileSystem(url)]
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,17 @@ import re
|
|||||||
from typing import AsyncGenerator, Dict, List
|
from typing import AsyncGenerator, Dict, List
|
||||||
from aiocqhttp import CQHttp
|
from aiocqhttp import CQHttp
|
||||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||||
from astrbot.api.message_components import At, Image, Node, Nodes, Plain, Record, File
|
from astrbot.api.message_components import (
|
||||||
|
Image,
|
||||||
|
Node,
|
||||||
|
Nodes,
|
||||||
|
Plain,
|
||||||
|
Record,
|
||||||
|
Video,
|
||||||
|
File,
|
||||||
|
BaseMessageComponent,
|
||||||
|
)
|
||||||
from astrbot.api.platform import Group, MessageMember
|
from astrbot.api.platform import Group, MessageMember
|
||||||
from astrbot.core import file_token_service, astrbot_config, logger
|
|
||||||
|
|
||||||
|
|
||||||
class AiocqhttpMessageEvent(AstrMessageEvent):
|
class AiocqhttpMessageEvent(AstrMessageEvent):
|
||||||
@@ -15,28 +23,38 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|||||||
super().__init__(message_str, message_obj, platform_meta, session_id)
|
super().__init__(message_str, message_obj, platform_meta, session_id)
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _from_segment_to_dict(segment: BaseMessageComponent) -> dict:
|
||||||
|
"""修复部分字段"""
|
||||||
|
if isinstance(segment, (Image, Record)):
|
||||||
|
# For Image and Record segments, we convert them to base64
|
||||||
|
bs64 = await segment.convert_to_base64()
|
||||||
|
return {
|
||||||
|
"type": segment.type.lower(),
|
||||||
|
"data": {
|
||||||
|
"file": f"base64://{bs64}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
elif isinstance(segment, File):
|
||||||
|
# For File segments, we need to handle the file differently
|
||||||
|
d = await segment.to_dict()
|
||||||
|
return d
|
||||||
|
elif isinstance(segment, Video):
|
||||||
|
d = await segment.to_dict()
|
||||||
|
return d
|
||||||
|
else:
|
||||||
|
# For other segments, we simply convert them to a dict by calling toDict
|
||||||
|
return segment.toDict()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _parse_onebot_json(message_chain: MessageChain):
|
async def _parse_onebot_json(message_chain: MessageChain):
|
||||||
"""解析成 OneBot json 格式"""
|
"""解析成 OneBot json 格式"""
|
||||||
ret = []
|
ret = []
|
||||||
for segment in message_chain.chain:
|
for segment in message_chain.chain:
|
||||||
d = segment.toDict()
|
|
||||||
if isinstance(segment, Plain):
|
if isinstance(segment, Plain):
|
||||||
d["type"] = "text"
|
if not segment.text.strip():
|
||||||
d["data"]["text"] = segment.text.strip()
|
|
||||||
# 如果是空文本或者只带换行符的文本,不发送
|
|
||||||
if not d["data"]["text"]:
|
|
||||||
continue
|
continue
|
||||||
elif isinstance(segment, (Image, Record)):
|
d = await AiocqhttpMessageEvent._from_segment_to_dict(segment)
|
||||||
# convert to base64
|
|
||||||
bs64 = await segment.convert_to_base64()
|
|
||||||
d["data"] = {
|
|
||||||
"file": f"base64://{bs64}",
|
|
||||||
}
|
|
||||||
elif isinstance(segment, At):
|
|
||||||
d["data"] = {
|
|
||||||
"qq": str(segment.qq), # 转换为字符串
|
|
||||||
}
|
|
||||||
ret.append(d)
|
ret.append(d)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -54,7 +72,8 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|||||||
nodes = Nodes([seg])
|
nodes = Nodes([seg])
|
||||||
seg = nodes
|
seg = nodes
|
||||||
|
|
||||||
payload = seg.toDict()
|
payload = await seg.to_dict()
|
||||||
|
|
||||||
if self.get_group_id():
|
if self.get_group_id():
|
||||||
payload["group_id"] = self.get_group_id()
|
payload["group_id"] = self.get_group_id()
|
||||||
await self.bot.call_action("send_group_forward_msg", **payload)
|
await self.bot.call_action("send_group_forward_msg", **payload)
|
||||||
@@ -64,21 +83,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|||||||
"send_private_forward_msg", **payload
|
"send_private_forward_msg", **payload
|
||||||
)
|
)
|
||||||
elif isinstance(seg, File):
|
elif isinstance(seg, File):
|
||||||
d = seg.toDict()
|
d = await AiocqhttpMessageEvent._from_segment_to_dict(seg)
|
||||||
url_or_path = await seg.get_file(allow_return_url=True)
|
|
||||||
if url_or_path.startswith("http"):
|
|
||||||
payload_file = url_or_path
|
|
||||||
elif callback_host := astrbot_config.get("callback_api_base"):
|
|
||||||
callback_host = str(callback_host).removesuffix("/")
|
|
||||||
token = await file_token_service.register_file(url_or_path)
|
|
||||||
payload_file = f"{callback_host}/api/file/{token}"
|
|
||||||
logger.debug(f"Generated file callback link: {payload_file}")
|
|
||||||
else:
|
|
||||||
payload_file = url_or_path
|
|
||||||
d["data"] = {
|
|
||||||
"name": seg.name,
|
|
||||||
"file": payload_file,
|
|
||||||
}
|
|
||||||
await self.bot.send(
|
await self.bot.send(
|
||||||
self.message_obj.raw_message,
|
self.message_obj.raw_message,
|
||||||
[d],
|
[d],
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ class TelegramPlatformAdapter(Platform):
|
|||||||
command_dict = {}
|
command_dict = {}
|
||||||
skip_commands = {"start"}
|
skip_commands = {"start"}
|
||||||
|
|
||||||
for handler_md in star_handlers_registry._handlers:
|
for handler_md in star_handlers_registry:
|
||||||
handler_metadata = handler_md[1]
|
handler_metadata = handler_md
|
||||||
if not star_map[handler_metadata.handler_module_path].activated:
|
if not star_map[handler_metadata.handler_module_path].activated:
|
||||||
continue
|
continue
|
||||||
for event_filter in handler_metadata.event_filters:
|
for event_filter in handler_metadata.event_filters:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
import telegramify_markdown
|
import telegramify_markdown
|
||||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||||
@@ -18,6 +19,16 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|||||||
|
|
||||||
|
|
||||||
class TelegramPlatformEvent(AstrMessageEvent):
|
class TelegramPlatformEvent(AstrMessageEvent):
|
||||||
|
# Telegram 的最大消息长度限制
|
||||||
|
MAX_MESSAGE_LENGTH = 4096
|
||||||
|
|
||||||
|
SPLIT_PATTERNS = {
|
||||||
|
"paragraph": re.compile(r"\n\n"),
|
||||||
|
"line": re.compile(r"\n"),
|
||||||
|
"sentence": re.compile(r"[.!?。!?]"),
|
||||||
|
"word": re.compile(r"\s"),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message_str: str,
|
message_str: str,
|
||||||
@@ -29,8 +40,33 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|||||||
super().__init__(message_str, message_obj, platform_meta, session_id)
|
super().__init__(message_str, message_obj, platform_meta, session_id)
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
@staticmethod
|
def _split_message(self, text: str) -> list[str]:
|
||||||
async def send_with_client(client: ExtBot, message: MessageChain, user_name: str):
|
if len(text) <= self.MAX_MESSAGE_LENGTH:
|
||||||
|
return [text]
|
||||||
|
|
||||||
|
chunks = []
|
||||||
|
while text:
|
||||||
|
if len(text) <= self.MAX_MESSAGE_LENGTH:
|
||||||
|
chunks.append(text)
|
||||||
|
break
|
||||||
|
|
||||||
|
split_point = self.MAX_MESSAGE_LENGTH
|
||||||
|
segment = text[: self.MAX_MESSAGE_LENGTH]
|
||||||
|
|
||||||
|
for _, pattern in self.SPLIT_PATTERNS.items():
|
||||||
|
if matches := list(pattern.finditer(segment)):
|
||||||
|
last_match = matches[-1]
|
||||||
|
split_point = last_match.end()
|
||||||
|
break
|
||||||
|
|
||||||
|
chunks.append(text[:split_point])
|
||||||
|
text = text[split_point:].lstrip()
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
async def send_with_client(
|
||||||
|
self, client: ExtBot, message: MessageChain, user_name: str
|
||||||
|
):
|
||||||
image_path = None
|
image_path = None
|
||||||
|
|
||||||
has_reply = False
|
has_reply = False
|
||||||
@@ -59,19 +95,22 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|||||||
|
|
||||||
if isinstance(i, Plain):
|
if isinstance(i, Plain):
|
||||||
if at_user_id and not at_flag:
|
if at_user_id and not at_flag:
|
||||||
i.text = f"@{at_user_id} " + i.text
|
i.text = f"@{at_user_id} {i.text}"
|
||||||
at_flag = True
|
at_flag = True
|
||||||
text = i.text
|
chunks = self._split_message(i.text)
|
||||||
try:
|
for chunk in chunks:
|
||||||
text = telegramify_markdown.markdownify(
|
try:
|
||||||
i.text, max_line_length=None, normalize_whitespace=False
|
md_text = telegramify_markdown.markdownify(
|
||||||
)
|
chunk, max_line_length=None, normalize_whitespace=False
|
||||||
except Exception as e:
|
)
|
||||||
logger.warning(
|
await client.send_message(
|
||||||
f"MarkdownV2 conversion failed: {e}. Using plain text instead."
|
text=md_text, parse_mode="MarkdownV2", **payload
|
||||||
)
|
)
|
||||||
return
|
except Exception as e:
|
||||||
await client.send_message(text=text, parse_mode="MarkdownV2", **payload)
|
logger.warning(
|
||||||
|
f"MarkdownV2 send failed: {e}. Using plain text instead."
|
||||||
|
)
|
||||||
|
await client.send_message(text=chunk, **payload)
|
||||||
elif isinstance(i, Image):
|
elif isinstance(i, Image):
|
||||||
image_path = await i.convert_to_file_path()
|
image_path = await i.convert_to_file_path()
|
||||||
await client.send_photo(photo=image_path, **payload)
|
await client.send_photo(photo=image_path, **payload)
|
||||||
@@ -147,17 +186,7 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Plain
|
# Plain
|
||||||
if not message_id:
|
if message_id and len(delta) <= self.MAX_MESSAGE_LENGTH:
|
||||||
try:
|
|
||||||
msg = await self.client.send_message(text=delta, **payload)
|
|
||||||
current_content = delta
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"发送消息失败(streaming): {e!s}")
|
|
||||||
message_id = msg.message_id
|
|
||||||
last_edit_time = (
|
|
||||||
asyncio.get_event_loop().time()
|
|
||||||
) # 记录初始消息发送时间
|
|
||||||
else:
|
|
||||||
current_time = asyncio.get_event_loop().time()
|
current_time = asyncio.get_event_loop().time()
|
||||||
time_since_last_edit = current_time - last_edit_time
|
time_since_last_edit = current_time - last_edit_time
|
||||||
|
|
||||||
@@ -176,6 +205,18 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|||||||
last_edit_time = (
|
last_edit_time = (
|
||||||
asyncio.get_event_loop().time()
|
asyncio.get_event_loop().time()
|
||||||
) # 更新上次编辑的时间
|
) # 更新上次编辑的时间
|
||||||
|
else:
|
||||||
|
# delta 长度一般不会大于 4096,因此这里直接发送
|
||||||
|
try:
|
||||||
|
msg = await self.client.send_message(text=delta, **payload)
|
||||||
|
current_content = delta
|
||||||
|
delta = ""
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"发送消息失败(streaming): {e!s}")
|
||||||
|
message_id = msg.message_id
|
||||||
|
last_edit_time = (
|
||||||
|
asyncio.get_event_loop().time()
|
||||||
|
) # 记录初始消息发送时间
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if delta and current_content != delta:
|
if delta and current_content != delta:
|
||||||
|
|||||||
@@ -69,39 +69,42 @@ class WeChatPadProAdapter(Platform):
|
|||||||
self.auth_key = loaded_credentials.get("auth_key")
|
self.auth_key = loaded_credentials.get("auth_key")
|
||||||
self.wxid = loaded_credentials.get("wxid")
|
self.wxid = loaded_credentials.get("wxid")
|
||||||
|
|
||||||
|
isLoginIn = await self.check_online_status()
|
||||||
|
|
||||||
# 检查在线状态
|
# 检查在线状态
|
||||||
if self.auth_key and await self.check_online_status():
|
if self.auth_key and isLoginIn:
|
||||||
logger.info("WeChatPadPro 设备已在线,跳过扫码登录。")
|
logger.info("WeChatPadPro 设备已在线,凭据存在,跳过扫码登录。")
|
||||||
# 如果在线,连接 WebSocket 接收消息
|
# 如果在线,连接 WebSocket 接收消息
|
||||||
self.ws_handle_task = asyncio.create_task(self.connect_websocket())
|
self.ws_handle_task = asyncio.create_task(self.connect_websocket())
|
||||||
else:
|
else:
|
||||||
logger.info("WeChatPadPro 设备不在线或无可用凭据,开始扫码登录流程。")
|
|
||||||
# 1. 生成授权码
|
# 1. 生成授权码
|
||||||
await self.generate_auth_key()
|
|
||||||
|
|
||||||
if not self.auth_key:
|
if not self.auth_key:
|
||||||
logger.error("无法获取授权码,WeChatPadPro 适配器启动失败。")
|
logger.info("WeChatPadPro 无可用凭据,将生成新的授权码。")
|
||||||
return
|
await self.generate_auth_key()
|
||||||
|
|
||||||
# 2. 获取登录二维码
|
# 2. 获取登录二维码
|
||||||
qr_code_url = await self.get_login_qr_code()
|
if not isLoginIn:
|
||||||
|
logger.info("WeChatPadPro 设备已离线,开始扫码登录。")
|
||||||
|
qr_code_url = await self.get_login_qr_code()
|
||||||
|
|
||||||
if qr_code_url:
|
if qr_code_url:
|
||||||
logger.info(f"请扫描以下二维码登录: {qr_code_url}")
|
logger.info(f"请扫描以下二维码登录: {qr_code_url}")
|
||||||
else:
|
else:
|
||||||
logger.error("无法获取登录二维码。")
|
logger.error("无法获取登录二维码。")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 3. 检测扫码状态
|
# 3. 检测扫码状态
|
||||||
login_successful = await self.check_login_status()
|
login_successful = await self.check_login_status()
|
||||||
|
|
||||||
if login_successful:
|
if login_successful:
|
||||||
# 登录成功后,连接 WebSocket 接收消息
|
logger.info("登录成功,WeChatPadPro适配器已连接。")
|
||||||
self.ws_handle_task = asyncio.create_task(self.connect_websocket())
|
else:
|
||||||
else:
|
logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。")
|
||||||
logger.warning("登录失败或超时,WeChatPadPro 适配器将关闭。")
|
await self.terminate()
|
||||||
await self.terminate()
|
return
|
||||||
return
|
|
||||||
|
# 登录成功后,连接 WebSocket 接收消息
|
||||||
|
self.ws_handle_task = asyncio.create_task(self.connect_websocket())
|
||||||
|
|
||||||
self._shutdown_event = asyncio.Event()
|
self._shutdown_event = asyncio.Event()
|
||||||
await self._shutdown_event.wait()
|
await self._shutdown_event.wait()
|
||||||
@@ -156,16 +159,29 @@ class WeChatPadProAdapter(Platform):
|
|||||||
if login_state == 1:
|
if login_state == 1:
|
||||||
logger.info("WeChatPadPro 设备当前在线。")
|
logger.info("WeChatPadPro 设备当前在线。")
|
||||||
return True
|
return True
|
||||||
else:
|
# login_state == 3 为离线状态
|
||||||
|
elif login_state == 3:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"WeChatPadPro 设备不在线,登录状态: {login_state}"
|
"WeChatPadPro 设备不在线。"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"未知的在线状态: {login_state:}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
# Code == 300 为微信退出状态。
|
||||||
|
elif response.status == 200 and response_data.get("Code") == 300:
|
||||||
|
logger.info(
|
||||||
|
"WeChatPadPro 设备已退出。"
|
||||||
|
)
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"检查在线状态失败: {response.status}, {response_data}"
|
f"检查在线状态失败: {response.status}, {response_data}"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except aiohttp.ClientConnectorError as e:
|
except aiohttp.ClientConnectorError as e:
|
||||||
logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
|
logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -179,7 +195,7 @@ class WeChatPadProAdapter(Platform):
|
|||||||
"""
|
"""
|
||||||
url = f"{self.base_url}/admin/GenAuthKey1"
|
url = f"{self.base_url}/admin/GenAuthKey1"
|
||||||
params = {"key": self.admin_key}
|
params = {"key": self.admin_key}
|
||||||
payload = {"Count": 1, "Days": 30} # 生成一个有效期30天的授权码
|
payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
try:
|
try:
|
||||||
@@ -336,12 +352,10 @@ class WeChatPadProAdapter(Platform):
|
|||||||
message = await asyncio.wait_for(
|
message = await asyncio.wait_for(
|
||||||
websocket.recv(), timeout=wait_time
|
websocket.recv(), timeout=wait_time
|
||||||
)
|
)
|
||||||
logger.info(message)
|
# logger.debug(message) # 不显示原始消息内容
|
||||||
asyncio.create_task(self.handle_websocket_message(message))
|
asyncio.create_task(self.handle_websocket_message(message))
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.warning(
|
logger.warning(f"WebSocket 连接空闲超过 {wait_time} s")
|
||||||
f"WebSocket 连接空闲超过 {wait_time} s"
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
except websockets.exceptions.ConnectionClosedOK:
|
except websockets.exceptions.ConnectionClosedOK:
|
||||||
logger.info("WebSocket 连接正常关闭。")
|
logger.info("WebSocket 连接正常关闭。")
|
||||||
@@ -350,7 +364,7 @@ class WeChatPadProAdapter(Platform):
|
|||||||
logger.error(f"处理 WebSocket 消息时发生错误: {e}")
|
logger.error(f"处理 WebSocket 消息时发生错误: {e}")
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"WebSocket 连接失败: {e}")
|
logger.error(f"WebSocket 连接失败: {e}, 请检查WeChatPadPro服务状态,或尝试重启WeChatPadPro适配器。")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
async def handle_websocket_message(self, message: str):
|
async def handle_websocket_message(self, message: str):
|
||||||
@@ -627,3 +641,67 @@ class WeChatPadProAdapter(Platform):
|
|||||||
)
|
)
|
||||||
# 调用实例方法 send
|
# 调用实例方法 send
|
||||||
await sending_event.send(message_chain)
|
await sending_event.send(message_chain)
|
||||||
|
|
||||||
|
async def get_contact_list(self):
|
||||||
|
"""
|
||||||
|
获取联系人列表。
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/friend/GetContactList"
|
||||||
|
params = {"key": self.auth_key}
|
||||||
|
payload = {"CurrentChatRoomContactSeq": 0, "CurrentWxcontactSeq": 0}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.post(url, params=params, json=payload) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
logger.error(f"获取联系人列表失败: {response.status}")
|
||||||
|
return None
|
||||||
|
result = await response.json()
|
||||||
|
if result.get("Code") == 200 and result.get("Data"):
|
||||||
|
contact_list = (
|
||||||
|
result.get("Data", {})
|
||||||
|
.get("ContactList", {})
|
||||||
|
.get("contactUsernameList", [])
|
||||||
|
)
|
||||||
|
return contact_list
|
||||||
|
else:
|
||||||
|
logger.error(f"获取联系人列表失败: {result}")
|
||||||
|
return None
|
||||||
|
except aiohttp.ClientConnectorError as e:
|
||||||
|
logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取联系人列表时发生错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_contact_details_list(
|
||||||
|
self, room_wx_id_list: list[str] = None, user_names: list[str] = None
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
获取联系人详情列表。
|
||||||
|
"""
|
||||||
|
if room_wx_id_list is None:
|
||||||
|
room_wx_id_list = []
|
||||||
|
if user_names is None:
|
||||||
|
user_names = []
|
||||||
|
url = f"{self.base_url}/friend/GetContactDetailsList"
|
||||||
|
params = {"key": self.auth_key}
|
||||||
|
payload = {"RoomWxIDList": room_wx_id_list, "UserNames": user_names}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
try:
|
||||||
|
async with session.post(url, params=params, json=payload) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
logger.error(f"获取联系人详情列表失败: {response.status}")
|
||||||
|
return None
|
||||||
|
result = await response.json()
|
||||||
|
if result.get("Code") == 200 and result.get("Data"):
|
||||||
|
contact_list = result.get("Data", {}).get("contactList", {})
|
||||||
|
return contact_list
|
||||||
|
else:
|
||||||
|
logger.error(f"获取联系人详情列表失败: {result}")
|
||||||
|
return None
|
||||||
|
except aiohttp.ClientConnectorError as e:
|
||||||
|
logger.error(f"连接到 WeChatPadPro 服务失败: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取联系人详情列表时发生错误: {e}")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from requests import Response
|
|||||||
from wechatpy.utils import check_signature
|
from wechatpy.utils import check_signature
|
||||||
from wechatpy.crypto import WeChatCrypto
|
from wechatpy.crypto import WeChatCrypto
|
||||||
from wechatpy import WeChatClient
|
from wechatpy import WeChatClient
|
||||||
from wechatpy.messages import TextMessage, ImageMessage, VoiceMessage
|
from wechatpy.messages import TextMessage, ImageMessage, VoiceMessage, BaseMessage
|
||||||
from wechatpy.exceptions import InvalidSignatureException
|
from wechatpy.exceptions import InvalidSignatureException
|
||||||
from wechatpy import parse_message
|
from wechatpy import parse_message
|
||||||
from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent
|
from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent
|
||||||
@@ -87,7 +87,11 @@ class WecomServer:
|
|||||||
logger.info(f"解析成功: {msg}")
|
logger.info(f"解析成功: {msg}")
|
||||||
|
|
||||||
if self.callback:
|
if self.callback:
|
||||||
await self.callback(msg)
|
result_xml = await self.callback(msg)
|
||||||
|
if not result_xml:
|
||||||
|
return "success"
|
||||||
|
if isinstance(result_xml, str):
|
||||||
|
return result_xml
|
||||||
|
|
||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
@@ -117,6 +121,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
|||||||
self.api_base_url = platform_config.get(
|
self.api_base_url = platform_config.get(
|
||||||
"api_base_url", "https://api.weixin.qq.com/cgi-bin/"
|
"api_base_url", "https://api.weixin.qq.com/cgi-bin/"
|
||||||
)
|
)
|
||||||
|
self.active_send_mode = self.config.get("active_send_mode", False)
|
||||||
|
|
||||||
if not self.api_base_url:
|
if not self.api_base_url:
|
||||||
self.api_base_url = "https://api.weixin.qq.com/cgi-bin/"
|
self.api_base_url = "https://api.weixin.qq.com/cgi-bin/"
|
||||||
@@ -138,9 +143,29 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
|||||||
|
|
||||||
self.client.API_BASE_URL = self.api_base_url
|
self.client.API_BASE_URL = self.api_base_url
|
||||||
|
|
||||||
async def callback(msg):
|
# 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重
|
||||||
|
# msgid -> Future
|
||||||
|
self.wexin_event_workers: dict[str, asyncio.Future] = {}
|
||||||
|
|
||||||
|
async def callback(msg: BaseMessage):
|
||||||
try:
|
try:
|
||||||
await self.convert_message(msg)
|
if self.active_send_mode:
|
||||||
|
await self.convert_message(msg, None)
|
||||||
|
else:
|
||||||
|
if msg.id in self.wexin_event_workers:
|
||||||
|
future = self.wexin_event_workers[msg.id]
|
||||||
|
logger.debug(f"duplicate message id checked: {msg.id}")
|
||||||
|
else:
|
||||||
|
future = asyncio.get_event_loop().create_future()
|
||||||
|
self.wexin_event_workers[msg.id] = future
|
||||||
|
await self.convert_message(msg, future)
|
||||||
|
# I love shield so much!
|
||||||
|
result = await asyncio.wait_for(asyncio.shield(future), 60) # wait for 60s
|
||||||
|
logger.debug(f"Got future result: {result}")
|
||||||
|
self.wexin_event_workers.pop(msg.id, None)
|
||||||
|
return result # xml. see weixin_offacc_event.py
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"转换消息时出现异常: {e}")
|
logger.error(f"转换消息时出现异常: {e}")
|
||||||
|
|
||||||
@@ -163,7 +188,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
|||||||
async def run(self):
|
async def run(self):
|
||||||
await self.server.start_polling()
|
await self.server.start_polling()
|
||||||
|
|
||||||
async def convert_message(self, msg) -> AstrBotMessage | None:
|
async def convert_message(
|
||||||
|
self, msg, future: asyncio.Future = None
|
||||||
|
) -> AstrBotMessage | None:
|
||||||
abm = AstrBotMessage()
|
abm = AstrBotMessage()
|
||||||
if isinstance(msg, TextMessage):
|
if isinstance(msg, TextMessage):
|
||||||
abm.message_str = msg.content
|
abm.message_str = msg.content
|
||||||
@@ -177,7 +204,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
|||||||
abm.message_id = msg.id
|
abm.message_id = msg.id
|
||||||
abm.timestamp = msg.time
|
abm.timestamp = msg.time
|
||||||
abm.session_id = abm.sender.user_id
|
abm.session_id = abm.sender.user_id
|
||||||
abm.raw_message = msg
|
|
||||||
elif msg.type == "image":
|
elif msg.type == "image":
|
||||||
assert isinstance(msg, ImageMessage)
|
assert isinstance(msg, ImageMessage)
|
||||||
abm.message_str = "[图片]"
|
abm.message_str = "[图片]"
|
||||||
@@ -191,7 +217,6 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
|||||||
abm.message_id = msg.id
|
abm.message_id = msg.id
|
||||||
abm.timestamp = msg.time
|
abm.timestamp = msg.time
|
||||||
abm.session_id = abm.sender.user_id
|
abm.session_id = abm.sender.user_id
|
||||||
abm.raw_message = msg
|
|
||||||
elif msg.type == "voice":
|
elif msg.type == "voice":
|
||||||
assert isinstance(msg, VoiceMessage)
|
assert isinstance(msg, VoiceMessage)
|
||||||
|
|
||||||
@@ -209,7 +234,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
|||||||
audio = AudioSegment.from_file(path)
|
audio = AudioSegment.from_file(path)
|
||||||
audio.export(path_wav, format="wav")
|
audio.export(path_wav, format="wav")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。")
|
logger.error(
|
||||||
|
f"转换音频失败: {e}。如果没有安装 pydub 和 ffmpeg 请先安装。"
|
||||||
|
)
|
||||||
path_wav = path
|
path_wav = path
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -224,11 +251,16 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
|||||||
abm.message_id = msg.id
|
abm.message_id = msg.id
|
||||||
abm.timestamp = msg.time
|
abm.timestamp = msg.time
|
||||||
abm.session_id = abm.sender.user_id
|
abm.session_id = abm.sender.user_id
|
||||||
abm.raw_message = msg
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"暂未实现的事件: {msg.type}")
|
logger.warning(f"暂未实现的事件: {msg.type}")
|
||||||
|
future.set_result(None)
|
||||||
return
|
return
|
||||||
|
# 很不优雅 :(
|
||||||
|
abm.raw_message = {
|
||||||
|
"message": msg,
|
||||||
|
"future": future,
|
||||||
|
"active_send_mode": self.active_send_mode,
|
||||||
|
}
|
||||||
logger.info(f"abm: {abm}")
|
logger.info(f"abm: {abm}")
|
||||||
await self.handle_msg(abm)
|
await self.handle_msg(abm)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from astrbot.api.event import AstrMessageEvent, MessageChain
|
|||||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||||
from astrbot.api.message_components import Plain, Image, Record
|
from astrbot.api.message_components import Plain, Image, Record
|
||||||
from wechatpy import WeChatClient
|
from wechatpy import WeChatClient
|
||||||
|
from wechatpy.replies import TextReply, ImageReply, VoiceReply
|
||||||
|
|
||||||
|
|
||||||
from astrbot.api import logger
|
from astrbot.api import logger
|
||||||
|
|
||||||
@@ -82,12 +84,23 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
|
|||||||
|
|
||||||
async def send(self, message: MessageChain):
|
async def send(self, message: MessageChain):
|
||||||
message_obj = self.message_obj
|
message_obj = self.message_obj
|
||||||
|
active_send_mode = message_obj.raw_message.get("active_send_mode", False)
|
||||||
for comp in message.chain:
|
for comp in message.chain:
|
||||||
if isinstance(comp, Plain):
|
if isinstance(comp, Plain):
|
||||||
# Split long text messages if needed
|
# Split long text messages if needed
|
||||||
plain_chunks = await self.split_plain(comp.text)
|
plain_chunks = await self.split_plain(comp.text)
|
||||||
for chunk in plain_chunks:
|
for chunk in plain_chunks:
|
||||||
self.client.message.send_text(message_obj.sender.user_id, chunk)
|
if active_send_mode:
|
||||||
|
self.client.message.send_text(message_obj.sender.user_id, chunk)
|
||||||
|
else:
|
||||||
|
reply = TextReply(
|
||||||
|
content=chunk,
|
||||||
|
message=self.message_obj.raw_message["message"],
|
||||||
|
)
|
||||||
|
xml = reply.render()
|
||||||
|
future = self.message_obj.raw_message["future"]
|
||||||
|
assert isinstance(future, asyncio.Future)
|
||||||
|
future.set_result(xml)
|
||||||
await asyncio.sleep(0.5) # Avoid sending too fast
|
await asyncio.sleep(0.5) # Avoid sending too fast
|
||||||
elif isinstance(comp, Image):
|
elif isinstance(comp, Image):
|
||||||
img_path = await comp.convert_to_file_path()
|
img_path = await comp.convert_to_file_path()
|
||||||
@@ -102,10 +115,22 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
logger.debug(f"微信公众平台上传图片返回: {response}")
|
logger.debug(f"微信公众平台上传图片返回: {response}")
|
||||||
self.client.message.send_image(
|
|
||||||
message_obj.sender.user_id,
|
if active_send_mode:
|
||||||
response["media_id"],
|
self.client.message.send_image(
|
||||||
)
|
message_obj.sender.user_id,
|
||||||
|
response["media_id"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
reply = ImageReply(
|
||||||
|
media_id=response["media_id"],
|
||||||
|
message=self.message_obj.raw_message["message"],
|
||||||
|
)
|
||||||
|
xml = reply.render()
|
||||||
|
future = self.message_obj.raw_message["future"]
|
||||||
|
assert isinstance(future, asyncio.Future)
|
||||||
|
future.set_result(xml)
|
||||||
|
|
||||||
elif isinstance(comp, Record):
|
elif isinstance(comp, Record):
|
||||||
record_path = await comp.convert_to_file_path()
|
record_path = await comp.convert_to_file_path()
|
||||||
# 转成amr
|
# 转成amr
|
||||||
@@ -124,10 +149,23 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
logger.info(f"微信公众平台上传语音返回: {response}")
|
logger.info(f"微信公众平台上传语音返回: {response}")
|
||||||
self.client.message.send_voice(
|
|
||||||
message_obj.sender.user_id,
|
|
||||||
response["media_id"],
|
if active_send_mode:
|
||||||
)
|
self.client.message.send_voice(
|
||||||
|
message_obj.sender.user_id,
|
||||||
|
response["media_id"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
reply = VoiceReply(
|
||||||
|
media_id=response["media_id"],
|
||||||
|
message=self.message_obj.raw_message["message"],
|
||||||
|
)
|
||||||
|
xml = reply.render()
|
||||||
|
future = self.message_obj.raw_message["future"]
|
||||||
|
assert isinstance(future, asyncio.Future)
|
||||||
|
future.set_result(xml)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。")
|
logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。")
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class ProviderType(enum.Enum):
|
|||||||
CHAT_COMPLETION = "chat_completion"
|
CHAT_COMPLETION = "chat_completion"
|
||||||
SPEECH_TO_TEXT = "speech_to_text"
|
SPEECH_TO_TEXT = "speech_to_text"
|
||||||
TEXT_TO_SPEECH = "text_to_speech"
|
TEXT_TO_SPEECH = "text_to_speech"
|
||||||
|
EMBEDDING = "embedding"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -155,7 +156,9 @@ class ProviderRequest:
|
|||||||
if self.image_urls:
|
if self.image_urls:
|
||||||
user_content = {
|
user_content = {
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [{"type": "text", "text": self.prompt if self.prompt else "[图片]"}],
|
"content": [
|
||||||
|
{"type": "text", "text": self.prompt if self.prompt else "[图片]"}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
for image_url in self.image_urls:
|
for image_url in self.image_urls:
|
||||||
if image_url.startswith("http"):
|
if image_url.startswith("http"):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import textwrap
|
|||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from typing import Dict, List, Awaitable, Literal, Any
|
from typing import Dict, List, Awaitable, Literal, Any
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -20,6 +21,13 @@ try:
|
|||||||
except (ModuleNotFoundError, ImportError):
|
except (ModuleNotFoundError, ImportError):
|
||||||
logger.warning("警告: 缺少依赖库 'mcp',将无法使用 MCP 服务。")
|
logger.warning("警告: 缺少依赖库 'mcp',将无法使用 MCP 服务。")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from mcp.client.streamable_http import streamablehttp_client
|
||||||
|
except (ModuleNotFoundError, ImportError):
|
||||||
|
logger.warning(
|
||||||
|
"警告: 缺少依赖库 'mcp' 或者 mcp 库版本过低,无法使用 Streamable HTTP 连接方式。"
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_MCP_CONFIG = {"mcpServers": {}}
|
DEFAULT_MCP_CONFIG = {"mcpServers": {}}
|
||||||
|
|
||||||
SUPPORTED_TYPES = [
|
SUPPORTED_TYPES = [
|
||||||
@@ -96,7 +104,10 @@ class MCPClient:
|
|||||||
async def connect_to_server(self, mcp_server_config: dict, name: str):
|
async def connect_to_server(self, mcp_server_config: dict, name: str):
|
||||||
"""连接到 MCP 服务器
|
"""连接到 MCP 服务器
|
||||||
|
|
||||||
如果 `url` 参数存在,则使用 SSE 的方式连接到 MCP 服务。
|
如果 `url` 参数存在:
|
||||||
|
1. 当 transport 指定为 `streamable_http` 时,使用 Streamable HTTP 连接方式。
|
||||||
|
1. 当 transport 指定为 `sse` 时,使用 SSE 连接方式。
|
||||||
|
2. 如果没有指定,默认使用 SSE 的方式连接到 MCP 服务。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mcp_server_config (dict): Configuration for the MCP server. See https://modelcontextprotocol.io/quickstart/server
|
mcp_server_config (dict): Configuration for the MCP server. See https://modelcontextprotocol.io/quickstart/server
|
||||||
@@ -108,15 +119,41 @@ class MCPClient:
|
|||||||
cfg.pop("active", None) # Remove active flag from config
|
cfg.pop("active", None) # Remove active flag from config
|
||||||
|
|
||||||
if "url" in cfg:
|
if "url" in cfg:
|
||||||
# SSE transport method
|
is_sse = True
|
||||||
self._streams_context = sse_client(url=cfg["url"])
|
if cfg.get("transport") == "streamable_http":
|
||||||
streams = await self._streams_context.__aenter__()
|
is_sse = False
|
||||||
|
if is_sse:
|
||||||
|
# SSE transport method
|
||||||
|
self._streams_context = sse_client(
|
||||||
|
url=cfg["url"],
|
||||||
|
headers=cfg.get("headers", {}),
|
||||||
|
timeout=cfg.get("timeout", 5),
|
||||||
|
sse_read_timeout=cfg.get("sse_read_timeout", 60 * 5),
|
||||||
|
)
|
||||||
|
streams = await self._streams_context.__aenter__()
|
||||||
|
|
||||||
# Create a new client session
|
# Create a new client session
|
||||||
# self.session = await self._session_context.__aenter__()
|
self.session = await self.exit_stack.enter_async_context(
|
||||||
self.session = await self.exit_stack.enter_async_context(
|
mcp.ClientSession(*streams)
|
||||||
mcp.ClientSession(*streams)
|
)
|
||||||
)
|
else:
|
||||||
|
timeout = timedelta(seconds=cfg.get("timeout", 30))
|
||||||
|
sse_read_timeout = timedelta(
|
||||||
|
seconds=cfg.get("sse_read_timeout", 60 * 5)
|
||||||
|
)
|
||||||
|
self._streams_context = streamablehttp_client(
|
||||||
|
url=cfg["url"],
|
||||||
|
headers=cfg.get("headers", {}),
|
||||||
|
timeout=timeout,
|
||||||
|
sse_read_timeout=sse_read_timeout,
|
||||||
|
terminate_on_close=cfg.get("terminate_on_close", True),
|
||||||
|
)
|
||||||
|
read_s, write_s, _ = await self._streams_context.__aenter__()
|
||||||
|
|
||||||
|
# Create a new client session
|
||||||
|
self.session = await self.exit_stack.enter_async_context(
|
||||||
|
mcp.ClientSession(read_stream=read_s, write_stream=write_s)
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
server_params = mcp.StdioServerParameters(
|
server_params = mcp.StdioServerParameters(
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ class ProviderManager:
|
|||||||
self.selected_provider_id = sp.get("curr_provider")
|
self.selected_provider_id = sp.get("curr_provider")
|
||||||
self.selected_stt_provider_id = self.provider_stt_settings.get("provider_id")
|
self.selected_stt_provider_id = self.provider_stt_settings.get("provider_id")
|
||||||
self.selected_tts_provider_id = self.provider_settings.get("provider_id")
|
self.selected_tts_provider_id = self.provider_settings.get("provider_id")
|
||||||
self.provider_enabled = self.provider_settings.get("enable", False)
|
# self.provider_enabled = self.provider_settings.get("enable", False)
|
||||||
self.stt_enabled = self.provider_stt_settings.get("enable", False)
|
# self.stt_enabled = self.provider_stt_settings.get("enable", False)
|
||||||
self.tts_enabled = self.provider_tts_settings.get("enable", False)
|
# self.tts_enabled = self.provider_tts_settings.get("enable", False)
|
||||||
|
|
||||||
# 人格情景管理
|
# 人格情景管理
|
||||||
# 目前没有拆成独立的模块
|
# 目前没有拆成独立的模块
|
||||||
@@ -98,9 +98,13 @@ class ProviderManager:
|
|||||||
"""加载的 Speech To Text Provider 的实例"""
|
"""加载的 Speech To Text Provider 的实例"""
|
||||||
self.tts_provider_insts: List[TTSProvider] = []
|
self.tts_provider_insts: List[TTSProvider] = []
|
||||||
"""加载的 Text To Speech Provider 的实例"""
|
"""加载的 Text To Speech Provider 的实例"""
|
||||||
|
self.embedding_provider_insts: List[Provider] = []
|
||||||
|
"""加载的 Embedding Provider 的实例"""
|
||||||
self.inst_map = {}
|
self.inst_map = {}
|
||||||
"""Provider 实例映射. key: provider_id, value: Provider 实例"""
|
"""Provider 实例映射. key: provider_id, value: Provider 实例"""
|
||||||
self.llm_tools = llm_tools
|
self.llm_tools = llm_tools
|
||||||
|
self.default_provider_inst: Provider = None
|
||||||
|
"""默认的 Provider 实例。第 0 个或者用户以前指定的 Provider 实例"""
|
||||||
self.curr_provider_inst: Provider = None
|
self.curr_provider_inst: Provider = None
|
||||||
"""当前使用的 Provider 实例"""
|
"""当前使用的 Provider 实例"""
|
||||||
self.curr_stt_provider_inst: STTProvider = None
|
self.curr_stt_provider_inst: STTProvider = None
|
||||||
@@ -119,14 +123,9 @@ class ProviderManager:
|
|||||||
for provider_config in self.providers_config:
|
for provider_config in self.providers_config:
|
||||||
await self.load_provider(provider_config)
|
await self.load_provider(provider_config)
|
||||||
|
|
||||||
if not self.curr_provider_inst:
|
self.default_provider_inst = self.inst_map.get(self.selected_provider_id)
|
||||||
logger.warning("未启用任何用于 文本生成 的提供商适配器。")
|
if not self.default_provider_inst and self.provider_insts:
|
||||||
|
self.default_provider_inst = self.provider_insts[0]
|
||||||
if self.stt_enabled and not self.curr_stt_provider_inst:
|
|
||||||
logger.warning("未启用任何用于 语音转文本 的提供商适配器。")
|
|
||||||
|
|
||||||
if self.tts_enabled and not self.curr_tts_provider_inst:
|
|
||||||
logger.warning("未启用任何用于 文本转语音 的提供商适配器。")
|
|
||||||
|
|
||||||
# 初始化 MCP Client 连接
|
# 初始化 MCP Client 连接
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
@@ -210,6 +209,14 @@ class ProviderManager:
|
|||||||
from .sources.minimax_tts_api_source import (
|
from .sources.minimax_tts_api_source import (
|
||||||
ProviderMiniMaxTTSAPI as ProviderMiniMaxTTSAPI,
|
ProviderMiniMaxTTSAPI as ProviderMiniMaxTTSAPI,
|
||||||
)
|
)
|
||||||
|
case "volcengine_tts":
|
||||||
|
from .sources.volcengine_tts import (
|
||||||
|
ProviderVolcengineTTS as ProviderVolcengineTTS,
|
||||||
|
)
|
||||||
|
case "openai_embedding":
|
||||||
|
from .sources.openai_embedding_source import (
|
||||||
|
OpenAIEmbeddingProvider as OpenAIEmbeddingProvider,
|
||||||
|
)
|
||||||
except (ImportError, ModuleNotFoundError) as e:
|
except (ImportError, ModuleNotFoundError) as e:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。"
|
f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。"
|
||||||
@@ -241,15 +248,12 @@ class ProviderManager:
|
|||||||
await inst.initialize()
|
await inst.initialize()
|
||||||
|
|
||||||
self.stt_provider_insts.append(inst)
|
self.stt_provider_insts.append(inst)
|
||||||
if (
|
if self.selected_stt_provider_id == provider_config["id"]:
|
||||||
self.selected_stt_provider_id == provider_config["id"]
|
|
||||||
and self.stt_enabled
|
|
||||||
):
|
|
||||||
self.curr_stt_provider_inst = inst
|
self.curr_stt_provider_inst = inst
|
||||||
logger.info(
|
logger.info(
|
||||||
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前语音转文本提供商适配器。"
|
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前语音转文本提供商适配器。"
|
||||||
)
|
)
|
||||||
if not self.curr_stt_provider_inst and self.stt_enabled:
|
if not self.curr_stt_provider_inst:
|
||||||
self.curr_stt_provider_inst = inst
|
self.curr_stt_provider_inst = inst
|
||||||
|
|
||||||
elif provider_metadata.provider_type == ProviderType.TEXT_TO_SPEECH:
|
elif provider_metadata.provider_type == ProviderType.TEXT_TO_SPEECH:
|
||||||
@@ -262,15 +266,12 @@ class ProviderManager:
|
|||||||
await inst.initialize()
|
await inst.initialize()
|
||||||
|
|
||||||
self.tts_provider_insts.append(inst)
|
self.tts_provider_insts.append(inst)
|
||||||
if (
|
if self.selected_tts_provider_id == provider_config["id"]:
|
||||||
self.selected_tts_provider_id == provider_config["id"]
|
|
||||||
and self.tts_enabled
|
|
||||||
):
|
|
||||||
self.curr_tts_provider_inst = inst
|
self.curr_tts_provider_inst = inst
|
||||||
logger.info(
|
logger.info(
|
||||||
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前文本转语音提供商适配器。"
|
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前文本转语音提供商适配器。"
|
||||||
)
|
)
|
||||||
if not self.curr_tts_provider_inst and self.tts_enabled:
|
if not self.curr_tts_provider_inst:
|
||||||
self.curr_tts_provider_inst = inst
|
self.curr_tts_provider_inst = inst
|
||||||
|
|
||||||
elif provider_metadata.provider_type == ProviderType.CHAT_COMPLETION:
|
elif provider_metadata.provider_type == ProviderType.CHAT_COMPLETION:
|
||||||
@@ -287,17 +288,22 @@ class ProviderManager:
|
|||||||
await inst.initialize()
|
await inst.initialize()
|
||||||
|
|
||||||
self.provider_insts.append(inst)
|
self.provider_insts.append(inst)
|
||||||
if (
|
if self.selected_provider_id == provider_config["id"]:
|
||||||
self.selected_provider_id == provider_config["id"]
|
|
||||||
and self.provider_enabled
|
|
||||||
):
|
|
||||||
self.curr_provider_inst = inst
|
self.curr_provider_inst = inst
|
||||||
logger.info(
|
logger.info(
|
||||||
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前提供商适配器。"
|
f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前提供商适配器。"
|
||||||
)
|
)
|
||||||
if not self.curr_provider_inst and self.provider_enabled:
|
if not self.curr_provider_inst:
|
||||||
self.curr_provider_inst = inst
|
self.curr_provider_inst = inst
|
||||||
|
|
||||||
|
elif provider_metadata.provider_type == ProviderType.EMBEDDING:
|
||||||
|
inst = provider_metadata.cls_type(
|
||||||
|
provider_config, self.provider_settings
|
||||||
|
)
|
||||||
|
if getattr(inst, "initialize", None):
|
||||||
|
await inst.initialize()
|
||||||
|
self.embedding_provider_insts.append(inst)
|
||||||
|
|
||||||
self.inst_map[provider_config["id"]] = inst
|
self.inst_map[provider_config["id"]] = inst
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@@ -318,11 +324,7 @@ class ProviderManager:
|
|||||||
|
|
||||||
if len(self.provider_insts) == 0:
|
if len(self.provider_insts) == 0:
|
||||||
self.curr_provider_inst = None
|
self.curr_provider_inst = None
|
||||||
elif (
|
elif self.curr_provider_inst is None and len(self.provider_insts) > 0:
|
||||||
self.curr_provider_inst is None
|
|
||||||
and len(self.provider_insts) > 0
|
|
||||||
and self.provider_enabled
|
|
||||||
):
|
|
||||||
self.curr_provider_inst = self.provider_insts[0]
|
self.curr_provider_inst = self.provider_insts[0]
|
||||||
self.selected_provider_id = self.curr_provider_inst.meta().id
|
self.selected_provider_id = self.curr_provider_inst.meta().id
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -331,11 +333,7 @@ class ProviderManager:
|
|||||||
|
|
||||||
if len(self.stt_provider_insts) == 0:
|
if len(self.stt_provider_insts) == 0:
|
||||||
self.curr_stt_provider_inst = None
|
self.curr_stt_provider_inst = None
|
||||||
elif (
|
elif self.curr_stt_provider_inst is None and len(self.stt_provider_insts) > 0:
|
||||||
self.curr_stt_provider_inst is None
|
|
||||||
and len(self.stt_provider_insts) > 0
|
|
||||||
and self.stt_enabled
|
|
||||||
):
|
|
||||||
self.curr_stt_provider_inst = self.stt_provider_insts[0]
|
self.curr_stt_provider_inst = self.stt_provider_insts[0]
|
||||||
self.selected_stt_provider_id = self.curr_stt_provider_inst.meta().id
|
self.selected_stt_provider_id = self.curr_stt_provider_inst.meta().id
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -344,11 +342,7 @@ class ProviderManager:
|
|||||||
|
|
||||||
if len(self.tts_provider_insts) == 0:
|
if len(self.tts_provider_insts) == 0:
|
||||||
self.curr_tts_provider_inst = None
|
self.curr_tts_provider_inst = None
|
||||||
elif (
|
elif self.curr_tts_provider_inst is None and len(self.tts_provider_insts) > 0:
|
||||||
self.curr_tts_provider_inst is None
|
|
||||||
and len(self.tts_provider_insts) > 0
|
|
||||||
and self.tts_enabled
|
|
||||||
):
|
|
||||||
self.curr_tts_provider_inst = self.tts_provider_insts[0]
|
self.curr_tts_provider_inst = self.tts_provider_insts[0]
|
||||||
self.selected_tts_provider_id = self.curr_tts_provider_inst.meta().id
|
self.selected_tts_provider_id = self.curr_tts_provider_inst.meta().id
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -179,3 +179,25 @@ class TTSProvider(AbstractProvider):
|
|||||||
async def get_audio(self, text: str) -> str:
|
async def get_audio(self, text: str) -> str:
|
||||||
"""获取文本的音频,返回音频文件路径"""
|
"""获取文本的音频,返回音频文件路径"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddingProvider(AbstractProvider):
|
||||||
|
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
|
||||||
|
super().__init__(provider_config)
|
||||||
|
self.provider_config = provider_config
|
||||||
|
self.provider_settings = provider_settings
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_embedding(self, text: str) -> list[float]:
|
||||||
|
"""获取文本的向量"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_embeddings(self, text: list[str]) -> list[list[float]]:
|
||||||
|
"""批量获取文本的向量"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_dim(self) -> int:
|
||||||
|
"""获取向量的维度"""
|
||||||
|
...
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ class OTTSProvider:
|
|||||||
async def _generate_signature(self) -> str:
|
async def _generate_signature(self) -> str:
|
||||||
await self._sync_time()
|
await self._sync_time()
|
||||||
timestamp = int(time.time()) + self.time_offset
|
timestamp = int(time.time()) + self.time_offset
|
||||||
nonce = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=10))
|
nonce = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=10))
|
||||||
path = re.sub(r'^https?://[^/]+', '', self.api_url) or '/'
|
path = re.sub(r"^https?://[^/]+", "", self.api_url) or "/"
|
||||||
return f"{timestamp}-{nonce}-0-{hashlib.md5(f'{path}-{timestamp}-{nonce}-0-{self.skey}'.encode()).hexdigest()}"
|
return f"{timestamp}-{nonce}-0-{hashlib.md5(f'{path}-{timestamp}-{nonce}-0-{self.skey}'.encode()).hexdigest()}"
|
||||||
|
|
||||||
async def get_audio(self, text: str, voice_params: Dict) -> str:
|
async def get_audio(self, text: str, voice_params: Dict) -> str:
|
||||||
@@ -92,7 +92,7 @@ class AzureNativeProvider(TTSProvider):
|
|||||||
def __init__(self, provider_config: dict, provider_settings: dict):
|
def __init__(self, provider_config: dict, provider_settings: dict):
|
||||||
super().__init__(provider_config, provider_settings)
|
super().__init__(provider_config, provider_settings)
|
||||||
self.subscription_key = provider_config.get("azure_tts_subscription_key", "").strip()
|
self.subscription_key = provider_config.get("azure_tts_subscription_key", "").strip()
|
||||||
if not re.fullmatch(r'^[a-zA-Z0-9]{32}$', self.subscription_key):
|
if not re.fullmatch(r"^[a-zA-Z0-9]{32}$", self.subscription_key):
|
||||||
raise ValueError("无效的Azure订阅密钥")
|
raise ValueError("无效的Azure订阅密钥")
|
||||||
self.region = provider_config.get("azure_tts_region", "eastus").strip()
|
self.region = provider_config.get("azure_tts_region", "eastus").strip()
|
||||||
self.endpoint = f"https://{self.region}.tts.speech.microsoft.com/cognitiveservices/v1"
|
self.endpoint = f"https://{self.region}.tts.speech.microsoft.com/cognitiveservices/v1"
|
||||||
@@ -188,7 +188,7 @@ class AzureTTSProvider(TTSProvider):
|
|||||||
raise ValueError(error_msg) from e
|
raise ValueError(error_msg) from e
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise ValueError(f"配置错误: 缺少必要参数 {e}") from e
|
raise ValueError(f"配置错误: 缺少必要参数 {e}") from e
|
||||||
if re.fullmatch(r'^[a-zA-Z0-9]{32}$', key_value):
|
if re.fullmatch(r"^[a-zA-Z0-9]{32}$", key_value):
|
||||||
return AzureNativeProvider(config, self.provider_settings)
|
return AzureNativeProvider(config, self.provider_settings)
|
||||||
raise ValueError("订阅密钥格式无效,应为32位字母数字或other[...]格式")
|
raise ValueError("订阅密钥格式无效,应为32位字母数字或other[...]格式")
|
||||||
|
|
||||||
|
|||||||
@@ -291,19 +291,19 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
result_parts: Optional[types.Part] = result.candidates[0].content.parts
|
result_parts: Optional[types.Part] = result.candidates[0].content.parts
|
||||||
|
|
||||||
if finish_reason == types.FinishReason.SAFETY:
|
if finish_reason == types.FinishReason.SAFETY:
|
||||||
raise Exception("模型生成内容未通过用户定义的内容安全检查")
|
raise Exception("模型生成内容未通过 Gemini 平台的安全检查")
|
||||||
|
|
||||||
if finish_reason in {
|
if finish_reason in {
|
||||||
types.FinishReason.PROHIBITED_CONTENT,
|
types.FinishReason.PROHIBITED_CONTENT,
|
||||||
types.FinishReason.SPII,
|
types.FinishReason.SPII,
|
||||||
types.FinishReason.BLOCKLIST,
|
types.FinishReason.BLOCKLIST,
|
||||||
}:
|
}:
|
||||||
raise Exception("模型生成内容违反Gemini平台政策")
|
raise Exception("模型生成内容违反 Gemini 平台政策")
|
||||||
|
|
||||||
# 防止旧版本SDK不存在IMAGE_SAFETY
|
# 防止旧版本SDK不存在IMAGE_SAFETY
|
||||||
if hasattr(types.FinishReason, "IMAGE_SAFETY"):
|
if hasattr(types.FinishReason, "IMAGE_SAFETY"):
|
||||||
if finish_reason == types.FinishReason.IMAGE_SAFETY:
|
if finish_reason == types.FinishReason.IMAGE_SAFETY:
|
||||||
raise Exception("模型生成内容违反Gemini平台政策")
|
raise Exception("模型生成内容违反 Gemini 平台政策")
|
||||||
|
|
||||||
if not result_parts:
|
if not result_parts:
|
||||||
logger.debug(result.candidates)
|
logger.debug(result.candidates)
|
||||||
|
|||||||
42
astrbot/core/provider/sources/openai_embedding_source.py
Normal file
42
astrbot/core/provider/sources/openai_embedding_source.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from openai import AsyncOpenAI
|
||||||
|
from ..provider import EmbeddingProvider
|
||||||
|
from ..register import register_provider_adapter
|
||||||
|
from ..entities import ProviderType
|
||||||
|
|
||||||
|
|
||||||
|
@register_provider_adapter(
|
||||||
|
"openai_embedding",
|
||||||
|
"OpenAI API Embedding 提供商适配器",
|
||||||
|
provider_type=ProviderType.EMBEDDING,
|
||||||
|
)
|
||||||
|
class OpenAIEmbeddingProvider(EmbeddingProvider):
|
||||||
|
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
|
||||||
|
super().__init__(provider_config, provider_settings)
|
||||||
|
self.provider_config = provider_config
|
||||||
|
self.provider_settings = provider_settings
|
||||||
|
self.client = AsyncOpenAI(
|
||||||
|
api_key=provider_config.get("embedding_api_key"),
|
||||||
|
base_url=provider_config.get(
|
||||||
|
"embedding_api_base", "https://api.openai.com/v1"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.model = provider_config.get("embedding_model", "text-embedding-3-small")
|
||||||
|
self.dimension = provider_config.get("embedding_dimensions", 1536)
|
||||||
|
|
||||||
|
async def get_embedding(self, text: str) -> list[float]:
|
||||||
|
"""
|
||||||
|
获取文本的嵌入
|
||||||
|
"""
|
||||||
|
embedding = await self.client.embeddings.create(input=text, model=self.model)
|
||||||
|
return embedding.data[0].embedding
|
||||||
|
|
||||||
|
async def get_embeddings(self, texts: list[str]) -> list[list[float]]:
|
||||||
|
"""
|
||||||
|
批量获取文本的嵌入
|
||||||
|
"""
|
||||||
|
embeddings = await self.client.embeddings.create(input=texts, model=self.model)
|
||||||
|
return [item.embedding for item in embeddings.data]
|
||||||
|
|
||||||
|
def get_dim(self) -> int:
|
||||||
|
"""获取向量的维度"""
|
||||||
|
return self.dimension
|
||||||
@@ -195,7 +195,11 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
for tool_call in choice.message.tool_calls:
|
for tool_call in choice.message.tool_calls:
|
||||||
for tool in tools.func_list:
|
for tool in tools.func_list:
|
||||||
if tool.name == tool_call.function.name:
|
if tool.name == tool_call.function.name:
|
||||||
args = json.loads(tool_call.function.arguments)
|
# workaround for #1454
|
||||||
|
if isinstance(tool_call.function.arguments, str):
|
||||||
|
args = json.loads(tool_call.function.arguments)
|
||||||
|
else:
|
||||||
|
args = tool_call.function.arguments
|
||||||
args_ls.append(args)
|
args_ls.append(args)
|
||||||
func_name_ls.append(tool_call.function.name)
|
func_name_ls.append(tool_call.function.name)
|
||||||
tool_call_ids.append(tool_call.id)
|
tool_call_ids.append(tool_call.id)
|
||||||
@@ -223,9 +227,9 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
session_id: str = None,
|
session_id: str = None,
|
||||||
image_urls: list[str] = None,
|
image_urls: list[str] = None,
|
||||||
func_tool: FuncCall = None,
|
func_tool: FuncCall = None,
|
||||||
contexts: list=None,
|
contexts: list = None,
|
||||||
system_prompt: str=None,
|
system_prompt: str = None,
|
||||||
tool_calls_result: ToolCallsResult=None,
|
tool_calls_result: ToolCallsResult = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
"""准备聊天所需的有效载荷和上下文"""
|
"""准备聊天所需的有效载荷和上下文"""
|
||||||
@@ -340,9 +344,9 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
async def text_chat(
|
async def text_chat(
|
||||||
self,
|
self,
|
||||||
prompt,
|
prompt,
|
||||||
session_id = None,
|
session_id=None,
|
||||||
image_urls = None,
|
image_urls=None,
|
||||||
func_tool = None,
|
func_tool=None,
|
||||||
contexts=None,
|
contexts=None,
|
||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
|
|||||||
107
astrbot/core/provider/sources/volcengine_tts.py
Normal file
107
astrbot/core/provider/sources/volcengine_tts.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import uuid
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import requests
|
||||||
|
from ..provider import TTSProvider
|
||||||
|
from ..entities import ProviderType
|
||||||
|
from ..register import register_provider_adapter
|
||||||
|
from astrbot import logger
|
||||||
|
|
||||||
|
@register_provider_adapter(
|
||||||
|
"volcengine_tts", "火山引擎 TTS", provider_type=ProviderType.TEXT_TO_SPEECH
|
||||||
|
)
|
||||||
|
class ProviderVolcengineTTS(TTSProvider):
|
||||||
|
def __init__(self, provider_config: dict, provider_settings: dict) -> None:
|
||||||
|
super().__init__(provider_config, provider_settings)
|
||||||
|
self.api_key = provider_config.get("api_key", "")
|
||||||
|
self.appid = provider_config.get("appid", "")
|
||||||
|
self.cluster = provider_config.get("volcengine_cluster", "")
|
||||||
|
self.voice_type = provider_config.get("volcengine_voice_type", "")
|
||||||
|
self.speed_ratio = provider_config.get("volcengine_speed_ratio", 1.0)
|
||||||
|
self.api_base = provider_config.get("api_base", f"https://openspeech.bytedance.com/api/v1/tts")
|
||||||
|
self.timeout = provider_config.get("timeout", 20)
|
||||||
|
|
||||||
|
def _build_request_payload(self, text: str) -> dict:
|
||||||
|
return {
|
||||||
|
"app": {
|
||||||
|
"appid": self.appid,
|
||||||
|
"token": self.api_key,
|
||||||
|
"cluster": self.cluster
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"uid": str(uuid.uuid4())
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"voice_type": self.voice_type,
|
||||||
|
"encoding": "mp3",
|
||||||
|
"speed_ratio": self.speed_ratio,
|
||||||
|
"volume_ratio": 1.0,
|
||||||
|
"pitch_ratio": 1.0,
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"reqid": str(uuid.uuid4()),
|
||||||
|
"text": text,
|
||||||
|
"text_type": "plain",
|
||||||
|
"operation": "query",
|
||||||
|
"with_frontend": 1,
|
||||||
|
"frontend_type": "unitTson"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_audio(self, text: str) -> str:
|
||||||
|
"""异步方法获取语音文件路径"""
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer; {self.api_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = self._build_request_payload(text)
|
||||||
|
|
||||||
|
logger.debug(f"请求头: {headers}")
|
||||||
|
logger.debug(f"请求 URL: {self.api_base}")
|
||||||
|
logger.debug(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
self.api_base,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
timeout=self.timeout
|
||||||
|
) as response:
|
||||||
|
logger.debug(f"响应状态码: {response.status}")
|
||||||
|
|
||||||
|
response_text = await response.text()
|
||||||
|
logger.debug(f"响应内容: {response_text[:200]}...")
|
||||||
|
|
||||||
|
if response.status == 200:
|
||||||
|
resp_data = json.loads(response_text)
|
||||||
|
|
||||||
|
if "data" in resp_data:
|
||||||
|
audio_data = base64.b64decode(resp_data["data"])
|
||||||
|
|
||||||
|
os.makedirs("data/temp", exist_ok=True)
|
||||||
|
|
||||||
|
file_path = f"data/temp/volcengine_tts_{uuid.uuid4()}.mp3"
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: open(file_path, "wb").write(audio_data)
|
||||||
|
)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
else:
|
||||||
|
error_msg = resp_data.get("message", "未知错误")
|
||||||
|
raise Exception(f"火山引擎 TTS API 返回错误: {error_msg}")
|
||||||
|
else:
|
||||||
|
raise Exception(f"火山引擎 TTS API 请求失败: {response.status}, {response_text}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_details = traceback.format_exc()
|
||||||
|
logger.debug(f"火山引擎 TTS 异常详情: {error_details}")
|
||||||
|
raise Exception(f"火山引擎 TTS 异常: {str(e)}")
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from openai import AsyncOpenAI
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleOpenAIEmbedding:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
model,
|
|
||||||
api_key,
|
|
||||||
api_base=None,
|
|
||||||
) -> None:
|
|
||||||
self.client = AsyncOpenAI(api_key=api_key, base_url=api_base)
|
|
||||||
self.model = model
|
|
||||||
|
|
||||||
async def get_embedding(self, text) -> List[float]:
|
|
||||||
"""
|
|
||||||
获取文本的嵌入
|
|
||||||
"""
|
|
||||||
embedding = await self.client.embeddings.create(input=text, model=self.model)
|
|
||||||
return embedding.data[0].embedding
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import os
|
|
||||||
from typing import List, Dict
|
|
||||||
from astrbot.core import logger
|
|
||||||
from .store import Store
|
|
||||||
from astrbot.core.config import AstrBotConfig
|
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeDBManager:
|
|
||||||
def __init__(self, astrbot_config: AstrBotConfig) -> None:
|
|
||||||
self.db_path = os.path.join(get_astrbot_data_path(), "knowledge_db")
|
|
||||||
self.config = astrbot_config.get("knowledge_db", {})
|
|
||||||
self.astrbot_config = astrbot_config
|
|
||||||
if not os.path.exists(self.db_path):
|
|
||||||
os.makedirs(self.db_path)
|
|
||||||
self.store_insts: Dict[str, Store] = {}
|
|
||||||
for name, cfg in self.config.items():
|
|
||||||
if cfg["strategy"] == "embedding":
|
|
||||||
logger.info(f"加载 Chroma Vector Store:{name}")
|
|
||||||
try:
|
|
||||||
from .store.chroma_db import ChromaVectorStore
|
|
||||||
except ImportError as ie:
|
|
||||||
logger.error(f"{ie} 可能未安装 chromadb 库。")
|
|
||||||
continue
|
|
||||||
self.store_insts[name] = ChromaVectorStore(
|
|
||||||
name, cfg["embedding_config"]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.error(f"不支持的策略:{cfg['strategy']}")
|
|
||||||
|
|
||||||
async def list_knowledge_db(self) -> List[str]:
|
|
||||||
return [
|
|
||||||
f
|
|
||||||
for f in os.listdir(self.db_path)
|
|
||||||
if os.path.isfile(os.path.join(self.db_path, f))
|
|
||||||
]
|
|
||||||
|
|
||||||
async def create_knowledge_db(self, name: str, config: Dict):
|
|
||||||
"""
|
|
||||||
config 格式:
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"strategy": "embedding", # 目前只支持 embedding
|
|
||||||
"chunk_method": {
|
|
||||||
"strategy": "fixed",
|
|
||||||
"chunk_size": 100,
|
|
||||||
"overlap_size": 10
|
|
||||||
},
|
|
||||||
"embedding_config": {
|
|
||||||
"strategy": "openai",
|
|
||||||
"base_url": "",
|
|
||||||
"model": "",
|
|
||||||
"api_key": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
if name in self.config:
|
|
||||||
raise ValueError(f"知识库已存在:{name}")
|
|
||||||
|
|
||||||
self.config[name] = config
|
|
||||||
self.astrbot_config["knowledge_db"] = self.config
|
|
||||||
self.astrbot_config.save_config()
|
|
||||||
|
|
||||||
async def insert_record(self, name: str, text: str):
|
|
||||||
if name not in self.store_insts:
|
|
||||||
raise ValueError(f"未找到知识库:{name}")
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
match self.config[name]["chunk_method"]["strategy"]:
|
|
||||||
case "fixed":
|
|
||||||
chunk_size = self.config[name]["chunk_method"]["chunk_size"]
|
|
||||||
chunk_overlap = self.config[name]["chunk_method"]["overlap_size"]
|
|
||||||
ret = self._fixed_chunk(text, chunk_size, chunk_overlap)
|
|
||||||
case _:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for chunk in ret:
|
|
||||||
await self.store_insts[name].save(chunk)
|
|
||||||
|
|
||||||
async def retrive_records(self, name: str, query: str, top_n: int = 3) -> List[str]:
|
|
||||||
if name not in self.store_insts:
|
|
||||||
raise ValueError(f"未找到知识库:{name}")
|
|
||||||
|
|
||||||
inst = self.store_insts[name]
|
|
||||||
return await inst.query(query, top_n)
|
|
||||||
|
|
||||||
def _fixed_chunk(self, text: str, chunk_size: int, chunk_overlap: int) -> List[str]:
|
|
||||||
chunks = []
|
|
||||||
start = 0
|
|
||||||
while start < len(text):
|
|
||||||
end = start + chunk_size
|
|
||||||
chunks.append(text[start:end])
|
|
||||||
start += chunk_size - chunk_overlap
|
|
||||||
return chunks
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
|
|
||||||
class Store:
|
|
||||||
async def save(self, text: str):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def query(self, query: str, top_n: int = 3) -> List[str]:
|
|
||||||
pass
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import chromadb
|
|
||||||
import uuid
|
|
||||||
from typing import List, Dict
|
|
||||||
from astrbot.api import logger
|
|
||||||
from ..embedding.openai_source import SimpleOpenAIEmbedding
|
|
||||||
from . import Store
|
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
||||||
|
|
||||||
|
|
||||||
class ChromaVectorStore(Store):
|
|
||||||
def __init__(self, name: str, embedding_cfg: Dict) -> None:
|
|
||||||
import os
|
|
||||||
self.chroma_client = chromadb.PersistentClient(
|
|
||||||
path=os.path.join(get_astrbot_data_path(), "long_term_memory_chroma.db")
|
|
||||||
)
|
|
||||||
self.collection = self.chroma_client.get_or_create_collection(name=name)
|
|
||||||
self.embedding = None
|
|
||||||
if embedding_cfg["strategy"] == "openai":
|
|
||||||
self.embedding = SimpleOpenAIEmbedding(
|
|
||||||
model=embedding_cfg["model"],
|
|
||||||
api_key=embedding_cfg["api_key"],
|
|
||||||
api_base=embedding_cfg.get("base_url", None),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def save(self, text: str, metadata: Dict = None):
|
|
||||||
logger.debug(f"Saving text: {text}")
|
|
||||||
embedding = await self.embedding.get_embedding(text)
|
|
||||||
|
|
||||||
self.collection.upsert(
|
|
||||||
documents=text,
|
|
||||||
metadatas=metadata,
|
|
||||||
ids=str(uuid.uuid4()),
|
|
||||||
embeddings=embedding,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def query(
|
|
||||||
self, query: str, top_n=3, metadata_filter: Dict = None
|
|
||||||
) -> List[str]:
|
|
||||||
embedding = await self.embedding.get_embedding(query)
|
|
||||||
|
|
||||||
results = self.collection.query(
|
|
||||||
query_embeddings=embedding, n_results=top_n, where=metadata_filter
|
|
||||||
)
|
|
||||||
return results["documents"][0]
|
|
||||||
@@ -16,7 +16,6 @@ from .star_handler import star_handlers_registry, StarHandlerMetadata, EventType
|
|||||||
from .filter.command import CommandFilter
|
from .filter.command import CommandFilter
|
||||||
from .filter.regex import RegexFilter
|
from .filter.regex import RegexFilter
|
||||||
from typing import Awaitable
|
from typing import Awaitable
|
||||||
from astrbot.core.rag.knowledge_db_mgr import KnowledgeDBManager
|
|
||||||
from astrbot.core.conversation_mgr import ConversationManager
|
from astrbot.core.conversation_mgr import ConversationManager
|
||||||
from astrbot.core.star.filter.platform_adapter_type import (
|
from astrbot.core.star.filter.platform_adapter_type import (
|
||||||
PlatformAdapterType,
|
PlatformAdapterType,
|
||||||
@@ -42,6 +41,8 @@ class Context:
|
|||||||
|
|
||||||
platform_manager: PlatformManager = None
|
platform_manager: PlatformManager = None
|
||||||
|
|
||||||
|
registered_web_apis: list = []
|
||||||
|
|
||||||
# back compatibility
|
# back compatibility
|
||||||
_register_tasks: List[Awaitable] = []
|
_register_tasks: List[Awaitable] = []
|
||||||
_star_manager = None
|
_star_manager = None
|
||||||
@@ -54,14 +55,12 @@ class Context:
|
|||||||
provider_manager: ProviderManager = None,
|
provider_manager: ProviderManager = None,
|
||||||
platform_manager: PlatformManager = None,
|
platform_manager: PlatformManager = None,
|
||||||
conversation_manager: ConversationManager = None,
|
conversation_manager: ConversationManager = None,
|
||||||
knowledge_db_manager: KnowledgeDBManager = None,
|
|
||||||
):
|
):
|
||||||
self._event_queue = event_queue
|
self._event_queue = event_queue
|
||||||
self._config = config
|
self._config = config
|
||||||
self._db = db
|
self._db = db
|
||||||
self.provider_manager = provider_manager
|
self.provider_manager = provider_manager
|
||||||
self.platform_manager = platform_manager
|
self.platform_manager = platform_manager
|
||||||
self.knowledge_db_manager = knowledge_db_manager
|
|
||||||
self.conversation_manager = conversation_manager
|
self.conversation_manager = conversation_manager
|
||||||
|
|
||||||
def get_registered_star(self, star_name: str) -> StarMetadata:
|
def get_registered_star(self, star_name: str) -> StarMetadata:
|
||||||
@@ -126,11 +125,8 @@ class Context:
|
|||||||
self.provider_manager.provider_insts.append(provider)
|
self.provider_manager.provider_insts.append(provider)
|
||||||
|
|
||||||
def get_provider_by_id(self, provider_id: str) -> Provider:
|
def get_provider_by_id(self, provider_id: str) -> Provider:
|
||||||
"""通过 ID 获取用于文本生成任务的 LLM Provider(Chat_Completion 类型)。"""
|
"""通过 ID 获取对应的 LLM Provider(Chat_Completion 类型)。"""
|
||||||
for provider in self.provider_manager.provider_insts:
|
return self.provider_manager.inst_map.get(provider_id)
|
||||||
if provider.meta().id == provider_id:
|
|
||||||
return provider
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_providers(self) -> List[Provider]:
|
def get_all_providers(self) -> List[Provider]:
|
||||||
"""获取所有用于文本生成任务的 LLM Provider(Chat_Completion 类型)。"""
|
"""获取所有用于文本生成任务的 LLM Provider(Chat_Completion 类型)。"""
|
||||||
@@ -301,3 +297,12 @@ class Context:
|
|||||||
注册一个异步任务。
|
注册一个异步任务。
|
||||||
"""
|
"""
|
||||||
self._register_tasks.append(task)
|
self._register_tasks.append(task)
|
||||||
|
|
||||||
|
def register_web_api(
|
||||||
|
self, route: str, view_handler: Awaitable, methods: list, desc: str
|
||||||
|
):
|
||||||
|
for idx, api in enumerate(self.registered_web_apis):
|
||||||
|
if api[0] == route and methods == api[2]:
|
||||||
|
self.registered_web_apis[idx] = (route, view_handler, methods, desc)
|
||||||
|
return
|
||||||
|
self.registered_web_apis.append((route, view_handler, methods, desc))
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import enum
|
import enum
|
||||||
import heapq
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Awaitable, List, Dict, TypeVar, Generic
|
from typing import Awaitable, List, Dict, TypeVar, Generic
|
||||||
from .filter import HandlerFilter
|
from .filter import HandlerFilter
|
||||||
@@ -8,100 +7,66 @@ from .star import star_map
|
|||||||
|
|
||||||
T = TypeVar("T", bound="StarHandlerMetadata")
|
T = TypeVar("T", bound="StarHandlerMetadata")
|
||||||
|
|
||||||
|
|
||||||
class StarHandlerRegistry(Generic[T]):
|
class StarHandlerRegistry(Generic[T]):
|
||||||
"""用于存储所有的 Star Handler"""
|
def __init__(self):
|
||||||
|
self.star_handlers_map: Dict[str, StarHandlerMetadata] = {}
|
||||||
star_handlers_map: Dict[str, StarHandlerMetadata] = {}
|
self._handlers: List[StarHandlerMetadata] = []
|
||||||
"""用于快速查找。key 是 handler_full_name"""
|
|
||||||
_handlers = []
|
|
||||||
|
|
||||||
def append(self, handler: StarHandlerMetadata):
|
def append(self, handler: StarHandlerMetadata):
|
||||||
"""添加一个 Handler"""
|
"""添加一个 Handler,并保持按优先级有序"""
|
||||||
if "priority" not in handler.extras_configs:
|
if "priority" not in handler.extras_configs:
|
||||||
handler.extras_configs["priority"] = 0
|
handler.extras_configs["priority"] = 0
|
||||||
|
|
||||||
heapq.heappush(self._handlers, (-handler.extras_configs["priority"], handler))
|
|
||||||
self.star_handlers_map[handler.handler_full_name] = handler
|
self.star_handlers_map[handler.handler_full_name] = handler
|
||||||
|
self._handlers.append(handler)
|
||||||
|
self._handlers.sort(key=lambda h: -h.extras_configs["priority"])
|
||||||
|
|
||||||
def _print_handlers(self):
|
def _print_handlers(self):
|
||||||
"""打印所有的 Handler"""
|
for handler in self._handlers:
|
||||||
for _, handler in self._handlers:
|
|
||||||
print(handler.handler_full_name)
|
print(handler.handler_full_name)
|
||||||
|
|
||||||
def get_handlers_by_event_type(
|
def get_handlers_by_event_type(
|
||||||
self, event_type: EventType, only_activated=True, platform_id=None
|
self, event_type: EventType, only_activated=True, platform_id=None
|
||||||
) -> List[StarHandlerMetadata]:
|
) -> List[StarHandlerMetadata]:
|
||||||
"""通过事件类型获取 Handler
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_type: 事件类型
|
|
||||||
only_activated: 是否只返回已激活的插件的处理器
|
|
||||||
platform_id: 平台ID,如果提供此参数,将过滤掉在此平台不兼容的处理器
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[StarHandlerMetadata]: 处理器列表
|
|
||||||
"""
|
|
||||||
handlers = []
|
handlers = []
|
||||||
for _, handler in self._handlers:
|
for handler in self._handlers:
|
||||||
if handler.event_type != event_type:
|
if handler.event_type != event_type:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 只激活的插件处理器
|
|
||||||
if only_activated:
|
if only_activated:
|
||||||
plugin = star_map.get(handler.handler_module_path)
|
plugin = star_map.get(handler.handler_module_path)
|
||||||
if not (plugin and plugin.activated):
|
if not (plugin and plugin.activated):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 平台兼容性过滤
|
|
||||||
if platform_id and event_type != EventType.OnAstrBotLoadedEvent:
|
if platform_id and event_type != EventType.OnAstrBotLoadedEvent:
|
||||||
if not handler.is_enabled_for_platform(platform_id):
|
if not handler.is_enabled_for_platform(platform_id):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
handlers.append(handler)
|
handlers.append(handler)
|
||||||
|
|
||||||
return handlers
|
return handlers
|
||||||
|
|
||||||
def get_handler_by_full_name(self, full_name: str) -> StarHandlerMetadata:
|
def get_handler_by_full_name(self, full_name: str) -> StarHandlerMetadata:
|
||||||
"""通过 Handler 的全名获取 Handler"""
|
|
||||||
return self.star_handlers_map.get(full_name, None)
|
return self.star_handlers_map.get(full_name, None)
|
||||||
|
|
||||||
def get_handlers_by_module_name(
|
def get_handlers_by_module_name(
|
||||||
self, module_name: str
|
self, module_name: str
|
||||||
) -> List[StarHandlerMetadata]:
|
) -> List[StarHandlerMetadata]:
|
||||||
"""通过模块名获取 Handler"""
|
|
||||||
return [
|
return [
|
||||||
handler
|
handler for handler in self._handlers
|
||||||
for _, handler in self._handlers
|
|
||||||
if handler.handler_module_path == module_name
|
if handler.handler_module_path == module_name
|
||||||
]
|
]
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""清空所有的 Handler"""
|
|
||||||
self.star_handlers_map.clear()
|
self.star_handlers_map.clear()
|
||||||
self._handlers.clear()
|
self._handlers.clear()
|
||||||
|
|
||||||
def remove(self, handler: StarHandlerMetadata):
|
def remove(self, handler: StarHandlerMetadata):
|
||||||
"""删除一个 Handler"""
|
self.star_handlers_map.pop(handler.handler_full_name, None)
|
||||||
# self._handlers.remove(handler)
|
self._handlers = [h for h in self._handlers if h != handler]
|
||||||
for i, h in enumerate(self._handlers):
|
|
||||||
if h[1] == handler:
|
|
||||||
self._handlers.pop(i)
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
del self.star_handlers_map[handler.handler_full_name]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""使 StarHandlerRegistry 支持迭代"""
|
return iter(self._handlers)
|
||||||
return (handler for _, handler in self._handlers)
|
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
"""返回 Handler 的数量"""
|
|
||||||
return len(self._handlers)
|
return len(self._handlers)
|
||||||
|
|
||||||
|
|
||||||
star_handlers_registry = StarHandlerRegistry()
|
star_handlers_registry = StarHandlerRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ except ImportError:
|
|||||||
if os.getenv("ASTRBOT_RELOAD", "0") == "1":
|
if os.getenv("ASTRBOT_RELOAD", "0") == "1":
|
||||||
logger.warning("未安装 watchfiles,无法实现插件的热重载。")
|
logger.warning("未安装 watchfiles,无法实现插件的热重载。")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import nh3
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("未安装 nh3 库,无法清理插件 README.md 中的 HTML 标签。")
|
||||||
|
nh3 = None
|
||||||
|
|
||||||
|
|
||||||
class PluginManager:
|
class PluginManager:
|
||||||
def __init__(self, context: Context, config: AstrBotConfig):
|
def __init__(self, context: Context, config: AstrBotConfig):
|
||||||
@@ -140,11 +146,13 @@ class PluginManager:
|
|||||||
if os.path.exists(os.path.join(path, d, "main.py")) or os.path.exists(
|
if os.path.exists(os.path.join(path, d, "main.py")) or os.path.exists(
|
||||||
os.path.join(path, d, d + ".py")
|
os.path.join(path, d, d + ".py")
|
||||||
):
|
):
|
||||||
modules.append({
|
modules.append(
|
||||||
"pname": d,
|
{
|
||||||
"module": module_str,
|
"pname": d,
|
||||||
"module_path": os.path.join(path, d, module_str),
|
"module": module_str,
|
||||||
})
|
"module_path": os.path.join(path, d, module_str),
|
||||||
|
}
|
||||||
|
)
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
def _get_plugin_modules(self) -> List[dict]:
|
def _get_plugin_modules(self) -> List[dict]:
|
||||||
@@ -158,7 +166,7 @@ class PluginManager:
|
|||||||
plugins.extend(_p)
|
plugins.extend(_p)
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
def _check_plugin_dept_update(self, target_plugin: str = None):
|
async def _check_plugin_dept_update(self, target_plugin: str = None):
|
||||||
"""检查插件的依赖
|
"""检查插件的依赖
|
||||||
如果 target_plugin 为 None,则检查所有插件的依赖
|
如果 target_plugin 为 None,则检查所有插件的依赖
|
||||||
"""
|
"""
|
||||||
@@ -177,7 +185,7 @@ class PluginManager:
|
|||||||
pth = os.path.join(plugin_path, "requirements.txt")
|
pth = os.path.join(plugin_path, "requirements.txt")
|
||||||
logger.info(f"正在安装插件 {p} 所需的依赖库: {pth}")
|
logger.info(f"正在安装插件 {p} 所需的依赖库: {pth}")
|
||||||
try:
|
try:
|
||||||
pip_installer.install(requirements_path=pth)
|
await pip_installer.install(requirements_path=pth)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"更新插件 {p} 的依赖失败。Code: {str(e)}")
|
logger.error(f"更新插件 {p} 的依赖失败。Code: {str(e)}")
|
||||||
|
|
||||||
@@ -399,7 +407,7 @@ class PluginManager:
|
|||||||
module = __import__(path, fromlist=[module_str])
|
module = __import__(path, fromlist=[module_str])
|
||||||
except (ModuleNotFoundError, ImportError):
|
except (ModuleNotFoundError, ImportError):
|
||||||
# 尝试安装依赖
|
# 尝试安装依赖
|
||||||
self._check_plugin_dept_update(target_plugin=root_dir_name)
|
await self._check_plugin_dept_update(target_plugin=root_dir_name)
|
||||||
module = __import__(path, fromlist=[module_str])
|
module = __import__(path, fromlist=[module_str])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@@ -634,16 +642,17 @@ class PluginManager:
|
|||||||
if not os.path.exists(readme_path):
|
if not os.path.exists(readme_path):
|
||||||
readme_path = os.path.join(plugin_path, "readme.md")
|
readme_path = os.path.join(plugin_path, "readme.md")
|
||||||
|
|
||||||
if os.path.exists(readme_path):
|
if os.path.exists(readme_path) and nh3:
|
||||||
try:
|
try:
|
||||||
with open(readme_path, "r", encoding="utf-8") as f:
|
with open(readme_path, "r", encoding="utf-8") as f:
|
||||||
readme_content = f.read()
|
readme_content = f.read()
|
||||||
|
cleaned_content = nh3.clean(readme_content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}")
|
logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}")
|
||||||
|
|
||||||
plugin_info = None
|
plugin_info = None
|
||||||
if plugin:
|
if plugin:
|
||||||
plugin_info = {"repo": plugin.repo, "readme": readme_content}
|
plugin_info = {"repo": plugin.repo, "readme": cleaned_content}
|
||||||
|
|
||||||
return plugin_info
|
return plugin_info
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from pip import main as pip_main
|
import asyncio
|
||||||
|
|
||||||
logger = logging.getLogger("astrbot")
|
logger = logging.getLogger("astrbot")
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ class PipInstaller:
|
|||||||
self.pip_install_arg = pip_install_arg
|
self.pip_install_arg = pip_install_arg
|
||||||
self.pypi_index_url = pypi_index_url
|
self.pypi_index_url = pypi_index_url
|
||||||
|
|
||||||
def install(
|
async def install(
|
||||||
self,
|
self,
|
||||||
package_name: str = None,
|
package_name: str = None,
|
||||||
requirements_path: str = None,
|
requirements_path: str = None,
|
||||||
@@ -29,12 +29,29 @@ class PipInstaller:
|
|||||||
args.extend(self.pip_install_arg.split())
|
args.extend(self.pip_install_arg.split())
|
||||||
|
|
||||||
logger.info(f"Pip 包管理器: pip {' '.join(args)}")
|
logger.info(f"Pip 包管理器: pip {' '.join(args)}")
|
||||||
|
try:
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
"pip", *args,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
|
||||||
result_code = pip_main(args)
|
assert process.stdout is not None
|
||||||
|
async for line in process.stdout:
|
||||||
|
logger.info(line.decode().strip())
|
||||||
|
|
||||||
# 清除 pip.main 导致的多余的 logging handlers
|
await process.wait()
|
||||||
for handler in logging.root.handlers[:]:
|
|
||||||
logging.root.removeHandler(handler)
|
|
||||||
|
|
||||||
if result_code != 0:
|
if process.returncode != 0:
|
||||||
raise Exception(f"安装失败,错误码:{result_code}")
|
raise Exception(f"安装失败,错误码:{process.returncode}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
# 没有 pip
|
||||||
|
from pip import main as pip_main
|
||||||
|
result_code = await asyncio.to_thread(pip_main, args)
|
||||||
|
|
||||||
|
# 清除 pip.main 导致的多余的 logging handlers
|
||||||
|
for handler in logging.root.handlers[:]:
|
||||||
|
logging.root.removeHandler(handler)
|
||||||
|
|
||||||
|
if result_code != 0:
|
||||||
|
raise Exception(f"安装失败,错误码:{result_code}")
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ class AuthRoute(Route):
|
|||||||
post_data = await request.json
|
post_data = await request.json
|
||||||
if post_data["username"] == username and post_data["password"] == password:
|
if post_data["username"] == username and post_data["password"] == password:
|
||||||
change_pwd_hint = False
|
change_pwd_hint = False
|
||||||
if username == "astrbot" and password == "77b90590a8945a7d36c963981a307dc9":
|
if (
|
||||||
|
username == "astrbot"
|
||||||
|
and password == "77b90590a8945a7d36c963981a307dc9"
|
||||||
|
and not DEMO_MODE
|
||||||
|
):
|
||||||
change_pwd_hint = True
|
change_pwd_hint = True
|
||||||
logger.warning("为了保证安全,请尽快修改默认密码。")
|
logger.warning("为了保证安全,请尽快修改默认密码。")
|
||||||
|
|
||||||
|
|||||||
@@ -61,16 +61,25 @@ class ChatRoute(Route):
|
|||||||
return Response().error("Missing key: filename").__dict__
|
return Response().error("Missing key: filename").__dict__
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(self.imgs_dir, filename), "rb") as f:
|
file_path = os.path.join(self.imgs_dir, os.path.basename(filename))
|
||||||
if filename.endswith(".wav"):
|
real_file_path = os.path.realpath(file_path)
|
||||||
|
real_imgs_dir = os.path.realpath(self.imgs_dir)
|
||||||
|
|
||||||
|
if not real_file_path.startswith(real_imgs_dir):
|
||||||
|
return Response().error("Invalid file path").__dict__
|
||||||
|
|
||||||
|
with open(real_file_path, "rb") as f:
|
||||||
|
filename_ext = os.path.splitext(filename)[1].lower()
|
||||||
|
|
||||||
|
if filename_ext == ".wav":
|
||||||
return QuartResponse(f.read(), mimetype="audio/wav")
|
return QuartResponse(f.read(), mimetype="audio/wav")
|
||||||
elif filename.split(".")[-1] in self.supported_imgs:
|
elif filename_ext[1:] in self.supported_imgs:
|
||||||
return QuartResponse(f.read(), mimetype="image/jpeg")
|
return QuartResponse(f.read(), mimetype="image/jpeg")
|
||||||
else:
|
else:
|
||||||
return QuartResponse(f.read())
|
return QuartResponse(f.read())
|
||||||
|
|
||||||
except FileNotFoundError:
|
except (FileNotFoundError, OSError):
|
||||||
return Response().error("File not found").__dict__
|
return Response().error("File access error").__dict__
|
||||||
|
|
||||||
async def post_image(self):
|
async def post_image(self):
|
||||||
post_data = await request.files
|
post_data = await request.files
|
||||||
@@ -126,17 +135,15 @@ class ChatRoute(Route):
|
|||||||
|
|
||||||
self.curr_user_cid[username] = conversation_id
|
self.curr_user_cid[username] = conversation_id
|
||||||
|
|
||||||
await web_chat_queue.put(
|
await web_chat_queue.put((
|
||||||
(
|
username,
|
||||||
username,
|
conversation_id,
|
||||||
conversation_id,
|
{
|
||||||
{
|
"message": message,
|
||||||
"message": message,
|
"image_url": image_url, # list
|
||||||
"image_url": image_url, # list
|
"audio_url": audio_url,
|
||||||
"audio_url": audio_url,
|
},
|
||||||
},
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 持久化
|
# 持久化
|
||||||
conversation = self.db.get_conversation_by_user_id(username, conversation_id)
|
conversation = self.db.get_conversation_by_user_id(username, conversation_id)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from astrbot.core.platform.register import platform_registry
|
|||||||
from astrbot.core.provider.register import provider_registry
|
from astrbot.core.provider.register import provider_registry
|
||||||
from astrbot.core.star.star import star_registry
|
from astrbot.core.star.star import star_registry
|
||||||
from astrbot.core import logger
|
from astrbot.core import logger
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
def try_cast(value: str, type_: str):
|
def try_cast(value: str, type_: str):
|
||||||
@@ -164,9 +165,84 @@ class ConfigRoute(Route):
|
|||||||
"/config/provider/update": ("POST", self.post_update_provider),
|
"/config/provider/update": ("POST", self.post_update_provider),
|
||||||
"/config/provider/delete": ("POST", self.post_delete_provider),
|
"/config/provider/delete": ("POST", self.post_delete_provider),
|
||||||
"/config/llmtools": ("GET", self.get_llm_tools),
|
"/config/llmtools": ("GET", self.get_llm_tools),
|
||||||
|
"/config/provider/check_status": ("GET", self.check_all_providers_status),
|
||||||
|
"/config/provider/list": ("GET", self.get_provider_config_list),
|
||||||
}
|
}
|
||||||
self.register_routes()
|
self.register_routes()
|
||||||
|
|
||||||
|
async def _test_single_provider(self, provider):
|
||||||
|
"""辅助函数:测试单个 provider 的可用性"""
|
||||||
|
meta = provider.meta()
|
||||||
|
provider_name = provider.provider_config.get("id", "Unknown Provider")
|
||||||
|
if not provider_name and meta:
|
||||||
|
provider_name = meta.id
|
||||||
|
elif not provider_name:
|
||||||
|
provider_name = "Unknown Provider"
|
||||||
|
status_info = {
|
||||||
|
"id": meta.id if meta else "Unknown ID",
|
||||||
|
"model": meta.model if meta else "Unknown Model",
|
||||||
|
"type": meta.type if meta else "Unknown Type",
|
||||||
|
"name": provider_name,
|
||||||
|
"status": "unavailable", # 默认为不可用
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
logger.debug(f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})")
|
||||||
|
try:
|
||||||
|
logger.debug(f"Sending 'Ping' to provider: {status_info['name']}")
|
||||||
|
response = await asyncio.wait_for(provider.text_chat(prompt="Ping"), timeout=20.0) # 超时 20 秒
|
||||||
|
logger.debug(f"Received response from {status_info['name']}: {response}")
|
||||||
|
# 只要 text_chat 调用成功返回一个 LLMResponse 对象 (即 response 不为 None),就认为可用
|
||||||
|
if response is not None:
|
||||||
|
status_info["status"] = "available"
|
||||||
|
response_text_snippet = ""
|
||||||
|
if hasattr(response, 'completion_text') and response.completion_text:
|
||||||
|
response_text_snippet = response.completion_text[:70] + "..." if len(response.completion_text) > 70 else response.completion_text
|
||||||
|
elif hasattr(response, 'result_chain') and response.result_chain:
|
||||||
|
try:
|
||||||
|
response_text_snippet = response.result_chain.get_plain_text()[:70] + "..." if len(response.result_chain.get_plain_text()) > 70 else response.result_chain.get_plain_text()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
logger.info(f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'")
|
||||||
|
else:
|
||||||
|
# 这个分支理论上不应该被走到,除非 text_chat 实现可能返回 None
|
||||||
|
status_info["error"] = "Test call returned None, but expected an LLMResponse object."
|
||||||
|
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.")
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
status_info["error"] = "Connection timed out after 10 seconds during test call."
|
||||||
|
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.")
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
status_info["error"] = error_message
|
||||||
|
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}")
|
||||||
|
logger.debug(f"Traceback for {status_info['name']}:\n{traceback.format_exc()}")
|
||||||
|
return status_info
|
||||||
|
|
||||||
|
async def check_all_providers_status(self):
|
||||||
|
"""
|
||||||
|
API 接口: 检查所有 LLM Providers 的状态
|
||||||
|
"""
|
||||||
|
logger.info("API call received: /config/provider/check_status")
|
||||||
|
try:
|
||||||
|
all_providers: typing.List = self.core_lifecycle.star_context.get_all_providers()
|
||||||
|
logger.debug(f"Found {len(all_providers)} providers to check.")
|
||||||
|
|
||||||
|
if not all_providers:
|
||||||
|
logger.info("No providers found to check.")
|
||||||
|
return Response().ok([]).__dict__
|
||||||
|
|
||||||
|
tasks = [self._test_single_provider(p) for p in all_providers]
|
||||||
|
logger.debug(f"Created {len(tasks)} tasks for concurrent provider checks.")
|
||||||
|
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
logger.info(f"Provider status check completed. Results: {results}")
|
||||||
|
|
||||||
|
return Response().ok(results).__dict__
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Critical error in check_all_providers_status: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response().error(f"检查 Provider 状态时发生严重错误: {str(e)}").__dict__
|
||||||
|
|
||||||
async def get_configs(self):
|
async def get_configs(self):
|
||||||
# plugin_name 为空时返回 AstrBot 配置
|
# plugin_name 为空时返回 AstrBot 配置
|
||||||
# 否则返回指定 plugin_name 的插件配置
|
# 否则返回指定 plugin_name 的插件配置
|
||||||
@@ -175,6 +251,17 @@ class ConfigRoute(Route):
|
|||||||
return Response().ok(await self._get_astrbot_config()).__dict__
|
return Response().ok(await self._get_astrbot_config()).__dict__
|
||||||
return Response().ok(await self._get_plugin_config(plugin_name)).__dict__
|
return Response().ok(await self._get_plugin_config(plugin_name)).__dict__
|
||||||
|
|
||||||
|
async def get_provider_config_list(self):
|
||||||
|
provider_type = request.args.get("provider_type", None)
|
||||||
|
if not provider_type:
|
||||||
|
return Response().error("缺少参数 provider_type").__dict__
|
||||||
|
provider_list = []
|
||||||
|
astrbot_config = self.core_lifecycle.astrbot_config
|
||||||
|
for provider in astrbot_config["provider"]:
|
||||||
|
if provider.get("provider_type", None) == provider_type:
|
||||||
|
provider_list.append(provider)
|
||||||
|
return Response().ok(provider_list).__dict__
|
||||||
|
|
||||||
async def post_astrbot_configs(self):
|
async def post_astrbot_configs(self):
|
||||||
post_configs = await request.json
|
post_configs = await request.json
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class LogRoute(Route):
|
|||||||
**message, # see astrbot/core/log.py
|
**message, # see astrbot/core/log.py
|
||||||
}
|
}
|
||||||
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||||||
|
await asyncio.sleep(0.07) # 控制发送频率,避免过快
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ from astrbot.core.star.filter.regex import RegexFilter
|
|||||||
from astrbot.core.star.star_handler import EventType
|
from astrbot.core.star.star_handler import EventType
|
||||||
from astrbot.core import DEMO_MODE
|
from astrbot.core import DEMO_MODE
|
||||||
|
|
||||||
|
try:
|
||||||
|
import nh3
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("未安装 nh3 库,无法清理插件 README.md 中的 HTML 标签。")
|
||||||
|
nh3 = None
|
||||||
|
|
||||||
|
|
||||||
class PluginRoute(Route):
|
class PluginRoute(Route):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -102,7 +108,10 @@ class PluginRoute(Route):
|
|||||||
|
|
||||||
async def get_plugins(self):
|
async def get_plugins(self):
|
||||||
_plugin_resp = []
|
_plugin_resp = []
|
||||||
|
plugin_name = request.args.get("name")
|
||||||
for plugin in self.plugin_manager.context.get_all_stars():
|
for plugin in self.plugin_manager.context.get_all_stars():
|
||||||
|
if plugin_name and plugin.name != plugin_name:
|
||||||
|
continue
|
||||||
_t = {
|
_t = {
|
||||||
"name": plugin.name,
|
"name": plugin.name,
|
||||||
"repo": "" if plugin.repo is None else plugin.repo,
|
"repo": "" if plugin.repo is None else plugin.repo,
|
||||||
@@ -145,9 +154,7 @@ class PluginRoute(Route):
|
|||||||
if handler.event_type == EventType.AdapterMessageEvent:
|
if handler.event_type == EventType.AdapterMessageEvent:
|
||||||
# 处理平台适配器消息事件
|
# 处理平台适配器消息事件
|
||||||
has_admin = False
|
has_admin = False
|
||||||
for (
|
for filter in (
|
||||||
filter
|
|
||||||
) in (
|
|
||||||
handler.event_filters
|
handler.event_filters
|
||||||
): # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高
|
): # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高
|
||||||
if isinstance(filter, CommandFilter):
|
if isinstance(filter, CommandFilter):
|
||||||
@@ -325,6 +332,9 @@ class PluginRoute(Route):
|
|||||||
return Response().error(str(e)).__dict__
|
return Response().error(str(e)).__dict__
|
||||||
|
|
||||||
async def get_plugin_readme(self):
|
async def get_plugin_readme(self):
|
||||||
|
if not nh3:
|
||||||
|
return Response().error("未安装 nh3 库").__dict__
|
||||||
|
|
||||||
plugin_name = request.args.get("name")
|
plugin_name = request.args.get("name")
|
||||||
logger.debug(f"正在获取插件 {plugin_name} 的README文件内容")
|
logger.debug(f"正在获取插件 {plugin_name} 的README文件内容")
|
||||||
|
|
||||||
@@ -360,9 +370,11 @@ class PluginRoute(Route):
|
|||||||
with open(readme_path, "r", encoding="utf-8") as f:
|
with open(readme_path, "r", encoding="utf-8") as f:
|
||||||
readme_content = f.read()
|
readme_content = f.read()
|
||||||
|
|
||||||
|
cleaned_content = nh3.clean(readme_content)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Response()
|
Response()
|
||||||
.ok({"content": readme_content}, "成功获取README内容")
|
.ok({"content": cleaned_content}, "成功获取README内容")
|
||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -383,14 +395,12 @@ class PluginRoute(Route):
|
|||||||
platform_type = platform.get("type", "")
|
platform_type = platform.get("type", "")
|
||||||
platform_id = platform.get("id", "")
|
platform_id = platform.get("id", "")
|
||||||
|
|
||||||
platforms.append(
|
platforms.append({
|
||||||
{
|
"name": platform_id, # 使用type作为name,这是系统内部使用的平台名称
|
||||||
"name": platform_id, # 使用type作为name,这是系统内部使用的平台名称
|
"id": platform_id, # 保留id字段以便前端可以显示
|
||||||
"id": platform_id, # 保留id字段以便前端可以显示
|
"type": platform_type,
|
||||||
"type": platform_type,
|
"display_name": f"{platform_type}({platform_id})",
|
||||||
"display_name": f"{platform_type}({platform_id})",
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
adjusted_platform_enable = {}
|
adjusted_platform_enable = {}
|
||||||
for platform_id, plugins in platform_enable.items():
|
for platform_id, plugins in platform_enable.items():
|
||||||
@@ -399,13 +409,11 @@ class PluginRoute(Route):
|
|||||||
# 获取所有插件,包括系统内部插件
|
# 获取所有插件,包括系统内部插件
|
||||||
plugins = []
|
plugins = []
|
||||||
for plugin in self.plugin_manager.context.get_all_stars():
|
for plugin in self.plugin_manager.context.get_all_stars():
|
||||||
plugins.append(
|
plugins.append({
|
||||||
{
|
"name": plugin.name,
|
||||||
"name": plugin.name,
|
"desc": plugin.desc,
|
||||||
"desc": plugin.desc,
|
"reserved": plugin.reserved, # 添加reserved标志
|
||||||
"reserved": plugin.reserved, # 添加reserved标志
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}"
|
f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}"
|
||||||
@@ -413,13 +421,11 @@ class PluginRoute(Route):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
Response()
|
Response()
|
||||||
.ok(
|
.ok({
|
||||||
{
|
"platforms": platforms,
|
||||||
"platforms": platforms,
|
"plugins": plugins,
|
||||||
"plugins": plugins,
|
"platform_enable": adjusted_platform_enable,
|
||||||
"platform_enable": adjusted_platform_enable,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from quart import request
|
|||||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||||
from astrbot.core.db import BaseDatabase
|
from astrbot.core.db import BaseDatabase
|
||||||
from astrbot.core.config import VERSION
|
from astrbot.core.config import VERSION
|
||||||
|
from astrbot.core.utils.io import get_dashboard_version
|
||||||
from astrbot.core import DEMO_MODE
|
from astrbot.core import DEMO_MODE
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +47,10 @@ class StatRoute(Route):
|
|||||||
return f"{h}小时{m}分{s}秒"
|
return f"{h}小时{m}分{s}秒"
|
||||||
|
|
||||||
async def get_version(self):
|
async def get_version(self):
|
||||||
return Response().ok({"version": VERSION}).__dict__
|
return Response().ok({
|
||||||
|
"version": VERSION,
|
||||||
|
"dashboard_version": await get_dashboard_version(),
|
||||||
|
}).__dict__
|
||||||
|
|
||||||
async def get_start_time(self):
|
async def get_start_time(self):
|
||||||
return Response().ok({"start_time": self.core_lifecycle.start_time}).__dict__
|
return Response().ok({"start_time": self.core_lifecycle.start_time}).__dict__
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class StaticFileRoute(Route):
|
|||||||
"/logs",
|
"/logs",
|
||||||
"/extension",
|
"/extension",
|
||||||
"/dashboard/default",
|
"/dashboard/default",
|
||||||
"/project-atri",
|
"/alkaid",
|
||||||
"/console",
|
"/console",
|
||||||
"/chat",
|
"/chat",
|
||||||
"/settings",
|
"/settings",
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class UpdateRoute(Route):
|
|||||||
# pip 更新依赖
|
# pip 更新依赖
|
||||||
logger.info("更新依赖中...")
|
logger.info("更新依赖中...")
|
||||||
try:
|
try:
|
||||||
pip_installer.install(requirements_path="requirements.txt")
|
await pip_installer.install(requirements_path="requirements.txt")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"更新依赖失败: {e}")
|
logger.error(f"更新依赖失败: {e}")
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ class UpdateRoute(Route):
|
|||||||
if not package:
|
if not package:
|
||||||
return Response().error("缺少参数 package 或不合法。").__dict__
|
return Response().error("缺少参数 package 或不合法。").__dict__
|
||||||
try:
|
try:
|
||||||
pip_installer.install(package, mirror=mirror)
|
await pip_installer.install(package, mirror=mirror)
|
||||||
return Response().ok(None, "安装成功。").__dict__
|
return Response().ok(None, "安装成功。").__dict__
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"/api/update_pip: {traceback.format_exc()}")
|
logger.error(f"/api/update_pip: {traceback.format_exc()}")
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ from astrbot.core.db import BaseDatabase
|
|||||||
from astrbot.core.utils.io import get_local_ip_addresses
|
from astrbot.core.utils.io import get_local_ip_addresses
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
|
APP: Quart = None
|
||||||
|
|
||||||
|
|
||||||
class AstrBotDashboard:
|
class AstrBotDashboard:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -27,6 +29,7 @@ class AstrBotDashboard:
|
|||||||
self.config = core_lifecycle.astrbot_config
|
self.config = core_lifecycle.astrbot_config
|
||||||
self.data_path = os.path.abspath(os.path.join(get_astrbot_data_path(), "dist"))
|
self.data_path = os.path.abspath(os.path.join(get_astrbot_data_path(), "dist"))
|
||||||
self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/")
|
self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/")
|
||||||
|
APP = self.app # noqa
|
||||||
self.app.config["MAX_CONTENT_LENGTH"] = (
|
self.app.config["MAX_CONTENT_LENGTH"] = (
|
||||||
128 * 1024 * 1024
|
128 * 1024 * 1024
|
||||||
) # 将 Flask 允许的最大上传文件体大小设置为 128 MB
|
) # 将 Flask 允许的最大上传文件体大小设置为 128 MB
|
||||||
@@ -51,12 +54,29 @@ class AstrBotDashboard:
|
|||||||
self.conversation_route = ConversationRoute(self.context, db, core_lifecycle)
|
self.conversation_route = ConversationRoute(self.context, db, core_lifecycle)
|
||||||
self.file_route = FileRoute(self.context)
|
self.file_route = FileRoute(self.context)
|
||||||
|
|
||||||
|
self.app.add_url_rule(
|
||||||
|
"/api/plug/<path:subpath>",
|
||||||
|
view_func=self.srv_plug_route,
|
||||||
|
methods=["GET", "POST"],
|
||||||
|
)
|
||||||
|
|
||||||
self.shutdown_event = shutdown_event
|
self.shutdown_event = shutdown_event
|
||||||
|
|
||||||
|
async def srv_plug_route(self, subpath, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
插件路由
|
||||||
|
"""
|
||||||
|
registered_web_apis = self.core_lifecycle.star_context.registered_web_apis
|
||||||
|
for api in registered_web_apis:
|
||||||
|
route, view_handler, methods, _ = api
|
||||||
|
if route == f"/{subpath}" and request.method in methods:
|
||||||
|
return await view_handler(*args, **kwargs)
|
||||||
|
return jsonify(Response().error("未找到该路由").__dict__)
|
||||||
|
|
||||||
async def auth_middleware(self):
|
async def auth_middleware(self):
|
||||||
if not request.path.startswith("/api"):
|
if not request.path.startswith("/api"):
|
||||||
return
|
return
|
||||||
allowed_endpoints = ["/api/auth/login", "/api/chat/get_file", "/api/file"]
|
allowed_endpoints = ["/api/auth/login", "/api/file"]
|
||||||
if any(request.path.startswith(prefix) for prefix in allowed_endpoints):
|
if any(request.path.startswith(prefix) for prefix in allowed_endpoints):
|
||||||
return
|
return
|
||||||
# claim jwt
|
# claim jwt
|
||||||
|
|||||||
7
changelogs/v3.5.11.md
Normal file
7
changelogs/v3.5.11.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# What's Changed
|
||||||
|
|
||||||
|
1. 新增:火山引擎 TTS
|
||||||
|
2. 修复:修复了 WeChatPadPro 在重新登录时为新设备的问题
|
||||||
|
2. ‼️修复:微信公众号(个人认证或者未认证)的情况下能接收但无法回复消息的问题
|
||||||
|
3. 修复:Minimax TTS 相关问题
|
||||||
|
4. 优化:登录界面侧边栏、关于页面样式,修复如果此前已经登录但未自行跳转的问题
|
||||||
18
changelogs/v3.5.12.md
Normal file
18
changelogs/v3.5.12.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# What's Changed
|
||||||
|
|
||||||
|
1. 新增:支持 MCP 的 Streamable HTTP 传输方式。详见 [#1637](https://github.com/Soulter/AstrBot/issues/1637)
|
||||||
|
2. 新增:支持 MCP 的 SSE 传输方式的自定义请求头。详见 [#1659](https://github.com/Soulter/AstrBot/issues/1659)
|
||||||
|
3. 优化:将 /llm 和 /model 和 /provider 指令设置为管理员指令
|
||||||
|
4. 修复:修复插件的 priority 部分失效的问题
|
||||||
|
5. 修复:修复 QQ 下合并转发消息内无法发送文件等问题,尽可能修复了各种文件、语音、视频、图片无法发送的问题
|
||||||
|
6. 优化:Telegram 支持长消息分段发送,优化消息编辑的逻辑
|
||||||
|
7. 优化:WebUI 强制默认修改密码
|
||||||
|
8. 优化:移除了 vpet
|
||||||
|
9. 新增:插件接口:支持动态路由注册
|
||||||
|
10. 优化:CLI 模式下的插件下载
|
||||||
|
11. 新增:WeChatPadPro 对接获取联系人接口
|
||||||
|
12. 新增:T2I、语音、视频支持文件服务
|
||||||
|
13. 优化:硅基流动下某些工具调用返回的 argument 格式适配
|
||||||
|
14. 优化:在使用 /llm 指令关闭后重启 AstrBot 后,模型提供商未被加载
|
||||||
|
15. 新增:新增基于 FAISS + SQLite 的向量存储接口
|
||||||
|
16. 新增:Alkaid Page
|
||||||
9
changelogs/v3.5.13.md
Normal file
9
changelogs/v3.5.13.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# What's Changed
|
||||||
|
|
||||||
|
1. 新增:WebUI 支持暗夜模式。
|
||||||
|
2. 修复:修复 WebUI Chat 接口的未授权访问安全漏洞、插件 README 可能存在的 XSS 注入漏洞。
|
||||||
|
3. 优化:优化 Vec DB 在 indexing 过程时的数据库事务处理。
|
||||||
|
4. 修复:WebUI 下,插件市场的推荐卡片无法点击帮助文档的问题。
|
||||||
|
5. 新增:知识库。
|
||||||
|
6. 新增:WebUI 提供商测试功能,一键检测可用性。
|
||||||
|
7. 新增:WebUI 提供商分类功能,按能力分类提供商。
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"axios-mock-adapter": "^1.22.0",
|
"axios-mock-adapter": "^1.22.0",
|
||||||
"chance": "1.1.11",
|
"chance": "1.1.11",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"js-md5": "^0.8.3",
|
"js-md5": "^0.8.3",
|
||||||
|
|||||||
BIN
dashboard/src/assets/images/astrbot_logo_mini.webp
Normal file
BIN
dashboard/src/assets/images/astrbot_logo_mini.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -340,12 +340,12 @@ export default {
|
|||||||
.config-title {
|
.config-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--v-primary-darken1);
|
color: var(--v-theme-primaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-hint {
|
.config-hint {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: rgba(0, 0, 0, 0.6);
|
color: var(--v-theme-secondaryText);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,12 +400,12 @@ export default {
|
|||||||
.property-name {
|
.property-name {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: var(--v-theme-primaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-hint {
|
.property-hint {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: rgba(0, 0, 0, 0.6);
|
color: var(--v-theme-secondaryText);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useCommonStore } from '@/stores/common';
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- 添加筛选级别控件 -->
|
<!-- 添加筛选级别控件 -->
|
||||||
<div class="filter-controls mb-2">
|
<div class="filter-controls mb-2" v-if="showLevelBtns">
|
||||||
<v-chip-group v-model="selectedLevels" column multiple>
|
<v-chip-group v-model="selectedLevels" column multiple>
|
||||||
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter
|
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter
|
||||||
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'">
|
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'">
|
||||||
@@ -52,6 +52,10 @@ export default {
|
|||||||
historyNum: {
|
historyNum: {
|
||||||
type: String,
|
type: String,
|
||||||
default: -1
|
default: -1
|
||||||
|
},
|
||||||
|
showLevelBtns: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, inject } from 'vue';
|
import { ref, computed, inject } from 'vue';
|
||||||
|
import {useCustomizerStore} from "@/stores/customizer";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
extension: {
|
extension: {
|
||||||
@@ -75,7 +76,9 @@ const viewReadme = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-card class="mx-auto d-flex flex-column" :elevation="highlight ? 0 : 1"
|
<v-card class="mx-auto d-flex flex-column" :elevation="highlight ? 0 : 1"
|
||||||
:style="{ height: $vuetify.display.xs ? '250px' : '220px', backgroundColor: highlight ? '#FAF0DB' : '#ffffff', color: highlight ? '#000' : '#000000' }">
|
:style="{ height: $vuetify.display.xs ? '250px' : '220px',
|
||||||
|
backgroundColor: useCustomizerStore().uiTheme==='PurpleTheme' ? marketMode ? '#f8f0dd' : '#ffffff' : '#282833',
|
||||||
|
color: useCustomizerStore().uiTheme==='PurpleTheme' ? '#000000dd' : '#ffffff'}">
|
||||||
<v-card-text style="padding: 16px; padding-bottom: 0px; display: flex; justify-content: space-between;">
|
<v-card-text style="padding: 16px; padding-bottom: 0px; display: flex; justify-content: space-between;">
|
||||||
|
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
@@ -128,7 +131,7 @@ const viewReadme = () => {
|
|||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-actions style="padding: 0px; margin-top: auto;">
|
<v-card-actions style="margin-left: 0px; gap: 2px;">
|
||||||
<v-btn color="teal-accent-4" text="查看文档" variant="text" @click="viewReadme"></v-btn>
|
<v-btn color="teal-accent-4" text="查看文档" variant="text" @click="viewReadme"></v-btn>
|
||||||
<v-btn v-if="!marketMode" color="teal-accent-4" text="操作" variant="text" @click="reveal = true"></v-btn>
|
<v-btn v-if="!marketMode" color="teal-accent-4" text="操作" variant="text" @click="reveal = true"></v-btn>
|
||||||
<v-btn v-if="marketMode && !extension?.installed" color="teal-accent-4" text="安装" variant="text"
|
<v-btn v-if="marketMode && !extension?.installed" color="teal-accent-4" text="安装" variant="text"
|
||||||
|
|||||||
@@ -104,11 +104,11 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.list-config-item {
|
.list-config-item {
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--v-theme-border);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #ffffff;
|
background-color: var(--v-theme-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-list-item {
|
.v-list-item {
|
||||||
|
|||||||
72
dashboard/src/components/shared/Logo.vue
Normal file
72
dashboard/src/components/shared/Logo.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="logo-container">
|
||||||
|
<div class="logo-content">
|
||||||
|
<div class="logo-image">
|
||||||
|
<img width="110" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
|
||||||
|
</div>
|
||||||
|
<div class="logo-text">
|
||||||
|
<h2 class="text-secondary">AstrBot 仪表盘</h2>
|
||||||
|
<!-- 父子组件传递css变量可能会出错,暂时使用十六进制颜色值 -->
|
||||||
|
<h4 :style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000aa' : '#ffffffcc'}"
|
||||||
|
class="hint-text">登录以继续</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// No props or other logic needed for this simple component
|
||||||
|
import {useCustomizerStore} from "@/stores/customizer";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image img {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image img:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text h4 {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -3,14 +3,25 @@ export type ConfigProps = {
|
|||||||
Customizer_drawer: boolean;
|
Customizer_drawer: boolean;
|
||||||
mini_sidebar: boolean;
|
mini_sidebar: boolean;
|
||||||
fontTheme: string;
|
fontTheme: string;
|
||||||
|
uiTheme: string;
|
||||||
inputBg: boolean;
|
inputBg: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function checkUITheme() {
|
||||||
|
const theme = localStorage.getItem("uiTheme");
|
||||||
|
console.log('memorized theme: ', theme);
|
||||||
|
if (!theme || !(['PurpleTheme', 'PurpleThemeDark'].includes(theme))) {
|
||||||
|
localStorage.setItem("uiTheme", "PurpleTheme");
|
||||||
|
return 'PurpleTheme';
|
||||||
|
} else return theme;
|
||||||
|
}
|
||||||
|
|
||||||
const config: ConfigProps = {
|
const config: ConfigProps = {
|
||||||
Sidebar_drawer: true,
|
Sidebar_drawer: true,
|
||||||
Customizer_drawer: false,
|
Customizer_drawer: false,
|
||||||
mini_sidebar: false,
|
mini_sidebar: false,
|
||||||
fontTheme: 'Roboto',
|
fontTheme: 'Roboto',
|
||||||
|
uiTheme: checkUITheme(),
|
||||||
inputBg: false
|
inputBg: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const customizer = useCustomizerStore();
|
|||||||
<template>
|
<template>
|
||||||
<v-locale-provider>
|
<v-locale-provider>
|
||||||
<v-app
|
<v-app
|
||||||
theme="PurpleTheme"
|
:theme="useCustomizerStore().uiTheme"
|
||||||
:class="[customizer.fontTheme, customizer.mini_sidebar ? 'mini-sidebar' : '', customizer.inputBg ? 'inputWithbg' : '']"
|
:class="[customizer.fontTheme, customizer.mini_sidebar ? 'mini-sidebar' : '', customizer.inputBg ? 'inputWithbg' : '']"
|
||||||
>
|
>
|
||||||
<VerticalHeaderVue />
|
<VerticalHeaderVue />
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import {ref} from 'vue';
|
||||||
import { useCustomizerStore } from '../../../stores/customizer';
|
import {useCustomizerStore} from '@/stores/customizer';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { md5 } from 'js-md5';
|
import {md5} from 'js-md5';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import {useAuthStore} from '@/stores/auth';
|
||||||
import { useCommonStore } from '@/stores/common';
|
import {useCommonStore} from '@/stores/common';
|
||||||
import { marked } from 'marked';
|
import {marked} from 'marked';
|
||||||
|
|
||||||
const customizer = useCustomizerStore();
|
const customizer = useCustomizerStore();
|
||||||
let dialog = ref(false);
|
let dialog = ref(false);
|
||||||
@@ -30,11 +30,11 @@ let installLoading = ref(false);
|
|||||||
let tab = ref(0);
|
let tab = ref(0);
|
||||||
|
|
||||||
let releasesHeader = [
|
let releasesHeader = [
|
||||||
{ title: '标签', key: 'tag_name' },
|
{title: '标签', key: 'tag_name'},
|
||||||
{ title: '发布时间', key: 'published_at' },
|
{title: '发布时间', key: 'published_at'},
|
||||||
{ title: '内容', key: 'body' },
|
{title: '内容', key: 'body'},
|
||||||
{ title: '源码地址', key: 'zipball_url' },
|
{title: '源码地址', key: 'zipball_url'},
|
||||||
{ title: '操作', key: 'switch' }
|
{title: '操作', key: 'switch'}
|
||||||
];
|
];
|
||||||
|
|
||||||
const open = (link: string) => {
|
const open = (link: string) => {
|
||||||
@@ -56,69 +56,78 @@ function accountEdit() {
|
|||||||
new_password: newPassword.value,
|
new_password: newPassword.value,
|
||||||
new_username: newUsername.value
|
new_username: newUsername.value
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.data.status == 'error') {
|
if (res.data.status == 'error') {
|
||||||
|
status.value = res.data.message;
|
||||||
|
password.value = '';
|
||||||
|
newPassword.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialog.value = !dialog.value;
|
||||||
status.value = res.data.message;
|
status.value = res.data.message;
|
||||||
|
setTimeout(() => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
authStore.logout();
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
status.value = err
|
||||||
password.value = '';
|
password.value = '';
|
||||||
newPassword.value = '';
|
newPassword.value = '';
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
dialog.value = !dialog.value;
|
|
||||||
status.value = res.data.message;
|
function getVersion() {
|
||||||
setTimeout(() => {
|
axios.get('/api/stat/version')
|
||||||
const authStore = useAuthStore();
|
.then((res) => {
|
||||||
authStore.logout();
|
botCurrVersion.value = "v" + res.data.data.version;
|
||||||
}, 1000);
|
dashboardCurrentVersion.value = res.data.data?.dashboard_version;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
status.value = err
|
});
|
||||||
password.value = '';
|
|
||||||
newPassword.value = '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkUpdate() {
|
function checkUpdate() {
|
||||||
updateStatus.value = '正在检查更新...';
|
updateStatus.value = '正在检查更新...';
|
||||||
axios.get('/api/update/check')
|
axios.get('/api/update/check')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
hasNewVersion.value = res.data.data.has_new_version;
|
hasNewVersion.value = res.data.data.has_new_version;
|
||||||
|
|
||||||
if (res.data.data.has_new_version) {
|
if (res.data.data.has_new_version) {
|
||||||
releaseMessage.value = res.data.message;
|
releaseMessage.value = res.data.message;
|
||||||
updateStatus.value = '有新版本!';
|
updateStatus.value = '有新版本!';
|
||||||
} else {
|
} else {
|
||||||
updateStatus.value = res.data.message;
|
updateStatus.value = res.data.message;
|
||||||
}
|
}
|
||||||
botCurrVersion.value = res.data.data.version;
|
dashboardHasNewVersion.value = res.data.data.dashboard_has_new_version;
|
||||||
dashboardCurrentVersion.value = res.data.data.dashboard_version;
|
})
|
||||||
dashboardHasNewVersion.value = res.data.data.dashboard_has_new_version;
|
.catch((err) => {
|
||||||
})
|
if (err.response.status == 401) {
|
||||||
.catch((err) => {
|
console.log("401");
|
||||||
if (err.response.status == 401) {
|
const authStore = useAuthStore();
|
||||||
console.log("401");
|
authStore.logout();
|
||||||
const authStore = useAuthStore();
|
return;
|
||||||
authStore.logout();
|
}
|
||||||
return;
|
console.log(err);
|
||||||
}
|
updateStatus.value = err
|
||||||
console.log(err);
|
});
|
||||||
updateStatus.value = err
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReleases() {
|
function getReleases() {
|
||||||
axios.get('/api/update/releases')
|
axios.get('/api/update/releases')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
// releases.value = res.data.data;
|
// releases.value = res.data.data;
|
||||||
// 更新 published_at 的时间为本地时间
|
// 更新 published_at 的时间为本地时间
|
||||||
releases.value = res.data.data.map((item: any) => {
|
releases.value = res.data.data.map((item: any) => {
|
||||||
item.published_at = new Date(item.published_at).toLocaleString();
|
item.published_at = new Date(item.published_at).toLocaleString();
|
||||||
return item;
|
return item;
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
.catch((err) => {
|
console.log(err);
|
||||||
console.log(err);
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDevCommits() {
|
function getDevCommits() {
|
||||||
@@ -128,17 +137,17 @@ function getDevCommits() {
|
|||||||
'Referer': 'https://api.github.com'
|
'Referer': 'https://api.github.com'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
devCommits.value = data.map((commit: any) => ({
|
devCommits.value = data.map((commit: any) => ({
|
||||||
sha: commit.sha,
|
sha: commit.sha,
|
||||||
date: new Date(commit.commit.author.date).toLocaleString(),
|
date: new Date(commit.commit.author.date).toLocaleString(),
|
||||||
message: commit.commit.message
|
message: commit.commit.message
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchVersion(version: string) {
|
function switchVersion(version: string) {
|
||||||
@@ -148,39 +157,44 @@ function switchVersion(version: string) {
|
|||||||
version: version,
|
version: version,
|
||||||
proxy: localStorage.getItem('selectedGitHubProxy') || ''
|
proxy: localStorage.getItem('selectedGitHubProxy') || ''
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateStatus.value = res.data.message;
|
updateStatus.value = res.data.message;
|
||||||
if (res.data.status == 'ok') {
|
if (res.data.status == 'ok') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
updateStatus.value = err
|
updateStatus.value = err
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
installLoading.value = false;
|
installLoading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDashboard() {
|
function updateDashboard() {
|
||||||
updateStatus.value = '正在更新...';
|
updateStatus.value = '正在更新...';
|
||||||
axios.post('/api/update/dashboard')
|
axios.post('/api/update/dashboard')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateStatus.value = res.data.message;
|
updateStatus.value = res.data.message;
|
||||||
if (res.data.status == 'ok') {
|
if (res.data.status == 'ok') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
updateStatus.value = err
|
updateStatus.value = err
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleDarkMode() {
|
||||||
|
customizer.SET_UI_THEME(customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark');
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersion();
|
||||||
checkUpdate();
|
checkUpdate();
|
||||||
|
|
||||||
const commonStore = useCommonStore();
|
const commonStore = useCommonStore();
|
||||||
@@ -199,32 +213,53 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
<template>
|
<template>
|
||||||
<v-app-bar elevation="0" height="55">
|
<v-app-bar elevation="0" height="55">
|
||||||
|
|
||||||
<v-btn style="margin-left: 22px;" class="hidden-md-and-down text-secondary" color="lightsecondary" icon rounded="sm"
|
<v-btn v-if="useCustomizerStore().uiTheme==='PurpleTheme'" style="margin-left: 22px;" class="hidden-md-and-down text-secondary" color="lightsecondary" icon rounded="sm"
|
||||||
variant="flat" @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
variant="flat" @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||||
<v-icon>mdi-menu</v-icon>
|
<v-icon>mdi-menu</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn class="hidden-lg-and-up text-secondary ms-3" color="lightsecondary" icon rounded="sm" variant="flat"
|
<v-btn v-else style="margin-left: 22px; color: var(--v-theme-primaryText); background-color: var(--v-theme-secondary)" class="hidden-md-and-down" icon rounded="sm"
|
||||||
@click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
variant="flat" @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
|
||||||
|
<v-icon>mdi-menu</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="hidden-lg-and-up text-secondary ms-3" color="lightsecondary" icon rounded="sm" variant="flat"
|
||||||
|
@click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||||
|
<v-icon>mdi-menu</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-else class="hidden-lg-and-up ms-3" icon rounded="sm" variant="flat"
|
||||||
|
@click.stop="customizer.SET_SIDEBAR_DRAWER" size="small">
|
||||||
<v-icon>mdi-menu</v-icon>
|
<v-icon>mdi-menu</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<span style="margin-left: 16px; font-size: 24px; font-weight: 1000;">Astr<span
|
<div style="margin-left: 16px; display: flex; align-items: center; gap: 8px;">
|
||||||
style="font-weight: normal;">Bot</span></span>
|
<span style=" font-size: 24px; font-weight: 1000;">Astr<span style="font-weight: normal;">Bot</span>
|
||||||
|
</span>
|
||||||
|
<span style="font-size: 12px; color: var(--v-theme-secondaryText);">{{ botCurrVersion }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<v-spacer />
|
<v-spacer/>
|
||||||
|
|
||||||
<div class="mr-4">
|
<div class="mr-4">
|
||||||
<small v-if="hasNewVersion">
|
<small v-if="hasNewVersion">
|
||||||
有新版本!
|
AstrBot 有新版本!
|
||||||
|
</small>
|
||||||
|
<small v-else-if="dashboardHasNewVersion">
|
||||||
|
WebUI 有新版本!
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<v-btn size="small" @click="toggleDarkMode();" class="text-primary mr-2" color="var(--v-theme-surface)"
|
||||||
|
variant="flat" rounded="sm">
|
||||||
|
<!-- 明暗主题切换按钮 -->
|
||||||
|
<v-icon v-if="useCustomizerStore().uiTheme === 'PurpleThemeDark'">mdi-weather-night</v-icon>
|
||||||
|
<v-icon v-else>mdi-white-balance-sunny</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<v-dialog v-model="updateStatusDialog" width="1000">
|
<v-dialog v-model="updateStatusDialog" width="1000">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn @click="checkUpdate(); getReleases(); getDevCommits();" class="text-primary mr-4" color="lightprimary"
|
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="text-primary mr-2"
|
||||||
variant="flat" rounded="sm" v-bind="props">
|
color="var(--v-theme-surface)"
|
||||||
更新 🔄
|
variant="flat" rounded="sm" v-bind="props">
|
||||||
|
更新
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-card>
|
<v-card>
|
||||||
@@ -241,15 +276,16 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;"
|
style="background-color: #646cff24; padding: 16px; border-radius: 10px; font-size: 14px; max-height: 400px; overflow-y: auto;"
|
||||||
v-html="marked(releaseMessage)" class="markdown-content">
|
v-html="marked(releaseMessage)" class="markdown-content">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 mt-4">
|
<div class="mb-4 mt-4">
|
||||||
<small>💡 TIP: 跳到旧版本或者切换到某个版本不会重新下载管理面板文件,这可能会造成部分数据显示错误。您可在 <a
|
<small>💡 TIP: 跳到旧版本或者切换到某个版本不会重新下载管理面板文件,这可能会造成部分数据显示错误。您可在 <a
|
||||||
href="https://github.com/Soulter/AstrBot/releases">此处</a>
|
href="https://github.com/Soulter/AstrBot/releases">此处</a>
|
||||||
找到对应的面板文件 dist.zip,解压后替换 data/dist 文件夹即可。当然,前端源代码在 dashboard 目录下,你也可以自己使用 npm install 和 npm build
|
找到对应的面板文件 dist.zip,解压后替换 data/dist 文件夹即可。当然,前端源代码在 dashboard 目录下,你也可以自己使用
|
||||||
|
npm install 和 npm build
|
||||||
构建。</small>
|
构建。</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -262,12 +298,13 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
<!-- 发行版 -->
|
<!-- 发行版 -->
|
||||||
<v-tabs-window-item key="0" v-show="tab == 0">
|
<v-tabs-window-item key="0" v-show="tab == 0">
|
||||||
<v-btn class="mt-4 mb-4" @click="switchVersion('latest')" color="primary" style="border-radius: 10px;"
|
<v-btn class="mt-4 mb-4" @click="switchVersion('latest')" color="primary" style="border-radius: 10px;"
|
||||||
:disabled="!hasNewVersion">
|
:disabled="!hasNewVersion">
|
||||||
更新到最新版本
|
更新到最新版本
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<small>`更新到最新版本` 按钮会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用 <a
|
<small>`更新到最新版本` 按钮会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker
|
||||||
href="https://containrrr.dev/watchtower/usage-overview/">watchtower</a> 来自动监控拉取。</small>
|
部署,也可以重新拉取镜像或者使用 <a
|
||||||
|
href="https://containrrr.dev/watchtower/usage-overview/">watchtower</a> 来自动监控拉取。</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-data-table :headers="releasesHeader" :items="releases" item-key="name">
|
<v-data-table :headers="releasesHeader" :items="releases" item-key="name">
|
||||||
@@ -290,8 +327,8 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
<v-tabs-window-item key="1" v-show="tab == 1">
|
<v-tabs-window-item key="1" v-show="tab == 1">
|
||||||
<div style="margin-top: 16px;">
|
<div style="margin-top: 16px;">
|
||||||
<v-data-table
|
<v-data-table
|
||||||
:headers="[{ title: 'SHA', key: 'sha' }, { title: '日期', key: 'date' }, { title: '信息', key: 'message' }, { title: '操作', key: 'switch' }]"
|
:headers="[{ title: 'SHA', key: 'sha' }, { title: '日期', key: 'date' }, { title: '信息', key: 'message' }, { title: '操作', key: 'switch' }]"
|
||||||
:items="devCommits" item-key="sha">
|
:items="devCommits" item-key="sha">
|
||||||
<template v-slot:item.switch="{ item }: { item: { sha: string } }">
|
<template v-slot:item.switch="{ item }: { item: { sha: string } }">
|
||||||
<v-btn @click="switchVersion(item.sha)" rounded="xl" variant="plain" color="primary">
|
<v-btn @click="switchVersion(item.sha)" rounded="xl" variant="plain" color="primary">
|
||||||
切换
|
切换
|
||||||
@@ -306,12 +343,13 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
<h3 class="mb-4">手动输入版本号或 Commit SHA</h3>
|
<h3 class="mb-4">手动输入版本号或 Commit SHA</h3>
|
||||||
|
|
||||||
<v-text-field label="输入版本号或 master 分支下的 commit hash。" v-model="version" required
|
<v-text-field label="输入版本号或 master 分支下的 commit hash。" v-model="version" required
|
||||||
variant="outlined"></v-text-field>
|
variant="outlined"></v-text-field>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<small>如 v3.3.16 (不带 SHA) 或 42e5ec5d80b93b6bfe8b566754d45ffac4c3fe0b</small>
|
<small>如 v3.3.16 (不带 SHA) 或 42e5ec5d80b93b6bfe8b566754d45ffac4c3fe0b</small>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://github.com/Soulter/AstrBot/commits/master"><small>查看 master 分支提交记录(点击右边的 copy
|
<a href="https://github.com/Soulter/AstrBot/commits/master"><small>查看 master 分支提交记录(点击右边的
|
||||||
即可复制)</small></a>
|
copy
|
||||||
|
即可复制)</small></a>
|
||||||
</div>
|
</div>
|
||||||
<v-btn color="error" style="border-radius: 10px;" @click="switchVersion(version)">
|
<v-btn color="error" style="border-radius: 10px;" @click="switchVersion(version)">
|
||||||
确定切换
|
确定切换
|
||||||
@@ -336,7 +374,7 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-btn color="primary" style="border-radius: 10px;" @click="updateDashboard()"
|
<v-btn color="primary" style="border-radius: 10px;" @click="updateDashboard()"
|
||||||
:disabled="!dashboardHasNewVersion">
|
:disabled="!dashboardHasNewVersion">
|
||||||
下载并更新
|
下载并更新
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -353,8 +391,8 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
|
|
||||||
<v-dialog v-model="dialog" persistent width="700">
|
<v-dialog v-model="dialog" persistent width="700">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn class="text-primary mr-4" color="lightprimary" variant="flat" rounded="sm" v-bind="props">
|
<v-btn size="small" class="text-primary mr-4" color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
|
||||||
账户 📰
|
账户
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-card>
|
<v-card>
|
||||||
@@ -367,16 +405,16 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
|
|
||||||
<v-alert v-if="accountWarning" color="warning" style="margin-bottom: 16px;">
|
<v-alert v-if="accountWarning" color="warning" style="margin-bottom: 16px;">
|
||||||
<div>为了安全,请尽快修改默认密码。</div>
|
<div>为了安全,请务必修改默认密码。</div>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<v-text-field label="原密码*" type="password" v-model="password" required
|
<v-text-field label="原密码*" type="password" v-model="password" required
|
||||||
variant="outlined"></v-text-field>
|
variant="outlined"></v-text-field>
|
||||||
|
|
||||||
<v-text-field label="新用户名" v-model="newUsername" required variant="outlined"></v-text-field>
|
<v-text-field label="新用户名" v-model="newUsername" required variant="outlined"></v-text-field>
|
||||||
|
|
||||||
<v-text-field label="新密码" type="password" v-model="newPassword" required
|
<v-text-field label="新密码" type="password" v-model="newPassword" required
|
||||||
variant="outlined"></v-text-field>
|
variant="outlined"></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
@@ -386,7 +424,7 @@ if (localStorage.getItem('change_pwd_hint') != null && localStorage.getItem('cha
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="blue-darken-1" variant="text" @click="dialog = false">
|
<v-btn v-if="!accountWarning" color="blue-darken-1" variant="text" @click="dialog = false">
|
||||||
关闭
|
关闭
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn color="blue-darken-1" variant="text" @click="accountEdit">
|
<v-btn color="blue-darken-1" variant="text" @click="accountEdit">
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ const customizer = useCustomizerStore();
|
|||||||
const sidebarMenu = shallowRef(sidebarItems);
|
const sidebarMenu = shallowRef(sidebarItems);
|
||||||
|
|
||||||
const showIframe = ref(false);
|
const showIframe = ref(false);
|
||||||
const version = ref("");
|
|
||||||
const buildVer = ref("");
|
|
||||||
const hasWebUIUpdate = ref(false);
|
|
||||||
|
|
||||||
// 默认桌面端 iframe 样式
|
// 默认桌面端 iframe 样式
|
||||||
const iframeStyle = ref({
|
const iframeStyle = ref({
|
||||||
@@ -68,9 +65,10 @@ function toggleIframe() {
|
|||||||
showIframe.value = !showIframe.value;
|
showIframe.value = !showIframe.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openIframeLink() {
|
function openIframeLink(url) {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.open("https://astrbot.app", "_blank");
|
let url_ = url || "https://astrbot.app";
|
||||||
|
window.open(url_, "_blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,25 +147,6 @@ function endDrag() {
|
|||||||
document.removeEventListener('touchend', onTouchEnd);
|
document.removeEventListener('touchend', onTouchEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取版本和更新信息
|
|
||||||
onMounted(() => {
|
|
||||||
axios.get('/api/stat/version')
|
|
||||||
.then((res) => {
|
|
||||||
version.value = "v" + res.data.data.version;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
axios.get('/api/update/check?type=dashboard')
|
|
||||||
.then((res) => {
|
|
||||||
hasWebUIUpdate.value = res.data.data.has_new_version;
|
|
||||||
buildVer.value = res.data.data.current_version;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -186,27 +165,23 @@ onMounted(() => {
|
|||||||
<NavItem :item="item" class="leftPadding" />
|
<NavItem :item="item" class="leftPadding" />
|
||||||
</template>
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
<div class="text-center">
|
<div style="position: absolute; bottom: 16px; width: 100%; font-size: 13px;" class="text-center">
|
||||||
<v-chip color="inputBorder" size="small"> {{ version }} </v-chip>
|
<v-btn style="margin-bottom: 8px;" size="small" variant="primary" v-if="!customizer.mini_sidebar" to="/settings">
|
||||||
</div>
|
🔧 设置
|
||||||
<div style="position: absolute; bottom: 32px; width: 100%; font-size: 13px;" class="text-center">
|
</v-btn>
|
||||||
<v-list-item v-if="!customizer.mini_sidebar" @click="toggleIframe">
|
<br/>
|
||||||
<v-btn variant="plain" size="small">
|
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" v-if="!customizer.mini_sidebar" @click="toggleIframe">
|
||||||
🤔 点击此处 查看/关闭 悬浮文档!
|
官方文档
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-list-item>
|
<br/>
|
||||||
<small style="display: block;" v-if="buildVer">WebUI 版本: {{ buildVer }}</small>
|
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" v-if="!customizer.mini_sidebar" @click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||||
<small style="display: block;" v-else>构建: embedded</small>
|
GitHub
|
||||||
<v-tooltip text="使用 /dashboard_update 指令更新管理面板">
|
</v-btn>
|
||||||
<template v-slot:activator="{ props }">
|
<br/>
|
||||||
<small v-bind="props" v-if="hasWebUIUpdate" style="display: block; margin-top: 4px;">面板有更新</small>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
<small style="display: block; margin-top: 8px;">AGPL-3.0</small>
|
|
||||||
</div>
|
</div>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<!-- 优化后的悬浮 iframe -->
|
|
||||||
<div
|
<div
|
||||||
v-if="showIframe"
|
v-if="showIframe"
|
||||||
id="draggable-iframe"
|
id="draggable-iframe"
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ const sidebarItem: menu[] = [
|
|||||||
to: '/console'
|
to: '/console'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '设置',
|
title: 'Alkaid',
|
||||||
icon: 'mdi-wrench',
|
icon: 'mdi-test-tube',
|
||||||
to: '/settings'
|
to: '/alkaid'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '关于',
|
title: '关于',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import '@mdi/font/css/materialdesignicons.css';
|
|||||||
import * as components from 'vuetify/components';
|
import * as components from 'vuetify/components';
|
||||||
import * as directives from 'vuetify/directives';
|
import * as directives from 'vuetify/directives';
|
||||||
import { PurpleTheme } from '@/theme/LightTheme';
|
import { PurpleTheme } from '@/theme/LightTheme';
|
||||||
|
import { PurpleThemeDark } from "@/theme/DarkTheme";
|
||||||
|
|
||||||
export default createVuetify({
|
export default createVuetify({
|
||||||
components,
|
components,
|
||||||
@@ -11,7 +12,8 @@ export default createVuetify({
|
|||||||
theme: {
|
theme: {
|
||||||
defaultTheme: 'PurpleTheme',
|
defaultTheme: 'PurpleTheme',
|
||||||
themes: {
|
themes: {
|
||||||
PurpleTheme
|
PurpleTheme,
|
||||||
|
PurpleThemeDark
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
|
|||||||
@@ -57,9 +57,26 @@ const MainRoutes = {
|
|||||||
component: () => import('@/views/ConsolePage.vue')
|
component: () => import('@/views/ConsolePage.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Project ATRI',
|
name: 'Alkaid',
|
||||||
path: '/project-atri',
|
path: '/alkaid',
|
||||||
component: () => import('@/views/ATRIProject.vue')
|
component: () => import('@/views/AlkaidPage.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'knowledge-base',
|
||||||
|
name: 'KnowledgeBase',
|
||||||
|
component: () => import('@/views/alkaid/KnowledgeBase.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'long-term-memory',
|
||||||
|
name: 'LongTermMemory',
|
||||||
|
component: () => import('@/views/alkaid/LongTermMemory.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'other',
|
||||||
|
name: 'OtherFeatures',
|
||||||
|
component: () => import('@/views/alkaid/Other.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chat',
|
name: 'Chat',
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
const authRequired = !publicPages.includes(to.path);
|
const authRequired = !publicPages.includes(to.path);
|
||||||
const auth: AuthStore = useAuthStore();
|
const auth: AuthStore = useAuthStore();
|
||||||
|
|
||||||
|
// 如果用户已登录且试图访问登录页面,则重定向到首页或之前尝试访问的页面
|
||||||
|
if (to.path === '/auth/login' && auth.has_token()) {
|
||||||
|
return next(auth.returnUrl || '/');
|
||||||
|
}
|
||||||
|
|
||||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||||
if (authRequired && !auth.has_token()) {
|
if (authRequired && !auth.has_token()) {
|
||||||
auth.returnUrl = to.fullPath;
|
auth.returnUrl = to.fullPath;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
.listitem {
|
.listitem {
|
||||||
height: calc(100vh - 100px);
|
height: calc(100vh - 100px);
|
||||||
.v-list {
|
.v-list {
|
||||||
color: rgb(var(--v-theme-lightText));
|
color: rgb(var(--v-theme-secondaryText));
|
||||||
}
|
}
|
||||||
.v-list-group__items .v-list-item,
|
.v-list-group__items .v-list-item,
|
||||||
.v-list-item {
|
.v-list-item {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const useCustomizerStore = defineStore({
|
|||||||
Customizer_drawer: config.Customizer_drawer,
|
Customizer_drawer: config.Customizer_drawer,
|
||||||
mini_sidebar: config.mini_sidebar,
|
mini_sidebar: config.mini_sidebar,
|
||||||
fontTheme: "Poppins",
|
fontTheme: "Poppins",
|
||||||
|
uiTheme: config.uiTheme,
|
||||||
inputBg: config.inputBg
|
inputBg: config.inputBg
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -21,6 +22,10 @@ export const useCustomizerStore = defineStore({
|
|||||||
},
|
},
|
||||||
SET_FONT(payload: string) {
|
SET_FONT(payload: string) {
|
||||||
this.fontTheme = payload;
|
this.fontTheme = payload;
|
||||||
}
|
},
|
||||||
|
SET_UI_THEME(payload: string) {
|
||||||
|
this.uiTheme = payload;
|
||||||
|
localStorage.setItem("uiTheme", payload);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
46
dashboard/src/theme/DarkTheme.ts
Normal file
46
dashboard/src/theme/DarkTheme.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { ThemeTypes } from '@/types/themeTypes/ThemeType';
|
||||||
|
|
||||||
|
const PurpleThemeDark: ThemeTypes = {
|
||||||
|
name: 'PurpleThemeDark',
|
||||||
|
dark: true,
|
||||||
|
variables: {
|
||||||
|
'border-color': '#1677ff',
|
||||||
|
'carousel-control-size': 10
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#1677ff',
|
||||||
|
secondary: '#722ed1',
|
||||||
|
info: '#03c9d7',
|
||||||
|
success: '#52c41a',
|
||||||
|
accent: '#FFAB91',
|
||||||
|
warning: '#faad14',
|
||||||
|
error: '#ff4d4f',
|
||||||
|
lightprimary: '#eef2f6',
|
||||||
|
lightsecondary: '#ede7f6',
|
||||||
|
lightsuccess: '#b9f6ca',
|
||||||
|
lighterror: '#f9d8d8',
|
||||||
|
lightwarning: '#fff8e1',
|
||||||
|
primaryText: '#ffffff',
|
||||||
|
secondaryText: '#ffffffcc',
|
||||||
|
darkprimary: '#1565c0',
|
||||||
|
darksecondary: '#4527a0',
|
||||||
|
borderLight: '#d0d0d0',
|
||||||
|
border: '#333333ee',
|
||||||
|
inputBorder: '#787878',
|
||||||
|
containerBg: '#1a1a1a',
|
||||||
|
surface: '#1f1f1f',
|
||||||
|
'on-surface-variant': '#000',
|
||||||
|
facebook: '#4267b2',
|
||||||
|
twitter: '#1da1f2',
|
||||||
|
linkedin: '#0e76a8',
|
||||||
|
gray100: '#cccccccc',
|
||||||
|
primary200: '#90caf9',
|
||||||
|
secondary200: '#b39ddb',
|
||||||
|
background: '#111111',
|
||||||
|
overlay: '#111111aa',
|
||||||
|
codeBg: '#282833',
|
||||||
|
code: '#ffffffdd'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PurpleThemeDark };
|
||||||
@@ -20,11 +20,12 @@ const PurpleTheme: ThemeTypes = {
|
|||||||
lightsuccess: '#b9f6ca',
|
lightsuccess: '#b9f6ca',
|
||||||
lighterror: '#f9d8d8',
|
lighterror: '#f9d8d8',
|
||||||
lightwarning: '#fff8e1',
|
lightwarning: '#fff8e1',
|
||||||
darkText: '#212121',
|
primaryText: '#000000dd',
|
||||||
lightText: '#616161',
|
secondaryText: '#000000aa',
|
||||||
darkprimary: '#1565c0',
|
darkprimary: '#1565c0',
|
||||||
darksecondary: '#4527a0',
|
darksecondary: '#4527a0',
|
||||||
borderLight: '#d0d0d0',
|
borderLight: '#d0d0d0',
|
||||||
|
border: '#d0d0d0',
|
||||||
inputBorder: '#787878',
|
inputBorder: '#787878',
|
||||||
containerBg: '#eef2f6',
|
containerBg: '#eef2f6',
|
||||||
surface: '#fff',
|
surface: '#fff',
|
||||||
@@ -32,9 +33,13 @@ const PurpleTheme: ThemeTypes = {
|
|||||||
facebook: '#4267b2',
|
facebook: '#4267b2',
|
||||||
twitter: '#1da1f2',
|
twitter: '#1da1f2',
|
||||||
linkedin: '#0e76a8',
|
linkedin: '#0e76a8',
|
||||||
gray100: '#fafafa',
|
gray100: '#fafafacc',
|
||||||
primary200: '#90caf9',
|
primary200: '#90caf9',
|
||||||
secondary200: '#b39ddb'
|
secondary200: '#b39ddb',
|
||||||
|
background: '#f9fafcf4',
|
||||||
|
overlay: '#ffffffaa',
|
||||||
|
codeBg: '#f5f0ff',
|
||||||
|
code: '#673ab7'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ export type ThemeTypes = {
|
|||||||
lightwarning?: string;
|
lightwarning?: string;
|
||||||
darkprimary?: string;
|
darkprimary?: string;
|
||||||
darksecondary?: string;
|
darksecondary?: string;
|
||||||
darkText?: string;
|
primaryText?: string;
|
||||||
lightText?: string;
|
secondaryText?: string;
|
||||||
borderLight?: string;
|
borderLight?: string;
|
||||||
|
border?: string;
|
||||||
inputBorder?: string;
|
inputBorder?: string;
|
||||||
containerBg?: string;
|
containerBg?: string;
|
||||||
surface?: string;
|
surface?: string;
|
||||||
background?: string;
|
background?: string;
|
||||||
|
overlay?: string;
|
||||||
'on-surface-variant'?: string;
|
'on-surface-variant'?: string;
|
||||||
facebook?: string;
|
facebook?: string;
|
||||||
twitter?: string;
|
twitter?: string;
|
||||||
@@ -31,5 +33,7 @@ export type ThemeTypes = {
|
|||||||
gray100?: string;
|
gray100?: string;
|
||||||
primary200?: string;
|
primary200?: string;
|
||||||
secondary200?: string;
|
secondary200?: string;
|
||||||
|
codeBg?: string;
|
||||||
|
code?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-alert style="margin-bottom: 16px"
|
|
||||||
text="这是一个长期实验性功能,目标是实现更具人类机能的 LLM 对话。推荐使用 gpt-4o-mini 作为文本生成和视觉理解模型,成本很低。推荐使用 text-embedding-3-small 作为 Embedding 模型,成本忽略不计。"
|
|
||||||
title="💡实验性功能" type="info" variant="tonal">
|
|
||||||
</v-alert>
|
|
||||||
<v-card>
|
|
||||||
<v-card-text>
|
|
||||||
<v-container fluid>
|
|
||||||
<AstrBotConfig :metadata="project_atri_config_metadata" :iterable="project_atri_config?.project_atri"
|
|
||||||
metadataKey="project_atri">
|
|
||||||
</AstrBotConfig>
|
|
||||||
</v-container>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-btn icon="mdi-content-save" size="x-large" style="position: fixed; right: 52px; bottom: 52px;" color="darkprimary"
|
|
||||||
@click="updateConfig">
|
|
||||||
</v-btn>
|
|
||||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack">
|
|
||||||
{{ save_message }}
|
|
||||||
</v-snackbar>
|
|
||||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios';
|
|
||||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
|
||||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
|
||||||
export default {
|
|
||||||
name: 'AtriProject',
|
|
||||||
components: {
|
|
||||||
AstrBotConfig,
|
|
||||||
WaitingForRestart
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
project_atri_config: {},
|
|
||||||
fetched: false,
|
|
||||||
project_atri_config_metadata: {},
|
|
||||||
save_message_snack: false,
|
|
||||||
save_message: "",
|
|
||||||
save_message_success: "",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getConfig();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getConfig() {
|
|
||||||
// 获取配置
|
|
||||||
axios.get('/api/config/get').then((res) => {
|
|
||||||
this.project_atri_config = res.data.data.config;
|
|
||||||
this.fetched = true
|
|
||||||
this.project_atri_config_metadata = res.data.data.metadata;
|
|
||||||
}).catch((err) => {
|
|
||||||
save_message = err;
|
|
||||||
save_message_snack = true;
|
|
||||||
save_message_success = "error";
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updateConfig() {
|
|
||||||
if (!this.fetched) return;
|
|
||||||
axios.post('/api/config/astrbot/update', this.project_atri_config).then((res) => {
|
|
||||||
if (res.data.status === "ok") {
|
|
||||||
this.save_message = res.data.message;
|
|
||||||
this.save_message_snack = true;
|
|
||||||
this.save_message_success = "success";
|
|
||||||
this.$refs.wfr.check();
|
|
||||||
} else {
|
|
||||||
this.save_message = res.data.message;
|
|
||||||
this.save_message_snack = true;
|
|
||||||
this.save_message_success = "error";
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
this.save_message = err;
|
|
||||||
this.save_message_snack = true;
|
|
||||||
this.save_message_success = "error";
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@@ -1,55 +1,95 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card style="height: 100%;">
|
<v-card style="height: 100%;" elevation="0" class="bg-surface">
|
||||||
<v-card-text style="padding: 0; height: 100%; overflow-y: auto;">
|
<v-card-text style="padding: 0; height: 100%; overflow-y: hidden;">
|
||||||
<div
|
<div class="about-wrapper">
|
||||||
style="display: flex; justify-content: center; align-items: center; height: 100%; flex-direction: column;">
|
<!-- Hero Section -->
|
||||||
<div @click="selectedLogo = selectedLogo == 0 ? 1 : 0" style="height: 300px;">
|
<section class="hero-section">
|
||||||
<img v-if="selectedLogo == 0" width="300" src="@/assets/images/logo-waifu.png" alt="AstrBot Logo"
|
<div class="logo-title-container">
|
||||||
class="fade-in">
|
<div @click="selectedLogo = selectedLogo == 0 ? 1 : 0" class="logo-container">
|
||||||
<img v-if="selectedLogo == 1" width="300" src="@/assets/images/logo-normal.svg" alt="AstrBot Logo"
|
<img v-if="selectedLogo == 0" width="280" src="@/assets/images/logo-waifu.png" alt="AstrBot Logo" class="fade-in">
|
||||||
class="fade-in">
|
<img v-if="selectedLogo == 1" width="280" src="@/assets/images/logo-normal.svg" alt="AstrBot Logo" class="fade-in">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="title-container">
|
||||||
|
<h1 class="text-h2 font-weight-bold">AstrBot</h1>
|
||||||
|
<p class="text-subtitle-1" style="color: var(--v-theme-secondaryText);">A project out of interests and loves ❤️</p>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<v-btn @click="open('https://github.com/Soulter/AstrBot')"
|
||||||
|
color="primary" variant="elevated" prepend-icon="mdi-star">
|
||||||
|
Star 这个项目! 🌟
|
||||||
|
</v-btn>
|
||||||
|
<v-btn class="ml-4" @click="open('https://github.com/Soulter/AstrBot/issues')"
|
||||||
|
color="secondary" variant="elevated" prepend-icon="mdi-comment-question">
|
||||||
|
提交 Issue
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h1 class="mt-8">AstrBot</h1>
|
<!-- Contributors Section -->
|
||||||
|
<section class="contributors-section">
|
||||||
|
<v-container>
|
||||||
|
<v-row justify="center" align="center">
|
||||||
|
<v-col cols="12" md="6" class="pr-md-8 contributors-info">
|
||||||
|
<h2 class="text-h4 font-weight-medium">贡献者</h2>
|
||||||
|
<p class="mb-4 text-body-1" style="color: var(--v-theme-secondaryText);">
|
||||||
|
本项目由众多开源社区成员共同维护。感谢每一位贡献者的付出!
|
||||||
|
</p>
|
||||||
|
<p class="text-body-1" style="color: var(--v-theme-secondaryText);">
|
||||||
|
<a href="https://github.com/Soulter/AstrBot/graphs/contributors" class="text-decoration-none custom-link">查看 AstrBot 贡献者</a>
|
||||||
|
</p>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card variant="outlined" class="overflow-hidden" elevation="2">
|
||||||
|
<v-img v-if="useCustomizerStore().uiTheme==='PurpleThemeDark'"
|
||||||
|
alt="Active Contributors of Soulter/AstrBot"
|
||||||
|
src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=575865240&limit=365&image_size=auto&color_scheme=dark">
|
||||||
|
</v-img>
|
||||||
|
<v-img v-else
|
||||||
|
alt="Active Contributors of Soulter/AstrBot"
|
||||||
|
src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=575865240&limit=365&image_size=auto&color_scheme=light">
|
||||||
|
</v-img>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</section>
|
||||||
|
|
||||||
<span class="mt-2" style="color: #777;">A project out of interests and loves ❤️</span>
|
<!-- Stats Section -->
|
||||||
|
<section class="stats-section">
|
||||||
|
<v-container>
|
||||||
|
<v-row justify="center" align="center" class="flex-md-row-reverse">
|
||||||
|
<v-col cols="12" md="6" class="pl-md-8 stats-info">
|
||||||
|
<h2 class="text-h4 font-weight-medium">全球部署</h2>
|
||||||
|
|
||||||
<span style="color: #777; margin-left: 32px; margin-right: 32px" class="mt-4">By <a
|
<div class="license-container mt-8">
|
||||||
href="https://soulter.top">Soulter</a>, <a
|
<img v-bind="props" src="https://www.gnu.org/graphics/agplv3-with-text-100x42.png" style="cursor: pointer;"/>
|
||||||
href="https://github.com/Soulter/AstrBot/graphs/contributors">AstrBot Contributors</a>
|
<p class="text-caption mt-2" style="color: var(--v-theme-secondaryText);">AstrBot 采用 AGPL v3 协议开源</p>
|
||||||
and <a href="https://github.com/Soulter/AstrBot_Plugins_Collection/graphs/contributors">AstrBot
|
</div>
|
||||||
Plugin Authors</a>
|
</v-col>
|
||||||
</span>
|
<v-col cols="12" md="6">
|
||||||
|
<v-card variant="outlined" class="overflow-hidden" elevation="2">
|
||||||
<!-- Copy-paste in your Readme.md file -->
|
<v-img v-if="useCustomizerStore().uiTheme==='PurpleThemeDark'"
|
||||||
|
alt="Stars Map of Soulter/AstrBot"
|
||||||
<img style="margin-top: 16px; width: 50%; max-width: 500px; margin-left: 32px; margin-right: 32px"
|
src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-map/thumbnail.png?activity=stars&repo_id=575865240&image_size=auto&color_scheme=dark">
|
||||||
alt="Active Contributors of Soulter/AstrBot - Last 28 days"
|
</v-img>
|
||||||
src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=575865240&limit=365&image_size=auto&color_scheme=light">
|
<v-img v-else
|
||||||
|
alt="Stars Map of Soulter/AstrBot"
|
||||||
<img style="margin-top: 16px; width: 50%; max-width: 500px; margin-left: 32px; margin-right: 32px"
|
src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-map/thumbnail.png?activity=stars&repo_id=575865240&image_size=auto&color_scheme=light">
|
||||||
alt="Active Contributors of Soulter/AstrBot - Last 28 days"
|
</v-img>
|
||||||
src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-map/thumbnail.png?activity=stars&repo_id=575865240&image_size=auto&color_scheme=light
|
</v-card>
|
||||||
">
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
<!-- Made with [OSS Insight](https://ossinsight.io/) -->
|
</section>
|
||||||
|
|
||||||
<v-btn class="text-primary mt-8" @click="open('https://github.com/Soulter/AstrBot')"
|
|
||||||
color="lightprimary" variant="flat" rounded="sm">
|
|
||||||
Star 这个项目! 🌟
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn class="text-primary mt-4" @click="open('https://github.com/Soulter/AstrBot/issues')"
|
|
||||||
color="lightprimary" variant="flat" rounded="sm">
|
|
||||||
有使用问题或者功能建议?提交 Issue!
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {useCustomizerStore} from "@/stores/customizer";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AboutPage',
|
name: 'AboutPage',
|
||||||
data() {
|
data() {
|
||||||
@@ -59,26 +99,141 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
useCustomizerStore,
|
||||||
open(url) {
|
open(url) {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
@keyframes fadeIn {
|
.about-wrapper {
|
||||||
from {
|
min-height: 100%;
|
||||||
opacity: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
.hero-section {
|
||||||
opacity: 1;
|
padding: 40px 20px;
|
||||||
}
|
background: linear-gradient(to right bottom, rgba(255,255,255,0.7), rgba(240,240,250,0.3));
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-title-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
max-width: 900px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributors-section, .stats-section {
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributors-section {
|
||||||
|
background-color: var(--v-theme-containerBg, #f9f9fb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributors-info, .stats-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-link {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 0;
|
||||||
|
position: relative;
|
||||||
|
color: var(--v-primary-base);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-link::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
transform: scaleX(0);
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: var(--v-primary-base);
|
||||||
|
transform-origin: bottom right;
|
||||||
|
transition: transform 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-link:hover::after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
transform-origin: bottom left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fadeIn 0.2s ease-in-out;
|
animation: fadeIn 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.logo-title-container {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-container {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contributors-section, .stats-section {
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .v-btn + .v-btn {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
80
dashboard/src/views/AlkaidPage.vue
Normal file
80
dashboard/src/views/AlkaidPage.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<v-card style="height: 100%; width: 100%;">
|
||||||
|
<v-card-text class="pa-4" style="height: 100%;">
|
||||||
|
<v-container fluid class="d-flex flex-column" style="height: 100%;">
|
||||||
|
<div style="margin-bottom: 32px;">
|
||||||
|
<h1 class="gradient-text">The Alkaid Project.</h1>
|
||||||
|
<small style="color: #a3a3a3;">AstrBot Alpha 项目</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
|
||||||
|
<v-btn size="large" :variant="isActive('knowledge-base') ? 'flat' : 'tonal'"
|
||||||
|
:color="isActive('knowledge-base') ? '#9b72cb' : ''" rounded="lg"
|
||||||
|
@click="navigateTo('knowledge-base')">
|
||||||
|
<v-icon start>mdi-text-box-search</v-icon>
|
||||||
|
知识库
|
||||||
|
</v-btn>
|
||||||
|
<v-btn size="large" :variant="isActive('long-term-memory') ? 'flat' : 'tonal'"
|
||||||
|
:color="isActive('long-term-memory') ? '#9b72cb' : ''" rounded="lg"
|
||||||
|
@click="navigateTo('long-term-memory')">
|
||||||
|
<v-icon start>mdi-dots-hexagon</v-icon>
|
||||||
|
长期记忆层
|
||||||
|
</v-btn>
|
||||||
|
<v-btn size="large" :variant="isActive('other') ? 'flat' : 'tonal'"
|
||||||
|
:color="isActive('other') ? '#9b72cb' : ''" rounded="lg"
|
||||||
|
@click="navigateTo('other')">
|
||||||
|
<v-icon start>mdi-tools</v-icon>
|
||||||
|
...
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sub-view" class="flex-grow-1" style="max-height: 100%;">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AlkaidPage',
|
||||||
|
components: {},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
navigateTo(tab) {
|
||||||
|
this.$router.push(`/alkaid/${tab}`);
|
||||||
|
},
|
||||||
|
isActive(tab) {
|
||||||
|
return this.$route.path.includes(`/alkaid/${tab}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 如果在根路径 /alkaid,默认跳转到知识库页面
|
||||||
|
if (this.$route.path === '/alkaid') {
|
||||||
|
this.navigateTo('knowledge-base');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(74deg, #2abfe1 0, #9b72cb 25%, #b55908 50%, #d93025 100%);
|
||||||
|
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
432
dashboard/src/views/AlkaidPage_sigma.vue
Normal file
432
dashboard/src/views/AlkaidPage_sigma.vue
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
<script setup>
|
||||||
|
import Graph from "graphology";
|
||||||
|
import Sigma from "sigma";
|
||||||
|
import ForceSupervisor from "graphology-layout-force/worker";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card style="height: 100%; width: 100%;">
|
||||||
|
<v-card-text class="pa-4" style="height: 100%;">
|
||||||
|
<v-container fluid class="d-flex flex-column" style="height: 100%;">
|
||||||
|
<div style="margin-bottom: 32px;">
|
||||||
|
<h1 class="gradient-text">The Alkaid Project.</h1>
|
||||||
|
<small style="color: #a3a3a3;">AstrBot 实验性项目</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
|
||||||
|
<v-btn size="large" :variant="activeTab === 'long-term-memory' ? 'flat' : 'tonal'"
|
||||||
|
:color="activeTab === 'long-term-memory' ? '#9b72cb' : ''" rounded="lg"
|
||||||
|
@click="activeTab = 'long-term-memory'">
|
||||||
|
<v-icon start>mdi-dots-hexagon</v-icon>
|
||||||
|
长期记忆层
|
||||||
|
</v-btn>
|
||||||
|
<v-btn size="large" :variant="activeTab === 'other' ? 'flat' : 'tonal'"
|
||||||
|
:color="activeTab === 'other' ? '#9b72cb' : ''" rounded="lg" @click="activeTab = 'other'">
|
||||||
|
<v-icon start>mdi-dots-horizontal</v-icon>
|
||||||
|
其他
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'long-term-memory'" id="long-term-memory" class="flex-grow-1"
|
||||||
|
style="display: flex; flex-direction: row;">
|
||||||
|
<div id="graph-container" style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px;">
|
||||||
|
</div>
|
||||||
|
<div id="graph-control-panel"
|
||||||
|
style="min-width: 450px; border: 1px solid #eee; border-radius: 8px; padding: 16px; margin-left: 16px;">
|
||||||
|
<div>
|
||||||
|
<span style="color: #333333;">可视化</span>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<v-autocomplete v-model="searchUserId" :items="userIdList" variant="outlined"
|
||||||
|
label="筛选用户 ID"></v-autocomplete>
|
||||||
|
<v-btn color="primary" @click="onNodeSelect" variant="tonal" style="margin-top: 8px;">
|
||||||
|
<v-icon start>mdi-magnify</v-icon>
|
||||||
|
筛选
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="secondary" @click="resetFilter" variant="tonal"
|
||||||
|
style="margin-top: 8px; margin-left: 8px;">
|
||||||
|
<v-icon start>mdi-filter-remove</v-icon>
|
||||||
|
重置筛选
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<v-btn color="primary" @click="refreshGraph" variant="tonal">
|
||||||
|
<v-icon start>mdi-refresh</v-icon>
|
||||||
|
刷新图形
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
|
<div v-if="selectedNode" class="mt-4">
|
||||||
|
<h3>节点详情</h3>
|
||||||
|
<v-card variant="outlined" class="mt-2 pa-3">
|
||||||
|
<div v-if="selectedNode.id">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">ID:</span>
|
||||||
|
<span>{{ selectedNode.id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode._label">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">类型:</span>
|
||||||
|
<span>{{ selectedNode._label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode.name">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">名称:</span>
|
||||||
|
<span>{{ selectedNode.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode.user_id">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">用户ID:</span>
|
||||||
|
<span>{{ selectedNode.user_id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode.ts">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">时间戳:</span>
|
||||||
|
<span>{{ selectedNode.ts }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode.type">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">类型:</span>
|
||||||
|
<span>{{ selectedNode.type }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="graphStats" class="mt-4">
|
||||||
|
<h3>图形统计</h3>
|
||||||
|
<v-card variant="outlined" class="mt-2 pa-3">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">节点数:</span>
|
||||||
|
<span>{{ graphStats.nodeCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">边数:</span>
|
||||||
|
<span>{{ graphStats.edgeCount }}</span>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'other'" class="flex-grow-1" style="display: flex; flex-direction: column;">
|
||||||
|
<div class="d-flex align-center justify-center"
|
||||||
|
style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px;">
|
||||||
|
<v-icon size="64" color="grey-lighten-1">mdi-tools</v-icon>
|
||||||
|
<p class="text-h6 text-grey ml-4">功能开发中</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||||
|
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||||
|
export default {
|
||||||
|
name: 'AlkaidPage',
|
||||||
|
components: {
|
||||||
|
AstrBotConfig,
|
||||||
|
WaitingForRestart
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
renderer: null,
|
||||||
|
graph: null,
|
||||||
|
layout: null,
|
||||||
|
activeTab: 'long-term-memory',
|
||||||
|
node_data: [],
|
||||||
|
edge_data: [],
|
||||||
|
searchUserId: null,
|
||||||
|
userIdList: [],
|
||||||
|
selectedNode: null,
|
||||||
|
graphStats: null,
|
||||||
|
nodeColors: {
|
||||||
|
'PhaseNode': '#4CAF50', // 绿色
|
||||||
|
'PassageNode': '#2196F3', // 蓝色
|
||||||
|
'FactNode': '#FF9800', // 橙色
|
||||||
|
'default': '#9C27B0' // 紫色作为默认
|
||||||
|
},
|
||||||
|
edgeColors: {
|
||||||
|
'_include_': '#607D8B',
|
||||||
|
'_related_': '#9E9E9E',
|
||||||
|
'default': '#BDBDBD'
|
||||||
|
},
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initSigma();
|
||||||
|
this.ltmGetGraph();
|
||||||
|
this.ltmGetUserIds();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.kill();
|
||||||
|
}
|
||||||
|
if (this.layout) {
|
||||||
|
this.layout.stop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeTab(newVal) {
|
||||||
|
if (newVal === 'long-term-memory') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (!this.renderer) {
|
||||||
|
this.initSigma();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.kill();
|
||||||
|
this.renderer = null;
|
||||||
|
}
|
||||||
|
if (this.layout) {
|
||||||
|
this.layout.stop();
|
||||||
|
this.layout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
ltmGetGraph(userId = null) {
|
||||||
|
this.isLoading = true;
|
||||||
|
const params = userId ? { user_id: userId } : {};
|
||||||
|
|
||||||
|
axios.get('/api/plug/alkaid/ltm/graph', { params })
|
||||||
|
.then(response => {
|
||||||
|
let nodes = response.data.data.nodes;
|
||||||
|
let edges = response.data.data.edges;
|
||||||
|
|
||||||
|
this.node_data = nodes;
|
||||||
|
this.edge_data = edges;
|
||||||
|
|
||||||
|
if (this.graph) {
|
||||||
|
this.graph.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const nodeId = node[0];
|
||||||
|
const nodeData = node[1];
|
||||||
|
|
||||||
|
if (!this.graph.hasNode(nodeId)) {
|
||||||
|
const nodeType = nodeData._label || 'default';
|
||||||
|
const color = this.nodeColors[nodeType] || this.nodeColors['default'];
|
||||||
|
|
||||||
|
this.graph.addNode(nodeId, {
|
||||||
|
x: Math.random(),
|
||||||
|
y: Math.random(),
|
||||||
|
size: 5,
|
||||||
|
label: nodeData.name || nodeId.split('_')[0],
|
||||||
|
color: color,
|
||||||
|
originalData: nodeData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加边
|
||||||
|
edges.forEach(edge => {
|
||||||
|
const sourceId = edge[0];
|
||||||
|
const targetId = edge[1];
|
||||||
|
const edgeData = edge[2];
|
||||||
|
|
||||||
|
if (this.graph.hasNode(sourceId) && this.graph.hasNode(targetId)) {
|
||||||
|
const edgeId = `${sourceId}->${targetId}`;
|
||||||
|
const relationType = edgeData.relation_type || 'default';
|
||||||
|
const color = this.edgeColors[relationType] || this.edgeColors['default'];
|
||||||
|
this.graph.addEdge(sourceId, targetId, {
|
||||||
|
size: 1,
|
||||||
|
color: color,
|
||||||
|
originalData: edgeData,
|
||||||
|
label: relationType,
|
||||||
|
type: "line"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`Edge ${sourceId} -> ${targetId} has missing nodes.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateGraphStats();
|
||||||
|
|
||||||
|
console.log('Graph initialized with', nodes.length, 'nodes and', edges.length, 'edges');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching graph data:', error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.layout) {
|
||||||
|
this.layout.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
ltmGetUserIds() {
|
||||||
|
axios.get('/api/plug/alkaid/ltm/user_ids')
|
||||||
|
.then(response => {
|
||||||
|
this.userIdList = response.data.data;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching user IDs:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateGraphStats() {
|
||||||
|
if (this.graph) {
|
||||||
|
this.graphStats = {
|
||||||
|
nodeCount: this.graph.order,
|
||||||
|
edgeCount: this.graph.size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshGraph() {
|
||||||
|
this.ltmGetGraph(this.searchUserId);
|
||||||
|
},
|
||||||
|
|
||||||
|
onNodeSelect() {
|
||||||
|
console.log('Selected user ID:', this.searchUserId);
|
||||||
|
if (!this.searchUserId || !this.graph) return;
|
||||||
|
|
||||||
|
// 使用API的user_id参数筛选数据
|
||||||
|
this.ltmGetGraph(this.searchUserId);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetFilter() {
|
||||||
|
this.searchUserId = null;
|
||||||
|
this.ltmGetGraph();
|
||||||
|
},
|
||||||
|
|
||||||
|
initSigma() {
|
||||||
|
const container = document.getElementById("graph-container");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.kill();
|
||||||
|
this.renderer = null;
|
||||||
|
}
|
||||||
|
if (this.layout) {
|
||||||
|
this.layout.stop();
|
||||||
|
this.layout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const graph = new Graph({
|
||||||
|
multi: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const layout = new ForceSupervisor(graph, {
|
||||||
|
isNodeFixed: (_, attr) => attr.highlighted, settings: {
|
||||||
|
gravity: 0.0001,
|
||||||
|
repulsion: 0.001
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layout.start();
|
||||||
|
|
||||||
|
this.layout = layout;
|
||||||
|
this.graph = graph;
|
||||||
|
const renderer = new Sigma(graph, container, {
|
||||||
|
minCameraRatio: 0.01,
|
||||||
|
maxCameraRatio: 2,
|
||||||
|
labelRenderedSizeThreshold: 1,
|
||||||
|
renderLabels: true,
|
||||||
|
renderEdgeLabels: true,
|
||||||
|
labelSize: 14,
|
||||||
|
labelColor: "#333333",
|
||||||
|
});
|
||||||
|
this.renderer = renderer;
|
||||||
|
|
||||||
|
let draggedNode = null;
|
||||||
|
let isDragging = false;
|
||||||
|
|
||||||
|
renderer.on("downNode", (e) => {
|
||||||
|
isDragging = true;
|
||||||
|
draggedNode = e.node;
|
||||||
|
graph.setNodeAttribute(draggedNode, "highlighted", true);
|
||||||
|
if (!renderer.getCustomBBox()) renderer.setCustomBBox(renderer.getBBox());
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.on("moveBody", ({ event }) => {
|
||||||
|
if (!isDragging || !draggedNode) return;
|
||||||
|
const pos = renderer.viewportToGraph(event);
|
||||||
|
|
||||||
|
graph.setNodeAttribute(draggedNode, "x", pos.x);
|
||||||
|
graph.setNodeAttribute(draggedNode, "y", pos.y);
|
||||||
|
event.preventSigmaDefault();
|
||||||
|
event.original.preventDefault();
|
||||||
|
event.original.stopPropagation();
|
||||||
|
});
|
||||||
|
const handleUp = () => {
|
||||||
|
if (draggedNode) {
|
||||||
|
graph.removeNodeAttribute(draggedNode, "highlighted");
|
||||||
|
}
|
||||||
|
isDragging = false;
|
||||||
|
draggedNode = null;
|
||||||
|
};
|
||||||
|
renderer.on("upNode", handleUp);
|
||||||
|
renderer.on("upStage", handleUp);
|
||||||
|
|
||||||
|
renderer.on("clickNode", (e) => {
|
||||||
|
const nodeId = e.node;
|
||||||
|
const nodeAttributes = graph.getNodeAttributes(nodeId);
|
||||||
|
this.selectedNode = nodeAttributes.originalData;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.on("clickStage", () => {
|
||||||
|
this.selectedNode = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getRandomColor() {
|
||||||
|
const letters = '0123456789ABCDEF';
|
||||||
|
let color = '#';
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
color += letters[Math.floor(Math.random() * 16)];
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(74deg, #2abfe1 0, #9b72cb 25%, #b55908 50%, #d93025 100%);
|
||||||
|
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-container {
|
||||||
|
position: relative;
|
||||||
|
background-color: #f2f6f9;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-container:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-header {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,31 +12,27 @@ marked.setOptions({
|
|||||||
<v-card class="chat-page-card">
|
<v-card class="chat-page-card">
|
||||||
<v-card-text class="chat-page-container">
|
<v-card-text class="chat-page-container">
|
||||||
<div class="chat-layout">
|
<div class="chat-layout">
|
||||||
<!-- 左侧对话列表面板 - 优化版 -->
|
|
||||||
<div class="sidebar-panel">
|
<div class="sidebar-panel">
|
||||||
<div class="sidebar-header">
|
<div style="padding: 16px; padding-top: 8px;">
|
||||||
<v-btn variant="elevated" rounded="lg" class="new-chat-btn" @click="newC" :disabled="!currCid"
|
<v-btn variant="elevated" rounded="lg" class="new-chat-btn" @click="newC" :disabled="!currCid"
|
||||||
prepend-icon="mdi-plus">
|
prepend-icon="mdi-plus">
|
||||||
创建对话
|
创建对话
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="conversations-container">
|
<div class="conversations-container">
|
||||||
<div class="sidebar-section-title" v-if="conversations.length > 0">
|
|
||||||
对话历史
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-card class="conversation-list-card" v-if="conversations.length > 0" flat>
|
<v-card class="conversation-list-card" v-if="conversations.length > 0" flat>
|
||||||
<v-list density="compact" nav class="conversation-list"
|
<v-list density="compact" nav class="conversation-list"
|
||||||
@update:selected="getConversationMessages">
|
@update:selected="getConversationMessages">
|
||||||
<v-list-item v-for="(item, i) in conversations" :key="item.cid" :value="item.cid"
|
<v-list-item v-for="(item, i) in conversations" :key="item.cid" :value="item.cid"
|
||||||
color="primary" rounded="lg" class="conversation-item" active-color="primary">
|
rounded="lg" class="conversation-item" active-color="primary">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon size="small" icon="mdi-message-text-outline"></v-icon>
|
<v-icon size="small" icon="mdi-message-text-outline"></v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title class="conversation-title">新对话</v-list-item-title>
|
<v-list-item-title class="conversation-title">新对话</v-list-item-title>
|
||||||
<v-list-item-subtitle class="timestamp">{{ formatDate(item.updated_at)
|
<v-list-item-subtitle class="timestamp">{{ formatDate(item.updated_at)
|
||||||
}}</v-list-item-subtitle>
|
}}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -151,10 +147,10 @@ marked.setOptions({
|
|||||||
|
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 -->
|
||||||
<div class="input-area fade-in">
|
<div class="input-area fade-in">
|
||||||
<v-text-field id="input-field" variant="outlined" v-model="prompt" :label="inputFieldLabel"
|
<v-text-field autocomplete="off" id="input-field" variant="outlined" v-model="prompt"
|
||||||
placeholder="开始输入..." :loading="loadingChat" clear-icon="mdi-close-circle" clearable
|
:label="inputFieldLabel" placeholder="开始输入..." :loading="loadingChat"
|
||||||
@click:clear="clearMessage" class="message-input" @keydown="handleInputKeyDown"
|
clear-icon="mdi-close-circle" clearable @click:clear="clearMessage" class="message-input"
|
||||||
hide-details>
|
@keydown="handleInputKeyDown" hide-details>
|
||||||
<template v-slot:loader>
|
<template v-slot:loader>
|
||||||
<v-progress-linear :active="loadingChat" height="3" color="deep-purple"
|
<v-progress-linear :active="loadingChat" height="3" color="deep-purple"
|
||||||
indeterminate></v-progress-linear>
|
indeterminate></v-progress-linear>
|
||||||
@@ -165,7 +161,7 @@ marked.setOptions({
|
|||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn v-bind="props" @click="sendMessage" class="send-btn" icon="mdi-send"
|
<v-btn v-bind="props" @click="sendMessage" class="send-btn" icon="mdi-send"
|
||||||
variant="text" color="deep-purple"
|
variant="text" color="deep-purple"
|
||||||
:disabled="!prompt && stagedImagesUrl.length === 0 && !stagedAudioUrl" />
|
:disabled="!prompt && stagedImagesName.length === 0 && !stagedAudioUrl" />
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
|
|
||||||
@@ -215,7 +211,8 @@ export default {
|
|||||||
messages: [],
|
messages: [],
|
||||||
conversations: [],
|
conversations: [],
|
||||||
currCid: '',
|
currCid: '',
|
||||||
stagedImagesUrl: [],
|
stagedImagesName: [], // 用于存储图片**文件名**的数组
|
||||||
|
stagedImagesUrl: [], // 用于存储图片的blob URL数组
|
||||||
loadingChat: false,
|
loadingChat: false,
|
||||||
|
|
||||||
inputFieldLabel: '聊天吧!',
|
inputFieldLabel: '聊天吧!',
|
||||||
@@ -233,7 +230,9 @@ export default {
|
|||||||
// Ctrl键长按相关变量
|
// Ctrl键长按相关变量
|
||||||
ctrlKeyDown: false,
|
ctrlKeyDown: false,
|
||||||
ctrlKeyTimer: null,
|
ctrlKeyTimer: null,
|
||||||
ctrlKeyLongPressThreshold: 300 // 长按阈值,单位毫秒
|
ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒
|
||||||
|
|
||||||
|
mediaCache: {}, // Add a cache to store media blobs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -262,9 +261,31 @@ export default {
|
|||||||
|
|
||||||
// 移除keyup事件监听
|
// 移除keyup事件监听
|
||||||
document.removeEventListener('keyup', this.handleInputKeyUp);
|
document.removeEventListener('keyup', this.handleInputKeyUp);
|
||||||
|
|
||||||
|
// Cleanup blob URLs
|
||||||
|
this.cleanupMediaCache();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
async getMediaFile(filename) {
|
||||||
|
if (this.mediaCache[filename]) {
|
||||||
|
return this.mediaCache[filename];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/chat/get_file', {
|
||||||
|
params: { filename },
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
|
||||||
|
const blobUrl = URL.createObjectURL(response.data);
|
||||||
|
this.mediaCache[filename] = blobUrl;
|
||||||
|
return blobUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching media file:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async startListeningEvent() {
|
async startListeningEvent() {
|
||||||
const response = await fetch('/api/chat/listen', {
|
const response = await fetch('/api/chat/listen', {
|
||||||
@@ -325,17 +346,19 @@ export default {
|
|||||||
|
|
||||||
if (chunk_json.type === 'image') {
|
if (chunk_json.type === 'image') {
|
||||||
let img = chunk_json.data.replace('[IMAGE]', '');
|
let img = chunk_json.data.replace('[IMAGE]', '');
|
||||||
|
const imageUrl = await this.getMediaFile(img);
|
||||||
let bot_resp = {
|
let bot_resp = {
|
||||||
type: 'bot',
|
type: 'bot',
|
||||||
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);"/>`
|
message: `<img src="${imageUrl}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
|
||||||
}
|
}
|
||||||
this.messages.push(bot_resp);
|
this.messages.push(bot_resp);
|
||||||
} else if (chunk_json.type === 'record') {
|
} else if (chunk_json.type === 'record') {
|
||||||
let audio = chunk_json.data.replace('[RECORD]', '');
|
let audio = chunk_json.data.replace('[RECORD]', '');
|
||||||
|
const audioUrl = await this.getMediaFile(audio);
|
||||||
let bot_resp = {
|
let bot_resp = {
|
||||||
type: 'bot',
|
type: 'bot',
|
||||||
message: `<audio controls class="audio-player">
|
message: `<audio controls class="audio-player">
|
||||||
<source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
|
<source src="${audioUrl}" type="audio/wav">
|
||||||
您的浏览器不支持音频播放。
|
您的浏览器不支持音频播放。
|
||||||
</audio>`
|
</audio>`
|
||||||
}
|
}
|
||||||
@@ -400,15 +423,14 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/chat/post_file', formData, {
|
const response = await axios.post('/api/chat/post_file', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data'
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const audio = response.data.data.filename;
|
const audio = response.data.data.filename;
|
||||||
console.log('Audio uploaded:', audio);
|
console.log('Audio uploaded:', audio);
|
||||||
|
|
||||||
this.stagedAudioUrl = `/api/chat/get_file?filename=${audio}`;
|
this.stagedAudioUrl = audio; // Store just the filename
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error uploading audio:', err);
|
console.error('Error uploading audio:', err);
|
||||||
}
|
}
|
||||||
@@ -427,13 +449,13 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/chat/post_image', formData, {
|
const response = await axios.post('/api/chat/post_image', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data'
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const img = response.data.data.filename;
|
const img = response.data.data.filename;
|
||||||
this.stagedImagesUrl.push(`/api/chat/get_file?filename=${img}`);
|
this.stagedImagesName.push(img); // Store just the filename
|
||||||
|
this.stagedImagesUrl.push(URL.createObjectURL(file)); // Create a blob URL for immediate display
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error uploading image:', err);
|
console.error('Error uploading image:', err);
|
||||||
@@ -443,6 +465,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
removeImage(index) {
|
removeImage(index) {
|
||||||
|
this.stagedImagesName.splice(index, 1);
|
||||||
this.stagedImagesUrl.splice(index, 1);
|
this.stagedImagesUrl.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -459,28 +482,30 @@ export default {
|
|||||||
getConversationMessages(cid) {
|
getConversationMessages(cid) {
|
||||||
if (!cid[0])
|
if (!cid[0])
|
||||||
return;
|
return;
|
||||||
axios.get('/api/chat/get_conversation?conversation_id=' + cid[0]).then(response => {
|
axios.get('/api/chat/get_conversation?conversation_id=' + cid[0]).then(async response => {
|
||||||
this.currCid = cid[0];
|
this.currCid = cid[0];
|
||||||
let message = JSON.parse(response.data.data.history);
|
let message = JSON.parse(response.data.data.history);
|
||||||
for (let i = 0; i < message.length; i++) {
|
for (let i = 0; i < message.length; i++) {
|
||||||
if (message[i].message.startsWith('[IMAGE]')) {
|
if (message[i].message.startsWith('[IMAGE]')) {
|
||||||
let img = message[i].message.replace('[IMAGE]', '');
|
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);"/>`
|
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].message.startsWith('[RECORD]')) {
|
if (message[i].message.startsWith('[RECORD]')) {
|
||||||
let audio = message[i].message.replace('[RECORD]', '');
|
let audio = message[i].message.replace('[RECORD]', '');
|
||||||
|
const audioUrl = await this.getMediaFile(audio);
|
||||||
message[i].message = `<audio controls class="audio-player">
|
message[i].message = `<audio controls class="audio-player">
|
||||||
<source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
|
<source src="${audioUrl}" type="audio/wav">
|
||||||
您的浏览器不支持音频播放。
|
您的浏览器不支持音频播放。
|
||||||
</audio>`
|
</audio>`
|
||||||
}
|
}
|
||||||
if (message[i].image_url && message[i].image_url.length > 0) {
|
if (message[i].image_url && message[i].image_url.length > 0) {
|
||||||
for (let j = 0; j < message[i].image_url.length; j++) {
|
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]}`;
|
message[i].image_url[j] = await this.getMediaFile(message[i].image_url[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (message[i].audio_url) {
|
if (message[i].audio_url) {
|
||||||
message[i].audio_url = `/api/chat/get_file?filename=${message[i].audio_url}`;
|
message[i].audio_url = await this.getMediaFile(message[i].audio_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.messages = message;
|
this.messages = message;
|
||||||
@@ -531,32 +556,41 @@ export default {
|
|||||||
await this.newConversation();
|
await this.newConversation();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messages.push({
|
// Create a message object with actual URLs for display
|
||||||
|
const userMessage = {
|
||||||
type: 'user',
|
type: 'user',
|
||||||
message: this.prompt,
|
message: this.prompt,
|
||||||
image_url: this.stagedImagesUrl,
|
image_url: [],
|
||||||
audio_url: this.stagedAudioUrl
|
audio_url: null
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Convert image filenames to blob URLs for display
|
||||||
|
if (this.stagedImagesName.length > 0) {
|
||||||
|
for (let i = 0; i < this.stagedImagesName.length; i++) {
|
||||||
|
// If it's just a filename, get the blob URL
|
||||||
|
if (!this.stagedImagesName[i].startsWith('blob:')) {
|
||||||
|
const imgUrl = await this.getMediaFile(this.stagedImagesName[i]);
|
||||||
|
userMessage.image_url.push(imgUrl);
|
||||||
|
} else {
|
||||||
|
userMessage.image_url.push(this.stagedImagesName[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert audio filename to blob URL for display
|
||||||
|
if (this.stagedAudioUrl) {
|
||||||
|
if (!this.stagedAudioUrl.startsWith('blob:')) {
|
||||||
|
userMessage.audio_url = await this.getMediaFile(this.stagedAudioUrl);
|
||||||
|
} else {
|
||||||
|
userMessage.audio_url = this.stagedAudioUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages.push(userMessage);
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
|
|
||||||
// images
|
|
||||||
let image_filenames = [];
|
|
||||||
for (let i = 0; i < this.stagedImagesUrl.length; i++) {
|
|
||||||
let img = this.stagedImagesUrl[i].replace('/api/chat/get_file?filename=', '');
|
|
||||||
image_filenames.push(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
// audio
|
|
||||||
let audio_filenames = [];
|
|
||||||
if (this.stagedAudioUrl) {
|
|
||||||
let audio = this.stagedAudioUrl.replace('/api/chat/get_file?filename=', '');
|
|
||||||
audio_filenames.push(audio);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadingChat = true;
|
this.loadingChat = true;
|
||||||
|
|
||||||
|
|
||||||
fetch('/api/chat/send', {
|
fetch('/api/chat/send', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -566,20 +600,19 @@ export default {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: this.prompt,
|
message: this.prompt,
|
||||||
conversation_id: this.currCid,
|
conversation_id: this.currCid,
|
||||||
image_url: image_filenames,
|
image_url: this.stagedImagesName, // Already contains just filenames
|
||||||
audio_url: audio_filenames
|
audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [] // Already contains just filename
|
||||||
}) // 发送请求体
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
this.prompt = '';
|
|
||||||
this.stagedImagesUrl = [];
|
|
||||||
this.stagedAudioUrl = "";
|
|
||||||
|
|
||||||
this.loadingChat = false;
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
})
|
||||||
console.error(err);
|
.then(response => {
|
||||||
});
|
this.prompt = '';
|
||||||
|
this.stagedImagesName = [];
|
||||||
|
this.stagedAudioUrl = "";
|
||||||
|
this.loadingChat = false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -620,6 +653,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cleanupMediaCache() {
|
||||||
|
Object.values(this.mediaCache).forEach(url => {
|
||||||
|
if (url.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.mediaCache = {};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -671,7 +713,6 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
|
||||||
background-color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-page-container {
|
.chat-page-container {
|
||||||
@@ -694,14 +735,13 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
background-color: #fcfcfc;
|
background-color: var(--v-theme-surface) !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversations-container {
|
.conversations-container {
|
||||||
@@ -718,7 +758,7 @@ export default {
|
|||||||
.sidebar-section-title {
|
.sidebar-section-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
@@ -776,7 +816,7 @@ export default {
|
|||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #999;
|
color: var(--v-theme-secondaryText);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,7 +859,7 @@ export default {
|
|||||||
|
|
||||||
.no-conversations-text {
|
.no-conversations-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #999;
|
color: var(--v-theme-secondaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 聊天内容区域 */
|
/* 聊天内容区域 */
|
||||||
@@ -855,21 +895,21 @@ export default {
|
|||||||
.bot-name {
|
.bot-name {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: #673ab7;
|
color: var(--v-theme-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-hint {
|
.welcome-hint {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-hint code {
|
.welcome-hint code {
|
||||||
background-color: #f5f0ff;
|
background-color: var(--v-theme-codeBg);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #673ab7;
|
color: var(--v-theme-code);
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -908,15 +948,15 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-bubble {
|
.user-bubble {
|
||||||
background-color: #f5f0ff;
|
background-color: var(--v-theme-background);
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-bubble {
|
.bot-bubble {
|
||||||
background-color: #fff;
|
background-color: var(--v-theme-surface);
|
||||||
border: 1px solid #e8e8e8;
|
border: 1px solid var(--v-theme-border);
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,9 +1003,9 @@ export default {
|
|||||||
/* 输入区域样式 */
|
/* 输入区域样式 */
|
||||||
.input-area {
|
.input-area {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: #fff;
|
background-color: var(--v-theme-surface);
|
||||||
position: relative;
|
position: relative;
|
||||||
border-top: 1px solid #f5f5f5;
|
border-top: 1px solid var(--v-theme-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-input {
|
.message-input {
|
||||||
@@ -1035,12 +1075,12 @@ export default {
|
|||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content h1 {
|
.markdown-content h1 {
|
||||||
font-size: 1.8em;
|
font-size: 1.8em;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid var(--v-theme-border);
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1063,7 +1103,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content pre {
|
.markdown-content pre {
|
||||||
background-color: #f8f8f8;
|
background-color: var(--v-theme-surface);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -1071,12 +1111,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content code {
|
.markdown-content code {
|
||||||
background-color: #f5f0ff;
|
background-color: var(--v-theme-codeBg);
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #673ab7;
|
color: var(--v-theme-code);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content img {
|
.markdown-content img {
|
||||||
@@ -1086,9 +1126,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content blockquote {
|
.markdown-content blockquote {
|
||||||
border-left: 4px solid #673ab7;
|
border-left: 4px solid var(--v-theme-secondary);
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1100,13 +1140,13 @@ export default {
|
|||||||
|
|
||||||
.markdown-content th,
|
.markdown-content th,
|
||||||
.markdown-content td {
|
.markdown-content td {
|
||||||
border: 1px solid #eee;
|
border: 1px solid var(--v-theme-background);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content th {
|
.markdown-content th {
|
||||||
background-color: #f5f0ff;
|
background-color: var(--v-theme-containerBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 动画类 */
|
/* 动画类 */
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import config from '@/config';
|
|||||||
<div v-for="(val2, key2, index2) in metadata[key]['metadata']">
|
<div v-for="(val2, key2, index2) in metadata[key]['metadata']">
|
||||||
<!-- <h3>{{ metadata[key]['metadata'][key2]['description'] }}</h3> -->
|
<!-- <h3>{{ metadata[key]['metadata'][key2]['description'] }}</h3> -->
|
||||||
<div v-if="metadata[key]['metadata'][key2]?.config_template"
|
<div v-if="metadata[key]['metadata'][key2]?.config_template"
|
||||||
v-show="key2 !== 'platform' && key2 !== 'provider'" style="border: 1px solid #e0e0e0; padding: 8px; margin-bottom: 16px; border-radius: 10px">
|
v-show="key2 !== 'platform' && key2 !== 'provider'" style="border: 1px solid var(--v-theme-border); padding: 8px; margin-bottom: 16px; border-radius: 10px">
|
||||||
<!-- 带有 config_template 的配置项 -->
|
<!-- 带有 config_template 的配置项 -->
|
||||||
<v-list-item-title style="font-weight: bold;">
|
<v-list-item-title style="font-weight: bold;">
|
||||||
{{ metadata[key]['metadata'][key2]['description'] }} ({{ key2 }})
|
{{ metadata[key]['metadata'][key2]['description'] }} ({{ key2 }})
|
||||||
@@ -88,7 +88,7 @@ import config from '@/config';
|
|||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- 如果配置项是一个 object,那么 iterable 需要取到这个 object 的值,否则取到整个 config_data -->
|
<!-- 如果配置项是一个 object,那么 iterable 需要取到这个 object 的值,否则取到整个 config_data -->
|
||||||
<div v-if="metadata[key]['metadata'][key2]['type'] == 'object'" style="border: 1px solid #e0e0e0; padding: 8px; margin-bottom: 16px; border-radius: 10px">
|
<div v-if="metadata[key]['metadata'][key2]['type'] == 'object'" style="border: 1px solid var(--v-theme-border); padding: 8px; margin-bottom: 16px; border-radius: 10px">
|
||||||
<AstrBotConfig
|
<AstrBotConfig
|
||||||
:metadata="metadata[key]['metadata']" :iterable="config_data[key2]" :metadataKey="key2">
|
:metadata="metadata[key]['metadata']" :iterable="config_data[key2]" :metadataKey="key2">
|
||||||
</AstrBotConfig>
|
</AstrBotConfig>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import axios from 'axios';
|
|||||||
<template>
|
<template>
|
||||||
<div style="height: 100%;">
|
<div style="height: 100%;">
|
||||||
<div
|
<div
|
||||||
style="background-color: white; padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px; display: flex; flex-direction: row; align-items: center; justify-content: space-between;">
|
style="background-color: var(--v-theme-surface); padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px; display: flex; flex-direction: row; align-items: center; justify-content: space-between;">
|
||||||
<h4>控制台</h4>
|
<h4>控制台</h4>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<v-switch
|
<v-switch
|
||||||
|
|||||||
@@ -52,13 +52,17 @@ import 'highlight.js/styles/github.css';
|
|||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
|
||||||
<small style="color: #bbb;">每个插件都是作者无偿提供的的劳动成果。如果您喜欢某个插件,请 Star!</small>
|
<small style="color: var(--v-theme-secondaryText);">每个插件都是作者无偿提供的的劳动成果。如果您喜欢某个插件,请 Star!</small>
|
||||||
<div v-if="pinnedPlugins.length > 0" class="mt-4">
|
<div v-if="pinnedPlugins.length > 0" class="mt-4">
|
||||||
<h2>🥳 推荐</h2>
|
<h2>🥳 推荐</h2>
|
||||||
|
|
||||||
<v-row style="margin-top: 8px;">
|
<v-row style="margin-top: 8px;">
|
||||||
<v-col cols="12" md="6" lg="6" v-for="plugin in pinnedPlugins">
|
<v-col cols="12" md="6" lg="6" v-for="plugin in pinnedPlugins">
|
||||||
<ExtensionCard :extension="plugin" market-mode="true" :highlight="true">
|
<ExtensionCard :extension="plugin" class="h-120 rounded-lg"
|
||||||
|
market-mode="true" :highlight="true"
|
||||||
|
@install="extension_url=plugin.repo;
|
||||||
|
newExtension()"
|
||||||
|
@view-readme="open(plugin.repo)">
|
||||||
</ExtensionCard>
|
</ExtensionCard>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@@ -77,7 +81,7 @@ import 'highlight.js/styles/github.css';
|
|||||||
style="height: 80px; width: 80px; margin-right: 8px; border-radius: 8px; margin-top: 8px; margin-bottom: 8px;"
|
style="height: 80px; width: 80px; margin-right: 8px; border-radius: 8px; margin-top: 8px; margin-bottom: 8px;"
|
||||||
alt="logo">
|
alt="logo">
|
||||||
<span v-if="item?.repo"><a :href="item?.repo"
|
<span v-if="item?.repo"><a :href="item?.repo"
|
||||||
style="color: #000; text-decoration:none">{{
|
style="color: var(--v-theme-primaryText, #000); text-decoration:none">{{
|
||||||
item.name }}</a></span>
|
item.name }}</a></span>
|
||||||
<span v-else>{{ item.name }}</span>
|
<span v-else>{{ item.name }}</span>
|
||||||
|
|
||||||
@@ -565,7 +569,7 @@ export default {
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
color: #24292e;
|
color: var(--v-theme-secondaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h1,
|
.markdown-body h1,
|
||||||
@@ -582,13 +586,13 @@ export default {
|
|||||||
|
|
||||||
.markdown-body h1 {
|
.markdown-body h1 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
border-bottom: 1px solid #eaecef;
|
border-bottom: 1px solid var(--v-theme-border);
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h2 {
|
.markdown-body h2 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
border-bottom: 1px solid #eaecef;
|
border-bottom: 1px solid var(--v-theme-border);
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,7 +604,7 @@ export default {
|
|||||||
.markdown-body code {
|
.markdown-body code {
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: rgba(27, 31, 35, 0.05);
|
background-color: var(--v-theme-codeBg);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
@@ -611,7 +615,7 @@ export default {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: #f6f8fa;
|
background-color: var(--v-theme-containerBg);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@@ -631,19 +635,19 @@ export default {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: #fff;
|
background-color: var(--v-theme-background);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote {
|
.markdown-body blockquote {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
color: #6a737d;
|
color: var(--v-theme-secondaryText);
|
||||||
border-left: 0.25em solid #dfe2e5;
|
border-left: 0.25em solid var(--v-theme-border);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body a {
|
.markdown-body a {
|
||||||
color: #0366d6;
|
color: var(--v-theme-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,23 +666,23 @@ export default {
|
|||||||
.markdown-body table th,
|
.markdown-body table th,
|
||||||
.markdown-body table td {
|
.markdown-body table td {
|
||||||
padding: 6px 13px;
|
padding: 6px 13px;
|
||||||
border: 1px solid #dfe2e5;
|
border: 1px solid var(--v-theme-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body table tr {
|
.markdown-body table tr {
|
||||||
background-color: #fff;
|
background-color: var(--v-theme-surface);
|
||||||
border-top: 1px solid #c6cbd1;
|
border-top: 1px solid var(--v-theme-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body table tr:nth-child(2n) {
|
.markdown-body table tr:nth-child(2n) {
|
||||||
background-color: #f6f8fa;
|
background-color: var(--v-theme-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body hr {
|
.markdown-body hr {
|
||||||
height: 0.25em;
|
height: 0.25em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 24px 0;
|
margin: 24px 0;
|
||||||
background-color: #e1e4e8;
|
background-color: var(--v-theme-containerBg);
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -378,6 +378,13 @@ const toggleAllPluginsForPlatform = (platformName) => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getExtensions();
|
await getExtensions();
|
||||||
|
|
||||||
|
// 检查是否有 open_config 参数
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const plugin_name = urlParams.get('open_config');
|
||||||
|
if (plugin_name) {
|
||||||
|
openExtensionConfig(plugin_name);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await commonStore.getPluginCollections();
|
const data = await commonStore.getPluginCollections();
|
||||||
pluginMarketData.value = data;
|
pluginMarketData.value = data;
|
||||||
|
|||||||
@@ -27,13 +27,39 @@
|
|||||||
|
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<!-- 添加分类标签页 -->
|
||||||
|
<v-card-text class="px-4 pt-3 pb-0">
|
||||||
|
<v-tabs v-model="activeProviderTypeTab" bg-color="transparent">
|
||||||
|
<v-tab value="all" class="font-weight-medium px-3">
|
||||||
|
<v-icon start>mdi-filter-variant</v-icon>
|
||||||
|
全部
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="chat_completion" class="font-weight-medium px-3">
|
||||||
|
<v-icon start>mdi-message-text</v-icon>
|
||||||
|
基本对话
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||||||
|
<v-icon start>mdi-microphone-message</v-icon>
|
||||||
|
语音转文字
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="text_to_speech" class="font-weight-medium px-3">
|
||||||
|
<v-icon start>mdi-volume-high</v-icon>
|
||||||
|
文字转语音
|
||||||
|
</v-tab>
|
||||||
|
<v-tab value="embedding" class="font-weight-medium px-3">
|
||||||
|
<v-icon start>mdi-code-json</v-icon>
|
||||||
|
Embedding
|
||||||
|
</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text class="px-4 py-3">
|
<v-card-text class="px-4 py-3">
|
||||||
<item-card-grid
|
<item-card-grid
|
||||||
:items="config_data.provider || []"
|
:items="filteredProviders"
|
||||||
title-field="id"
|
title-field="id"
|
||||||
enabled-field="enable"
|
enabled-field="enable"
|
||||||
empty-icon="mdi-api-off"
|
empty-icon="mdi-api-off"
|
||||||
empty-text="暂无服务提供商,点击 新增服务提供商 添加"
|
:empty-text="getEmptyText()"
|
||||||
@toggle-enabled="providerStatusChange"
|
@toggle-enabled="providerStatusChange"
|
||||||
@delete="deleteProvider"
|
@delete="deleteProvider"
|
||||||
@edit="configExistingProvider"
|
@edit="configExistingProvider"
|
||||||
@@ -61,6 +87,51 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 供应商状态部分 -->
|
||||||
|
<v-card class="mb-6" elevation="2">
|
||||||
|
<v-card-title class="d-flex align-center py-3 px-4">
|
||||||
|
<v-icon color="primary" class="me-2">mdi-heart-pulse</v-icon>
|
||||||
|
<span class="text-h6">供应商可用性</span>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" variant="tonal" :loading="loadingStatus" @click="fetchProviderStatus">
|
||||||
|
<v-icon left>mdi-refresh</v-icon>
|
||||||
|
刷新状态
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-subtitle class="px-4 py-1 text-caption text-medium-emphasis">
|
||||||
|
通过测试模型对话可用性判断,可能产生API费用
|
||||||
|
</v-card-subtitle>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-text class="px-4 py-3">
|
||||||
|
<v-alert v-if="providerStatuses.length === 0" type="info" variant="tonal">
|
||||||
|
点击"刷新状态"按钮获取供应商可用性
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-container v-else class="pa-0">
|
||||||
|
<v-row>
|
||||||
|
<v-col v-for="status in providerStatuses" :key="status.id" cols="12" sm="6" md="4">
|
||||||
|
<v-card variant="outlined" class="status-card">
|
||||||
|
<v-card-item>
|
||||||
|
<v-icon :color="status.status === 'available' ? 'success' : 'error'" class="me-2">
|
||||||
|
{{ status.status === 'available' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||||
|
</v-icon>
|
||||||
|
<span class="font-weight-bold">{{ status.id }}</span>
|
||||||
|
<v-chip :color="status.status === 'available' ? 'success' : 'error'" size="small" class="ml-2">
|
||||||
|
{{ status.status === 'available' ? '可用' : '不可用' }}
|
||||||
|
</v-chip>
|
||||||
|
</v-card-item>
|
||||||
|
<v-card-text v-if="status.status === 'unavailable'" class="text-caption text-medium-emphasis">
|
||||||
|
<span class="font-weight-bold">错误信息:</span> {{ status.error }}
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
<!-- 日志部分 -->
|
<!-- 日志部分 -->
|
||||||
<v-card elevation="2">
|
<v-card elevation="2">
|
||||||
<v-card-title class="d-flex align-center py-3 px-4">
|
<v-card-title class="d-flex align-center py-3 px-4">
|
||||||
@@ -109,10 +180,14 @@
|
|||||||
<v-icon start>mdi-volume-high</v-icon>
|
<v-icon start>mdi-volume-high</v-icon>
|
||||||
文字转语音
|
文字转语音
|
||||||
</v-tab>
|
</v-tab>
|
||||||
|
<v-tab value="embedding" class="font-weight-medium px-3">
|
||||||
|
<v-icon start>mdi-code-json</v-icon>
|
||||||
|
Embedding
|
||||||
|
</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
||||||
<v-window v-model="activeProviderTab" class="mt-4">
|
<v-window v-model="activeProviderTab" class="mt-4">
|
||||||
<v-window-item v-for="tabType in ['chat_completion', 'speech_to_text', 'text_to_speech']"
|
<v-window-item v-for="tabType in ['chat_completion', 'speech_to_text', 'text_to_speech', 'embedding']"
|
||||||
:key="tabType"
|
:key="tabType"
|
||||||
:value="tabType">
|
:value="tabType">
|
||||||
<v-row class="mt-1">
|
<v-row class="mt-1">
|
||||||
@@ -222,9 +297,58 @@ export default {
|
|||||||
|
|
||||||
showConsole: false,
|
showConsole: false,
|
||||||
|
|
||||||
|
// 供应商状态相关
|
||||||
|
providerStatuses: [],
|
||||||
|
loadingStatus: false,
|
||||||
|
|
||||||
// 新增提供商对话框相关
|
// 新增提供商对话框相关
|
||||||
showAddProviderDialog: false,
|
showAddProviderDialog: false,
|
||||||
activeProviderTab: 'chat_completion',
|
activeProviderTab: 'chat_completion',
|
||||||
|
|
||||||
|
// 添加提供商类型分类
|
||||||
|
activeProviderTypeTab: 'all',
|
||||||
|
|
||||||
|
// 兼容旧版本(< v3.5.11)的 mapping,用于映射到对应的提供商能力类型
|
||||||
|
oldVersionProviderTypeMapping: {
|
||||||
|
"openai_chat_completion": "chat_completion",
|
||||||
|
"anthropic_chat_completion": "chat_completion",
|
||||||
|
"googlegenai_chat_completion": "chat_completion",
|
||||||
|
"zhipu_chat_completion": "chat_completion",
|
||||||
|
"llm_tuner": "chat_completion",
|
||||||
|
"dify": "chat_completion",
|
||||||
|
"dashscope": "chat_completion",
|
||||||
|
"openai_whisper_api": "speech_to_text",
|
||||||
|
"openai_whisper_selfhost": "speech_to_text",
|
||||||
|
"sensevoice_stt_selfhost": "speech_to_text",
|
||||||
|
"openai_tts_api": "text_to_speech",
|
||||||
|
"edge_tts": "text_to_speech",
|
||||||
|
"gsvi_tts_api": "text_to_speech",
|
||||||
|
"fishaudio_tts_api": "text_to_speech",
|
||||||
|
"dashscope_tts": "text_to_speech",
|
||||||
|
"azure_tts": "text_to_speech",
|
||||||
|
"minimax_tts_api": "text_to_speech",
|
||||||
|
"volcengine_tts": "text_to_speech",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
// 根据选择的标签过滤提供商列表
|
||||||
|
filteredProviders() {
|
||||||
|
if (!this.config_data.provider || this.activeProviderTypeTab === 'all') {
|
||||||
|
return this.config_data.provider || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.config_data.provider.filter(provider => {
|
||||||
|
// 如果provider.provider_type已经存在,直接使用它
|
||||||
|
if (provider.provider_type) {
|
||||||
|
return provider.provider_type === this.activeProviderTypeTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则使用映射关系
|
||||||
|
const mappedType = this.oldVersionProviderTypeMapping[provider.type];
|
||||||
|
return mappedType === this.activeProviderTypeTab;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -243,6 +367,15 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取空列表文本
|
||||||
|
getEmptyText() {
|
||||||
|
if (this.activeProviderTypeTab === 'all') {
|
||||||
|
return "暂无服务提供商,点击 新增服务提供商 添加";
|
||||||
|
} else {
|
||||||
|
return `暂无${this.getTabTypeName(this.activeProviderTypeTab)}类型的服务提供商,点击 新增服务提供商 添加`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 按提供商类型获取模板列表
|
// 按提供商类型获取模板列表
|
||||||
getTemplatesByType(type) {
|
getTemplatesByType(type) {
|
||||||
const templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {};
|
const templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {};
|
||||||
@@ -294,7 +427,8 @@ export default {
|
|||||||
const names = {
|
const names = {
|
||||||
'chat_completion': '基本对话',
|
'chat_completion': '基本对话',
|
||||||
'speech_to_text': '语音转文本',
|
'speech_to_text': '语音转文本',
|
||||||
'text_to_speech': '文本转语音'
|
'text_to_speech': '文本转语音',
|
||||||
|
'embedding': 'Embedding'
|
||||||
};
|
};
|
||||||
return names[tabType] || tabType;
|
return names[tabType] || tabType;
|
||||||
},
|
},
|
||||||
@@ -442,6 +576,22 @@ export default {
|
|||||||
this.save_message = message;
|
this.save_message = message;
|
||||||
this.save_message_success = "error";
|
this.save_message_success = "error";
|
||||||
this.save_message_snack = true;
|
this.save_message_snack = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取供应商状态
|
||||||
|
fetchProviderStatus() {
|
||||||
|
this.loadingStatus = true;
|
||||||
|
axios.get('/api/config/provider/check_status').then((res) => {
|
||||||
|
if (res.data && res.data.status === 'ok') {
|
||||||
|
this.providerStatuses = res.data.data || [];
|
||||||
|
} else {
|
||||||
|
this.showError(res.data?.message || "获取供应商状态失败");
|
||||||
|
}
|
||||||
|
this.loadingStatus = false;
|
||||||
|
}).catch((err) => {
|
||||||
|
this.loadingStatus = false;
|
||||||
|
this.showError(err.response?.data?.message || err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div style="background-color: white; padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px;">
|
<div style="background-color: var(--v-theme-surface, #fff); padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px;">
|
||||||
|
|
||||||
<v-list lines="two">
|
<v-list lines="two">
|
||||||
<v-list-subheader>网络</v-list-subheader>
|
<v-list-subheader>网络</v-list-subheader>
|
||||||
|
|||||||
@@ -327,7 +327,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<small>1. 某些 MCP 服务器可能需要按照其要求在 env 中填充 `API_KEY` 或 `TOKEN` 等信息,请注意检查是否填写。</small>
|
<small>1. 某些 MCP 服务器可能需要按照其要求在 env 中填充 `API_KEY` 或 `TOKEN` 等信息,请注意检查是否填写。</small>
|
||||||
<br>
|
<br>
|
||||||
<small>2. 当配置中带有 url 参数时,将使用 SSE 的方式连接到服务器。</small>
|
<small>2. 当配置中指定 url 参数时:如果还同时指定 `transport` 参数的值为 `streamable_http`,则使用 Steamable HTTP,否则使用 SSE 连接。</small>
|
||||||
|
|
||||||
<div class="monaco-container">
|
<div class="monaco-container">
|
||||||
<VueMonacoEditor v-model:value="serverConfigJson" theme="vs-dark" language="json" :options="{
|
<VueMonacoEditor v-model:value="serverConfigJson" theme="vs-dark" language="json" :options="{
|
||||||
|
|||||||
894
dashboard/src/views/alkaid/KnowledgeBase.vue
Normal file
894
dashboard/src/views/alkaid/KnowledgeBase.vue
Normal file
@@ -0,0 +1,894 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-grow-1" style="display: flex; flex-direction: column; height: 100%;">
|
||||||
|
<div style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px; padding: 16px">
|
||||||
|
<!-- knowledge card -->
|
||||||
|
<div v-if="!installed" class="d-flex align-center justify-center flex-column"
|
||||||
|
style="flex-grow: 1; width: 100%; height: 100%;">
|
||||||
|
<h2>还没有安装知识库插件
|
||||||
|
<v-icon v-class="ml - 2" size="small" color="grey"
|
||||||
|
@click="openUrl('https://astrbot.app/use/knowledge-base.html')">mdi-information-outline</v-icon>
|
||||||
|
</h2>
|
||||||
|
<v-btn style="margin-top: 16px;" variant="tonal" color="primary" @click="installPlugin"
|
||||||
|
:loading="installing">
|
||||||
|
立即安装
|
||||||
|
</v-btn>
|
||||||
|
<ConsoleDisplayer v-show="installing" style="background-color: #fff; max-height: 300px; margin-top: 16px; max-width: 100%" :show-level-btns="false"></ConsoleDisplayer>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="kbCollections.length == 0" class="d-flex align-center justify-center flex-column"
|
||||||
|
style="flex-grow: 1; width: 100%; height: 100%;">
|
||||||
|
<h2>还没有知识库,快创建一个吧!🙂</h2>
|
||||||
|
<v-btn style="margin-top: 16px;" variant="tonal" color="primary" @click="showCreateDialog = true">
|
||||||
|
创建知识库
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h2 class="mb-4">知识库列表
|
||||||
|
<v-icon v-class="ml - 2" size="x-small" color="grey"
|
||||||
|
@click="openUrl('https://astrbot.app/use/knowledge-base.html')">mdi-information-outline</v-icon>
|
||||||
|
</h2>
|
||||||
|
<v-btn class="mb-4" prepend-icon="mdi-plus" variant="tonal" color="primary"
|
||||||
|
@click="showCreateDialog = true">
|
||||||
|
创建知识库
|
||||||
|
</v-btn>
|
||||||
|
<v-btn class="mb-4 ml-4" prepend-icon="mdi-cog" variant="tonal" color="success"
|
||||||
|
@click="$router.push('/extension?open_config=astrbot_plugin_knowledge_base')">
|
||||||
|
配置
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<div class="kb-grid">
|
||||||
|
<div v-for="(kb, index) in kbCollections" :key="index" class="kb-card"
|
||||||
|
@click="openKnowledgeBase(kb)">
|
||||||
|
<div class="book-spine"></div>
|
||||||
|
<div class="book-content">
|
||||||
|
<div class="emoji-container">
|
||||||
|
<span class="kb-emoji">{{ kb.emoji || '🙂' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="kb-name">{{ kb.collection_name }}</div>
|
||||||
|
<div class="kb-count">{{ kb.count || 0 }} 条知识</div>
|
||||||
|
<div class="kb-actions">
|
||||||
|
<v-btn icon variant="text" size="small" color="error" @click.stop="confirmDelete(kb)">
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 16px; text-align: center;">
|
||||||
|
<small style="color: #a3a3a3">Tips: 在聊天页面通过 /kb 指令了解如何使用!</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 创建知识库对话框 -->
|
||||||
|
<v-dialog v-model="showCreateDialog" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h4">创建新知识库</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
|
||||||
|
<div style="width: 100%; display: flex; align-items: center; justify-content: center;">
|
||||||
|
<span id="emoji-display" @click="showEmojiPicker = true">
|
||||||
|
{{ newKB.emoji || '🙂' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<v-form @submit.prevent="submitCreateForm">
|
||||||
|
|
||||||
|
|
||||||
|
<v-text-field variant="outlined" v-model="newKB.name" label="知识库名称" required></v-text-field>
|
||||||
|
|
||||||
|
<v-textarea v-model="newKB.description" label="描述" variant="outlined" placeholder="知识库的简短描述..."
|
||||||
|
rows="3"></v-textarea>
|
||||||
|
|
||||||
|
<v-select v-model="newKB.embedding_provider_id" :items="embeddingProviderConfigs"
|
||||||
|
:item-props="embeddingModelProps" label="Embedding(嵌入)模型" variant="outlined" class="mt-2">
|
||||||
|
</v-select>
|
||||||
|
|
||||||
|
<small>Tips: 一旦选择了一个知识库的嵌入模型,请不要再修改该提供商的模型或者向量维度信息,否则将严重影响该知识库的召回率甚至报错。</small>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="error" variant="text" @click="showCreateDialog = false">取消</v-btn>
|
||||||
|
<v-btn color="primary" variant="text" @click="submitCreateForm">创建</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 表情选择器对话框 -->
|
||||||
|
<v-dialog v-model="showEmojiPicker" max-width="400px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h6">选择表情</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="emoji-picker">
|
||||||
|
<div v-for="(category, catIndex) in emojiCategories" :key="catIndex" class="mb-4">
|
||||||
|
<div class="text-subtitle-2 mb-2">{{ category.name }}</div>
|
||||||
|
<div class="emoji-grid">
|
||||||
|
<div v-for="(emoji, emojiIndex) in category.emojis" :key="emojiIndex" class="emoji-item"
|
||||||
|
@click="selectEmoji(emoji)">
|
||||||
|
{{ emoji }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" variant="text" @click="showEmojiPicker = false">关闭</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 知识库内容管理对话框 -->
|
||||||
|
<v-dialog v-model="showContentDialog" max-width="1000px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<div class="me-2 emoji-sm">{{ currentKB.emoji || '🙂' }}</div>
|
||||||
|
<span>{{ currentKB.collection_name }} - 知识库管理</span>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn variant="plain" icon @click="showContentDialog = false">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<div v-if="currentKB._embedding_provider_config" class="px-6 py-2">
|
||||||
|
<v-chip class="mr-2" color="primary" variant="tonal" size="small" rounded="sm">
|
||||||
|
<v-icon start size="small">mdi-database</v-icon>
|
||||||
|
嵌入模型: {{ currentKB._embedding_provider_config.embedding_model }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip color="secondary" variant="tonal" size="small" rounded="sm">
|
||||||
|
<v-icon start size="small">mdi-vector-point</v-icon>
|
||||||
|
向量维度: {{ currentKB._embedding_provider_config.embedding_dimensions }}
|
||||||
|
</v-chip>
|
||||||
|
<small style="margin-left: 8px;">💡 使用方式: 在聊天页中输入 “/kb use {{ currentKB.collection_name }}”</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-tabs v-model="activeTab">
|
||||||
|
<v-tab value="upload">上传文件</v-tab>
|
||||||
|
<v-tab value="search">搜索内容</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<v-window v-model="activeTab" class="mt-4">
|
||||||
|
<!-- 上传文件标签页 -->
|
||||||
|
<v-window-item value="upload">
|
||||||
|
<div class="upload-container pa-4">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h3>上传文件到知识库</h3>
|
||||||
|
<p class="text-subtitle-1">支持 txt、pdf、word、excel 等多种格式</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-zone" @dragover.prevent @drop.prevent="onFileDrop"
|
||||||
|
@click="triggerFileInput">
|
||||||
|
<input type="file" ref="fileInput" style="display: none" @change="onFileSelected" />
|
||||||
|
<v-icon size="48" color="primary">mdi-cloud-upload</v-icon>
|
||||||
|
<p class="mt-2">拖放文件到这里或点击上传</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 优化后的分片长度和重叠长度设置 -->
|
||||||
|
<v-card class="mt-4 chunk-settings-card" variant="outlined" color="grey-lighten-4">
|
||||||
|
<v-card-title class="pa-4 pb-0 d-flex align-center">
|
||||||
|
<v-icon color="primary" class="mr-2">mdi-puzzle-outline</v-icon>
|
||||||
|
<span class="text-subtitle-1 font-weight-bold">分片设置</span>
|
||||||
|
<v-tooltip location="top">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-icon v-bind="props" class="ml-2" size="small" color="grey">
|
||||||
|
mdi-information-outline
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<span>
|
||||||
|
分片长度决定每块文本的大小,重叠长度决定相邻文本块之间的重叠程度。<br>
|
||||||
|
较小的分片更精确但会增加数量,适当的重叠可提高检索准确性。
|
||||||
|
</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4 pt-2">
|
||||||
|
<div class="d-flex flex-wrap" style="gap: 8px">
|
||||||
|
<v-text-field v-model="chunkSize" label="分片长度" type="number"
|
||||||
|
hint="控制每个文本块大小,留空使用默认值" persistent-hint variant="outlined"
|
||||||
|
density="comfortable" class="flex-grow-1 chunk-field"
|
||||||
|
prepend-inner-icon="mdi-text-box-outline" min="50"></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field v-model="overlap" label="重叠长度" type="number"
|
||||||
|
hint="控制相邻文本块重叠度,留空使用默认值" persistent-hint variant="outlined"
|
||||||
|
density="comfortable" class="flex-grow-1 chunk-field"
|
||||||
|
prepend-inner-icon="mdi-vector-intersection" min="0"></v-text-field>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<div class="selected-files mt-4" v-if="selectedFile">
|
||||||
|
<div type="info" variant="tonal" class="d-flex align-center">
|
||||||
|
<div>
|
||||||
|
<v-icon class="me-2">{{ getFileIcon(selectedFile.name) }}</v-icon>
|
||||||
|
<span style="font-weight: 1000;">{{ selectedFile.name }}</span>
|
||||||
|
</div>
|
||||||
|
<v-btn size="small" color="error" variant="text" @click="selectedFile = null">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<v-btn color="primary" variant="elevated" :loading="uploading"
|
||||||
|
:disabled="!selectedFile" @click="uploadFile">
|
||||||
|
上传到知识库
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-progress mt-4" v-if="uploading">
|
||||||
|
<v-progress-linear indeterminate color="primary"></v-progress-linear>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<!-- 搜索内容标签页 -->
|
||||||
|
<v-window-item value="search">
|
||||||
|
<div class="search-container pa-4">
|
||||||
|
<v-form @submit.prevent="searchKnowledgeBase" class="d-flex align-center">
|
||||||
|
<v-text-field v-model="searchQuery" label="搜索知识库内容" append-icon="mdi-magnify"
|
||||||
|
variant="outlined" class="flex-grow-1 me-2" @click:append="searchKnowledgeBase"
|
||||||
|
@keyup.enter="searchKnowledgeBase" placeholder="输入关键词搜索知识库内容..."
|
||||||
|
hide-details></v-text-field>
|
||||||
|
|
||||||
|
<v-select v-model="topK" :items="[3, 5, 10, 20]" label="结果数量" variant="outlined"
|
||||||
|
style="max-width: 120px;" hide-details></v-select>
|
||||||
|
</v-form>
|
||||||
|
|
||||||
|
<div class="search-results mt-4">
|
||||||
|
<div v-if="searching">
|
||||||
|
<v-progress-linear indeterminate color="primary"></v-progress-linear>
|
||||||
|
<p class="text-center mt-4">正在搜索...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="searchResults.length > 0">
|
||||||
|
<h3 class="mb-2">搜索结果</h3>
|
||||||
|
<v-card v-for="(result, index) in searchResults" :key="index"
|
||||||
|
class="mb-4 search-result-card" variant="outlined">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex align-center mb-2">
|
||||||
|
<v-icon class="me-2" size="small"
|
||||||
|
color="primary">mdi-file-document-outline</v-icon>
|
||||||
|
<span class="text-caption text-medium-emphasis">{{
|
||||||
|
result.metadata.source }}</span>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-chip v-if="result.score" size="small" color="primary"
|
||||||
|
variant="tonal">
|
||||||
|
相关度: {{ Math.round(result.score * 100) }}%
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<div class="search-content">{{ result.content }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="searchPerformed">
|
||||||
|
<v-alert type="info" variant="tonal">
|
||||||
|
没有找到匹配的内容
|
||||||
|
</v-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-window-item>
|
||||||
|
</v-window>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 删除知识库确认对话框 -->
|
||||||
|
<v-dialog v-model="showDeleteDialog" max-width="400px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">确认删除</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p>您确定要删除知识库 <span class="font-weight-bold">{{ deleteTarget.collection_name }}</span> 吗?</p>
|
||||||
|
<p class="text-red">此操作不可逆,所有知识库内容将被永久删除。</p>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey-darken-1" variant="text" @click="showDeleteDialog = false">取消</v-btn>
|
||||||
|
<v-btn color="error" variant="text" @click="deleteKnowledgeBase" :loading="deleting">删除</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 消息提示 -->
|
||||||
|
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
|
||||||
|
{{ snackbar.text }}
|
||||||
|
</v-snackbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KnowledgeBase',
|
||||||
|
components: {
|
||||||
|
ConsoleDisplayer,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
installing: false,
|
||||||
|
kbCollections: [],
|
||||||
|
showCreateDialog: false,
|
||||||
|
showEmojiPicker: false,
|
||||||
|
newKB: {
|
||||||
|
name: '',
|
||||||
|
emoji: '🙂',
|
||||||
|
description: '',
|
||||||
|
embedding_provider_id: ''
|
||||||
|
},
|
||||||
|
snackbar: {
|
||||||
|
show: false,
|
||||||
|
text: '',
|
||||||
|
color: 'success'
|
||||||
|
},
|
||||||
|
emojiCategories: [
|
||||||
|
{
|
||||||
|
name: '笑脸和情感',
|
||||||
|
emojis: ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '动物和自然',
|
||||||
|
emojis: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '食物和饮料',
|
||||||
|
emojis: ['🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '活动和物品',
|
||||||
|
emojis: ['⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🥅', '🏒', '🏑', '🥍']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '旅行和地点',
|
||||||
|
emojis: ['🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🚚', '🚛', '🚜', '🛴', '🚲']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '符号和旗帜',
|
||||||
|
emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
showContentDialog: false,
|
||||||
|
currentKB: {
|
||||||
|
collection_name: '',
|
||||||
|
emoji: ''
|
||||||
|
},
|
||||||
|
activeTab: 'upload',
|
||||||
|
selectedFile: null,
|
||||||
|
chunkSize: null,
|
||||||
|
overlap: null,
|
||||||
|
uploading: false,
|
||||||
|
searchQuery: '',
|
||||||
|
searchResults: [],
|
||||||
|
searching: false,
|
||||||
|
searchPerformed: false,
|
||||||
|
topK: 5,
|
||||||
|
showDeleteDialog: false,
|
||||||
|
deleteTarget: {
|
||||||
|
collection_name: ''
|
||||||
|
},
|
||||||
|
deleting: false,
|
||||||
|
embeddingProviderConfigs: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.checkPlugin();
|
||||||
|
this.getEmbeddingProviderList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
embeddingModelProps(providerConfig) {
|
||||||
|
return {
|
||||||
|
title: providerConfig.embedding_model,
|
||||||
|
subtitle: `提供商 ID: ${providerConfig.id} | 嵌入模型维度: ${providerConfig.embedding_dimensions}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkPlugin() {
|
||||||
|
axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base')
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status !== 'ok') {
|
||||||
|
this.showSnackbar('插件未安装或不可用', 'error');
|
||||||
|
}
|
||||||
|
if (response.data.data.length > 0) {
|
||||||
|
this.installed = true;
|
||||||
|
this.getKBCollections();
|
||||||
|
} else {
|
||||||
|
this.installed = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error checking plugin:', error);
|
||||||
|
this.showSnackbar('检查插件失败', 'error');
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
installPlugin() {
|
||||||
|
this.installing = true;
|
||||||
|
axios.post('/api/plugin/install', {
|
||||||
|
url: "https://github.com/lxfight/astrbot_plugin_knowledge_base",
|
||||||
|
proxy: localStorage.getItem('selectedGitHubProxy') || ""
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'ok') {
|
||||||
|
this.checkPlugin();
|
||||||
|
} else {
|
||||||
|
this.showSnackbar(response.data.message || '安装失败', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error installing plugin:', error);
|
||||||
|
this.showSnackbar('安装插件失败', 'error');
|
||||||
|
}).finally(() => {
|
||||||
|
this.installing = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getKBCollections() {
|
||||||
|
axios.get('/api/plug/alkaid/kb/collections')
|
||||||
|
.then(response => {
|
||||||
|
this.kbCollections = response.data.data;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching knowledge base collections:', error);
|
||||||
|
this.showSnackbar('获取知识库列表失败', 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createCollection(name, emoji, description) {
|
||||||
|
// 如果 this.newKB.embedding_provider_id 是 Object
|
||||||
|
if (typeof this.newKB.embedding_provider_id === 'object') {
|
||||||
|
this.newKB.embedding_provider_id = this.newKB.embedding_provider_id.id || '';
|
||||||
|
}
|
||||||
|
axios.post('/api/plug/alkaid/kb/create_collection', {
|
||||||
|
collection_name: name,
|
||||||
|
emoji: emoji,
|
||||||
|
description: description,
|
||||||
|
embedding_provider_id: this.newKB.embedding_provider_id || ''
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'ok') {
|
||||||
|
this.showSnackbar('知识库创建成功');
|
||||||
|
this.getKBCollections();
|
||||||
|
this.showCreateDialog = false;
|
||||||
|
this.resetNewKB();
|
||||||
|
} else {
|
||||||
|
this.showSnackbar(response.data.message || '创建失败', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error creating knowledge base collection:', error);
|
||||||
|
this.showSnackbar('创建知识库失败', 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
submitCreateForm() {
|
||||||
|
if (!this.newKB.name) {
|
||||||
|
this.showSnackbar('请输入知识库名称', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.createCollection(
|
||||||
|
this.newKB.name,
|
||||||
|
this.newKB.emoji || '🙂',
|
||||||
|
this.newKB.description,
|
||||||
|
this.newKB.embedding_provider_id || ''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetNewKB() {
|
||||||
|
this.newKB = {
|
||||||
|
name: '',
|
||||||
|
emoji: '🙂',
|
||||||
|
description: '',
|
||||||
|
embedding_provider: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
openKnowledgeBase(kb) {
|
||||||
|
// 不再跳转路由,而是打开对话框
|
||||||
|
this.currentKB = kb;
|
||||||
|
this.showContentDialog = true;
|
||||||
|
this.resetContentDialog();
|
||||||
|
},
|
||||||
|
|
||||||
|
resetContentDialog() {
|
||||||
|
this.activeTab = 'upload';
|
||||||
|
this.selectedFile = null;
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.searchResults = [];
|
||||||
|
this.searchPerformed = false;
|
||||||
|
// 重置分片长度和重叠长度参数
|
||||||
|
this.chunkSize = null;
|
||||||
|
this.overlap = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
triggerFileInput() {
|
||||||
|
this.$refs.fileInput.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileSelected(event) {
|
||||||
|
const files = event.target.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
this.selectedFile = files[0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileDrop(event) {
|
||||||
|
const files = event.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
this.selectedFile = files[0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getFileIcon(filename) {
|
||||||
|
const extension = filename.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
switch (extension) {
|
||||||
|
case 'pdf':
|
||||||
|
return 'mdi-file-pdf-box';
|
||||||
|
case 'doc':
|
||||||
|
case 'docx':
|
||||||
|
return 'mdi-file-word-box';
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsx':
|
||||||
|
return 'mdi-file-excel-box';
|
||||||
|
case 'ppt':
|
||||||
|
case 'pptx':
|
||||||
|
return 'mdi-file-powerpoint-box';
|
||||||
|
case 'txt':
|
||||||
|
return 'mdi-file-document-outline';
|
||||||
|
default:
|
||||||
|
return 'mdi-file-outline';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadFile() {
|
||||||
|
if (!this.selectedFile) {
|
||||||
|
this.showSnackbar('请先选择文件', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uploading = true;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', this.selectedFile);
|
||||||
|
formData.append('collection_name', this.currentKB.collection_name);
|
||||||
|
|
||||||
|
// 添加可选的分片长度和重叠长度参数
|
||||||
|
if (this.chunkSize && this.chunkSize > 0) {
|
||||||
|
formData.append('chunk_size', this.chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.overlap && this.overlap >= 0) {
|
||||||
|
formData.append('chunk_overlap', this.overlap);
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post('/api/plug/alkaid/kb/collection/add_file', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'ok') {
|
||||||
|
this.showSnackbar('操作成功: ' + response.data.message);
|
||||||
|
this.selectedFile = null;
|
||||||
|
|
||||||
|
// 刷新知识库列表,获取更新的数量
|
||||||
|
this.getKBCollections();
|
||||||
|
} else {
|
||||||
|
this.showSnackbar(response.data.message || '上传失败', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error uploading file:', error);
|
||||||
|
this.showSnackbar('文件上传失败', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.uploading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
searchKnowledgeBase() {
|
||||||
|
if (!this.searchQuery.trim()) {
|
||||||
|
this.showSnackbar('请输入搜索内容', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searching = true;
|
||||||
|
this.searchPerformed = true;
|
||||||
|
|
||||||
|
axios.get(`/api/plug/alkaid/kb/collection/search`, {
|
||||||
|
params: {
|
||||||
|
collection_name: this.currentKB.collection_name,
|
||||||
|
query: this.searchQuery,
|
||||||
|
top_k: this.topK
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'ok') {
|
||||||
|
this.searchResults = response.data.data || [];
|
||||||
|
|
||||||
|
if (this.searchResults.length === 0) {
|
||||||
|
this.showSnackbar('没有找到匹配的内容', 'info');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showSnackbar(response.data.message || '搜索失败', 'error');
|
||||||
|
this.searchResults = [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error searching knowledge base:', error);
|
||||||
|
this.showSnackbar('搜索知识库失败', 'error');
|
||||||
|
this.searchResults = [];
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.searching = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showSnackbar(text, color = 'success') {
|
||||||
|
this.snackbar.text = text;
|
||||||
|
this.snackbar.color = color;
|
||||||
|
this.snackbar.show = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectEmoji(emoji) {
|
||||||
|
this.newKB.emoji = emoji;
|
||||||
|
this.showEmojiPicker = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDelete(kb) {
|
||||||
|
this.deleteTarget = kb;
|
||||||
|
this.showDeleteDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteKnowledgeBase() {
|
||||||
|
if (!this.deleteTarget.collection_name) {
|
||||||
|
this.showSnackbar('删除目标不存在', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deleting = true;
|
||||||
|
|
||||||
|
axios.get('/api/plug/alkaid/kb/collection/delete', {
|
||||||
|
params: {
|
||||||
|
collection_name: this.deleteTarget.collection_name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'ok') {
|
||||||
|
this.showSnackbar('知识库删除成功');
|
||||||
|
this.getKBCollections(); // 刷新列表
|
||||||
|
this.showDeleteDialog = false;
|
||||||
|
} else {
|
||||||
|
this.showSnackbar(response.data.message || '删除失败', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting knowledge base:', error);
|
||||||
|
this.showSnackbar('删除知识库失败', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.deleting = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getEmbeddingProviderList() {
|
||||||
|
axios.get('/api/config/provider/list', {
|
||||||
|
params: {
|
||||||
|
provider_type: 'embedding'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'ok') {
|
||||||
|
this.embeddingProviderConfigs = response.data.data || [];
|
||||||
|
} else {
|
||||||
|
this.showSnackbar(response.data.message || '获取嵌入模型列表失败', 'error');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching embedding providers:', error);
|
||||||
|
this.showSnackbar('获取嵌入模型列表失败', 'error');
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
openUrl(url) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.kb-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-card {
|
||||||
|
height: 280px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--v-theme-background);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-spine {
|
||||||
|
width: 12px;
|
||||||
|
background-color: #5c6bc0;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px 0 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(145deg, #f5f7fa 0%, #e4e8f0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-container {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--v-theme-background);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-emoji {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-count {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-picker {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-item {
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
#emoji-display {
|
||||||
|
font-size: 64px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#emoji-display:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-sm {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone {
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone:hover {
|
||||||
|
border-color: #5c6bc0;
|
||||||
|
background-color: rgba(92, 107, 192, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-card {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-content {
|
||||||
|
white-space: pre-line;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-actions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kb-card:hover .kb-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chunk-settings-card {
|
||||||
|
border: 1px solid rgba(92, 107, 192, 0.2) !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chunk-settings-card:hover {
|
||||||
|
border-color: rgba(92, 107, 192, 0.4) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chunk-field :deep(.v-field__input) {
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chunk-field :deep(.v-field__prepend-inner) {
|
||||||
|
padding-right: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chunk-field:focus-within :deep(.v-field__prepend-inner) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
581
dashboard/src/views/alkaid/LongTermMemory.vue
Normal file
581
dashboard/src/views/alkaid/LongTermMemory.vue
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
<template>
|
||||||
|
<div id="long-term-memory" class="flex-grow-1" style="display: flex; flex-direction: row; ">
|
||||||
|
<!-- <div id="graph-container"
|
||||||
|
style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px; max-height: calc(100% - 40px);">
|
||||||
|
</div> -->
|
||||||
|
<div id="graph-container-nonono"
|
||||||
|
style="display: flex; justify-content: center; align-items: center; width: 100%; font-weight: 1000; font-size: 24px;">
|
||||||
|
加速开发中...
|
||||||
|
</div>
|
||||||
|
<div id="graph-control-panel"
|
||||||
|
style="min-width: 450px; border: 1px solid #eee; border-radius: 8px; padding: 16px; padding-bottom: 0px; margin-left: 16px; max-height: calc(100% - 40px);">
|
||||||
|
<div>
|
||||||
|
<!-- <span style="color: #333333;">可视化</span> -->
|
||||||
|
<h3>筛选</h3>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<v-autocomplete v-model="searchUserId" density="compact" :items="userIdList" variant="outlined"
|
||||||
|
label="筛选用户 ID"></v-autocomplete>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<v-btn color="primary" @click="onNodeSelect" variant="tonal">
|
||||||
|
<v-icon start>mdi-magnify</v-icon>
|
||||||
|
筛选
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="secondary" @click="resetFilter" variant="tonal">
|
||||||
|
<v-icon start>mdi-filter-remove</v-icon>
|
||||||
|
重置筛选
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="primary" @click="refreshGraph" variant="tonal">
|
||||||
|
<v-icon start>mdi-refresh</v-icon>
|
||||||
|
刷新图形
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增搜索记忆功能 -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3>搜索记忆</h3>
|
||||||
|
<v-card variant="outlined" class="mt-2 pa-3">
|
||||||
|
<div>
|
||||||
|
<v-text-field v-model="searchMemoryUserId" label="用户 ID" variant="outlined" density="compact" hide-details
|
||||||
|
class="mb-2"></v-text-field>
|
||||||
|
<v-text-field v-model="searchQuery" label="输入关键词" variant="outlined" density="compact" hide-details
|
||||||
|
@keyup.enter="searchMemory" class="mb-2"></v-text-field>
|
||||||
|
<v-btn color="info" @click="searchMemory" :loading="isSearching" variant="tonal">
|
||||||
|
<v-icon start>mdi-text-search</v-icon>
|
||||||
|
搜索
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增搜索结果展示区域 -->
|
||||||
|
<div v-if="searchResults.length > 0" class="mt-3">
|
||||||
|
<v-divider class="mb-3"></v-divider>
|
||||||
|
<div class="text-subtitle-1 mb-2">搜索结果 ({{ searchResults.length }})</div>
|
||||||
|
<v-expansion-panels variant="accordion">
|
||||||
|
<v-expansion-panel v-for="(result, index) in searchResults" :key="index">
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
<div>
|
||||||
|
<span class="text-truncate d-inline-block" style="max-width: 300px;">{{ result.text.substring(0, 30)
|
||||||
|
}}...</span>
|
||||||
|
<span class="ms-2 text-caption text-grey">(相关度: {{ (result.score * 100).toFixed(1) }}%)</span>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-body-1">{{ result.text }}</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<span class="text-caption text-grey">文档ID: {{ result.doc_id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hasSearched" class="mt-3 text-center text-body-1 text-grey">
|
||||||
|
未找到相关记忆内容
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增添加记忆数据的表单 -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3>添加记忆数据</h3>
|
||||||
|
<v-card variant="outlined" class="mt-2 pa-3">
|
||||||
|
<v-form @submit.prevent="addMemoryData">
|
||||||
|
<v-textarea v-model="newMemoryText" label="输入文本内容" variant="outlined" rows="4" hide-details
|
||||||
|
class="mb-2"></v-textarea>
|
||||||
|
|
||||||
|
<v-text-field v-model="newMemoryUserId" label="用户 ID" variant="outlined" density="compact"
|
||||||
|
hide-details></v-text-field>
|
||||||
|
|
||||||
|
<v-switch v-model="needSummarize" color="primary" label="需要摘要" hide-details></v-switch>
|
||||||
|
|
||||||
|
<v-btn color="success" type="submit" :loading="isSubmitting" :disabled="!newMemoryText || !newMemoryUserId">
|
||||||
|
<v-icon start>mdi-plus</v-icon>
|
||||||
|
添加数据
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedNode" class="mt-4">
|
||||||
|
<h3>节点详情</h3>
|
||||||
|
<v-card variant="outlined" class="mt-2 pa-3">
|
||||||
|
<div v-if="selectedNode.id">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">ID:</span>
|
||||||
|
<span>{{ selectedNode.id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode._label">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">类型:</span>
|
||||||
|
<span>{{ selectedNode._label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode.name">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">名称:</span>
|
||||||
|
<span>{{ selectedNode.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode.user_id">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">用户ID:</span>
|
||||||
|
<span>{{ selectedNode.user_id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode.ts">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">时间戳:</span>
|
||||||
|
<span>{{ selectedNode.ts }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedNode.type">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">类型:</span>
|
||||||
|
<span>{{ selectedNode.type }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="graphStats" class="mt-4">
|
||||||
|
<h3>图形统计</h3>
|
||||||
|
<v-card variant="outlined" class="mt-2 pa-3">
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">节点数:</span>
|
||||||
|
<span>{{ graphStats.nodeCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-space-between">
|
||||||
|
<span class="text-subtitle-2">边数:</span>
|
||||||
|
<span>{{ graphStats.edgeCount }}</span>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as d3 from "d3"; // npm install d3
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LongTermMemory',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
simulation: null,
|
||||||
|
svg: null,
|
||||||
|
zoom: null,
|
||||||
|
node_data: [],
|
||||||
|
edge_data: [],
|
||||||
|
nodes: [],
|
||||||
|
links: [],
|
||||||
|
searchUserId: null,
|
||||||
|
userIdList: [],
|
||||||
|
selectedNode: null,
|
||||||
|
graphStats: null,
|
||||||
|
nodeColors: {
|
||||||
|
'PhaseNode': '#4CAF50', // 绿色
|
||||||
|
'PassageNode': '#2196F3', // 蓝色
|
||||||
|
'FactNode': '#FF9800', // 橙色
|
||||||
|
'default': '#9C27B0' // 紫色作为默认
|
||||||
|
},
|
||||||
|
edgeColors: {
|
||||||
|
'_include_': '#607D8B',
|
||||||
|
'_related_': '#9E9E9E',
|
||||||
|
'default': '#BDBDBD'
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
// 添加新的数据属性
|
||||||
|
newMemoryText: '',
|
||||||
|
newMemoryUserId: null,
|
||||||
|
needSummarize: false,
|
||||||
|
isSubmitting: false,
|
||||||
|
// 搜索记忆相关属性
|
||||||
|
searchMemoryUserId: null,
|
||||||
|
searchQuery: '',
|
||||||
|
isSearching: false,
|
||||||
|
searchResults: [],
|
||||||
|
hasSearched: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initD3Graph();
|
||||||
|
this.ltmGetGraph();
|
||||||
|
this.ltmGetUserIds();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.simulation) {
|
||||||
|
this.simulation.stop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 添加搜索记忆方法
|
||||||
|
searchMemory() {
|
||||||
|
if (!this.searchQuery.trim()) {
|
||||||
|
this.$toast.warning('请输入搜索关键词');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSearching = true;
|
||||||
|
this.hasSearched = true;
|
||||||
|
this.searchResults = [];
|
||||||
|
|
||||||
|
// 构建查询参数
|
||||||
|
const params = {
|
||||||
|
query: this.searchQuery
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果有选择用户ID,也加入查询参数
|
||||||
|
if (this.searchMemoryUserId) {
|
||||||
|
params.user_id = this.searchMemoryUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.get('/api/plug/alkaid/ltm/graph/search', { params })
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'ok') {
|
||||||
|
const data = response.data.data;
|
||||||
|
|
||||||
|
// 处理返回的文档数组
|
||||||
|
this.searchResults = Object.keys(data).map(doc_id => {
|
||||||
|
return {
|
||||||
|
doc_id: doc_id,
|
||||||
|
text: data[doc_id].text || '无文本内容',
|
||||||
|
score: data[doc_id].score || 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.searchResults.length === 0) {
|
||||||
|
this.$toast.info('未找到相关记忆内容');
|
||||||
|
} else {
|
||||||
|
this.$toast.success(`找到 ${this.searchResults.length} 条相关记忆`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$toast.error('搜索失败: ' + response.data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('搜索记忆数据失败:', error);
|
||||||
|
this.$toast.error('搜索失败: ' + (error.response?.data?.message || error.message));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isSearching = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加新方法,用于提交记忆数据
|
||||||
|
addMemoryData() {
|
||||||
|
if (!this.newMemoryText || !this.newMemoryUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSubmitting = true;
|
||||||
|
|
||||||
|
// 准备提交数据
|
||||||
|
const payload = {
|
||||||
|
text: this.newMemoryText,
|
||||||
|
user_id: this.newMemoryUserId,
|
||||||
|
need_summarize: this.needSummarize
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.post('/api/plug/alkaid/ltm/graph/add', payload)
|
||||||
|
.then(response => {
|
||||||
|
// 成功添加后刷新图表
|
||||||
|
this.refreshGraph();
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
// this.newMemoryText = '';
|
||||||
|
// this.needSummarize = false;
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
this.$toast.success('记忆数据添加成功!');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('添加记忆数据失败:', error);
|
||||||
|
this.$toast.error('添加记忆数据失败: ' + (error.response?.data?.message || error.message));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
ltmGetGraph(userId = null) {
|
||||||
|
this.isLoading = true;
|
||||||
|
const params = userId ? { user_id: userId } : {};
|
||||||
|
|
||||||
|
axios.get('/api/plug/alkaid/ltm/graph', { params })
|
||||||
|
.then(response => {
|
||||||
|
let nodesRaw = response.data.data.nodes;
|
||||||
|
let edgesRaw = response.data.data.edges;
|
||||||
|
|
||||||
|
this.node_data = nodesRaw;
|
||||||
|
this.edge_data = edgesRaw;
|
||||||
|
|
||||||
|
// 转换为D3所需的数据格式
|
||||||
|
this.nodes = nodesRaw.map(node => {
|
||||||
|
const nodeId = node[0];
|
||||||
|
const nodeData = node[1];
|
||||||
|
const nodeType = nodeData._label || 'default';
|
||||||
|
const color = this.nodeColors[nodeType] || this.nodeColors['default'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: nodeId,
|
||||||
|
label: nodeData.name || nodeId.split('_')[0],
|
||||||
|
color: color,
|
||||||
|
originalData: nodeData
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.links = edgesRaw.map(edge => {
|
||||||
|
const sourceId = edge[0];
|
||||||
|
const targetId = edge[1];
|
||||||
|
const edgeData = edge[2];
|
||||||
|
const relationType = edgeData.relation_type || 'default';
|
||||||
|
const color = this.edgeColors[relationType] || this.edgeColors['default'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
color: color,
|
||||||
|
originalData: edgeData,
|
||||||
|
label: relationType
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateD3Graph();
|
||||||
|
this.updateGraphStats();
|
||||||
|
console.log('Graph initialized with', this.nodes.length, 'nodes and', this.links.length, 'links');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching graph data:', error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
ltmGetUserIds() {
|
||||||
|
axios.get('/api/plug/alkaid/ltm/user_ids')
|
||||||
|
.then(response => {
|
||||||
|
this.userIdList = response.data.data;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching user IDs:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateGraphStats() {
|
||||||
|
this.graphStats = {
|
||||||
|
nodeCount: this.nodes.length,
|
||||||
|
edgeCount: this.links.length
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshGraph() {
|
||||||
|
this.ltmGetGraph(this.searchUserId);
|
||||||
|
},
|
||||||
|
|
||||||
|
onNodeSelect() {
|
||||||
|
console.log('Selected user ID:', this.searchUserId);
|
||||||
|
if (!this.searchUserId) return;
|
||||||
|
|
||||||
|
// 使用API的user_id参数筛选数据
|
||||||
|
this.ltmGetGraph(this.searchUserId);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetFilter() {
|
||||||
|
this.searchUserId = null;
|
||||||
|
this.searchQuery = ''; // 重置搜索关键词
|
||||||
|
this.searchResults = []; // 清空搜索结果
|
||||||
|
this.hasSearched = false; // 重置搜索状态
|
||||||
|
this.ltmGetGraph();
|
||||||
|
},
|
||||||
|
|
||||||
|
initD3Graph() {
|
||||||
|
const container = document.getElementById("graph-container");
|
||||||
|
if (!container) return;
|
||||||
|
d3.select("#graph-container svg").remove();
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const height = container.clientHeight;
|
||||||
|
const svg = d3.select("#graph-container")
|
||||||
|
.append("svg")
|
||||||
|
.attr("width", "100%")
|
||||||
|
.attr("height", "100%")
|
||||||
|
.attr("viewBox", [0, 0, width, height])
|
||||||
|
.classed("d3-graph", true);
|
||||||
|
const g = svg.append("g");
|
||||||
|
const zoom = d3.zoom()
|
||||||
|
.scaleExtent([0.1, 10])
|
||||||
|
.on("zoom", (event) => {
|
||||||
|
g.attr("transform", event.transform);
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.call(zoom);
|
||||||
|
const simulation = d3.forceSimulation()
|
||||||
|
.force("link", d3.forceLink().id(d => d.id).distance(100))
|
||||||
|
.force("charge", d3.forceManyBody().strength(-300))
|
||||||
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||||
|
.force("collision", d3.forceCollide().radius(30));
|
||||||
|
|
||||||
|
this.svg = svg;
|
||||||
|
this.g = g;
|
||||||
|
this.zoom = zoom;
|
||||||
|
this.simulation = simulation;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateD3Graph() {
|
||||||
|
if (!this.svg || !this.simulation) return;
|
||||||
|
const g = this.g;
|
||||||
|
g.selectAll("*").remove();
|
||||||
|
g.append("defs").append("marker")
|
||||||
|
.attr("id", "arrowhead")
|
||||||
|
.attr("viewBox", "0 -5 10 10")
|
||||||
|
.attr("refX", 20)
|
||||||
|
.attr("refY", 0)
|
||||||
|
.attr("orient", "auto")
|
||||||
|
.attr("markerWidth", 6)
|
||||||
|
.attr("markerHeight", 6)
|
||||||
|
.append("path")
|
||||||
|
.attr("d", "M0,-5L10,0L0,5")
|
||||||
|
.attr("fill", "#999");
|
||||||
|
const link = g.append("g")
|
||||||
|
.selectAll("line")
|
||||||
|
.data(this.links)
|
||||||
|
.join("line")
|
||||||
|
.attr("stroke", d => d.color)
|
||||||
|
.attr("stroke-width", 1.5)
|
||||||
|
.attr("marker-end", "url(#arrowhead)");
|
||||||
|
const edgeLabels = g.append("g")
|
||||||
|
.selectAll("text")
|
||||||
|
.data(this.links)
|
||||||
|
.join("text")
|
||||||
|
.text(d => d.label)
|
||||||
|
.attr("font-size", "8px")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("fill", "#666")
|
||||||
|
.attr("dy", -5);
|
||||||
|
const node = g.append("g")
|
||||||
|
.selectAll("circle")
|
||||||
|
.data(this.nodes)
|
||||||
|
.join("circle")
|
||||||
|
.attr("r", 8)
|
||||||
|
.attr("fill", d => d.color)
|
||||||
|
.style("cursor", "pointer")
|
||||||
|
.call(this.dragBehavior());
|
||||||
|
const nodeLabels = g.append("g")
|
||||||
|
.selectAll("text")
|
||||||
|
.data(this.nodes)
|
||||||
|
.join("text")
|
||||||
|
.text(d => d.label)
|
||||||
|
.attr("font-size", "10px")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("fill", "#333")
|
||||||
|
.attr("dy", -12);
|
||||||
|
node.on("click", (event, d) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.selectedNode = d.originalData;
|
||||||
|
});
|
||||||
|
this.svg.on("click", () => {
|
||||||
|
this.selectedNode = null;
|
||||||
|
});
|
||||||
|
this.simulation
|
||||||
|
.nodes(this.nodes)
|
||||||
|
.on("tick", () => {
|
||||||
|
link
|
||||||
|
.attr("x1", d => d.source.x)
|
||||||
|
.attr("y1", d => d.source.y)
|
||||||
|
.attr("x2", d => d.target.x)
|
||||||
|
.attr("y2", d => d.target.y);
|
||||||
|
edgeLabels
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
node
|
||||||
|
.attr("cx", d => d.x)
|
||||||
|
.attr("cy", d => d.y);
|
||||||
|
nodeLabels
|
||||||
|
.attr("x", d => d.x)
|
||||||
|
.attr("y", d => d.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.simulation.force("link")
|
||||||
|
.links(this.links);
|
||||||
|
|
||||||
|
this.simulation.alpha(1).restart();
|
||||||
|
},
|
||||||
|
|
||||||
|
dragBehavior() {
|
||||||
|
return d3.drag()
|
||||||
|
.on("start", (event, d) => {
|
||||||
|
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
||||||
|
d.fx = d.x;
|
||||||
|
d.fy = d.y;
|
||||||
|
})
|
||||||
|
.on("drag", (event, d) => {
|
||||||
|
d.fx = event.x;
|
||||||
|
d.fy = event.y;
|
||||||
|
})
|
||||||
|
.on("end", (event, d) => {
|
||||||
|
if (!event.active) this.simulation.alphaTarget(0);
|
||||||
|
d.fx = null;
|
||||||
|
d.fy = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRandomColor() {
|
||||||
|
const letters = '0123456789ABCDEF';
|
||||||
|
let color = '#';
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
color += letters[Math.floor(Math.random() * 16)];
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#long-term-memory {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-container {
|
||||||
|
position: relative;
|
||||||
|
background-color: #f2f6f9;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-control-panel {
|
||||||
|
overflow-y: auto;
|
||||||
|
/* 让控制面板可滚动而不是整个页面滚动 */
|
||||||
|
min-width: 450px;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-container:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-header {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-container svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-graph {
|
||||||
|
background-color: #f2f6f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
15
dashboard/src/views/alkaid/Other.vue
Normal file
15
dashboard/src/views/alkaid/Other.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-grow-1" style="display: flex; flex-direction: column; height: 100%;">
|
||||||
|
<div class="d-flex align-center justify-center"
|
||||||
|
style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px;">
|
||||||
|
<span size="64">🌍</span>
|
||||||
|
<p class="text-h6 text-grey ml-4">前面的世界,以后再来探索吧!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'OtherFeatures'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,23 +1,154 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AuthLogin from '../authForms/AuthLogin.vue';
|
import AuthLogin from '../authForms/AuthLogin.vue';
|
||||||
|
import Logo from '@/components/shared/Logo.vue';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import {useCustomizerStore} from "@/stores/customizer";
|
||||||
|
|
||||||
|
const cardVisible = ref(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查用户是否已登录,如果已登录则重定向
|
||||||
|
if (authStore.has_token()) {
|
||||||
|
router.push(authStore.returnUrl || '/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加一个小延迟以获得更好的动画效果
|
||||||
|
setTimeout(() => {
|
||||||
|
cardVisible.value = true;
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="display: flex; justify-content: center; flex-direction: column; align-items: center; height: 100vh; background-color: aliceblue;">
|
<div v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="login-page-container">
|
||||||
<v-card variant="outlined" style="max-width: 500px; box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);">
|
<div class="login-background"></div>
|
||||||
<v-card-text class="pa-9">
|
<v-card
|
||||||
<div class="text-center">
|
variant="outlined"
|
||||||
|
class="login-card"
|
||||||
|
:class="{ 'card-visible': cardVisible }"
|
||||||
|
>
|
||||||
|
<v-card-text class="pa-10">
|
||||||
|
<div class="logo-wrapper">
|
||||||
<Logo />
|
<Logo />
|
||||||
<h2 class="text-secondary text-h2 mt-4">AstrBot 仪表盘</h2>
|
</div>
|
||||||
<h4 class="text-disabled text-h4 mt-3">登录以继续</h4>
|
<div class="divider-container">
|
||||||
|
<v-divider class="custom-divider"></v-divider>
|
||||||
|
</div>
|
||||||
|
<AuthLogin />
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
<div v-else class="login-page-container-dark">
|
||||||
|
<div class="login-background-dark"></div>
|
||||||
|
<v-card
|
||||||
|
variant="outlined"
|
||||||
|
class="login-card"
|
||||||
|
:class="{ 'card-visible': cardVisible }"
|
||||||
|
>
|
||||||
|
<v-card-text class="pa-10">
|
||||||
|
<div class="logo-wrapper">
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
<div class="divider-container">
|
||||||
|
<v-divider class="custom-divider"></v-divider>
|
||||||
</div>
|
</div>
|
||||||
<AuthLogin />
|
<AuthLogin />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.login-page-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, #ebf5fd 0%, #e0e9f8 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page-container-dark {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, #1a1b1c 0%, #1d1e21 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-background {
|
||||||
|
position: absolute;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
background: radial-gradient(circle, rgba(94, 53, 177, 0.03) 0%, rgba(30, 136, 229, 0.06) 70%);
|
||||||
|
z-index: 0;
|
||||||
|
animation: rotate 60s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-background-dark {
|
||||||
|
position: absolute;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
background-color: var(--v-theme-surface);
|
||||||
|
z-index: 0;
|
||||||
|
animation: rotate 60s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
max-width: 520px;
|
||||||
|
width: 90%;
|
||||||
|
color: var(--v-theme-primaryText) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
border-color: var(--v-theme-border) !important;
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.07) !important;
|
||||||
|
background-color: var(--v-theme-surface) !important;
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&.card-visible {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-wrapper {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-container {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-divider {
|
||||||
|
border-color: rgba(0, 0, 0, 0.05) !important;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.loginBox {
|
.loginBox {
|
||||||
max-width: 475px;
|
max-width: 475px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import {ref, useCssModule} from 'vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { Form } from 'vee-validate';
|
import { Form } from 'vee-validate';
|
||||||
import md5 from 'js-md5';
|
import md5 from 'js-md5';
|
||||||
|
import {useCustomizerStore} from "@/stores/customizer";
|
||||||
|
|
||||||
const valid = ref(false);
|
const valid = ref(false);
|
||||||
const show1 = ref(false);
|
const show1 = ref(false);
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
async function validate(values: any, { setErrors }: any) {
|
async function validate(values: any, { setErrors }: any) {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
// md5加密
|
// md5加密
|
||||||
let password_ = password.value;
|
let password_ = password.value;
|
||||||
if (password.value != '') {
|
if (password.value != '') {
|
||||||
@@ -21,67 +25,154 @@ async function validate(values: any, { setErrors }: any) {
|
|||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
return authStore.login(username.value, password_).then((res) => {
|
return authStore.login(username.value, password_).then((res) => {
|
||||||
console.log(res);
|
console.log(res);
|
||||||
|
loading.value = false;
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
setErrors({ apiError: err });
|
setErrors({ apiError: err });
|
||||||
|
loading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Form @submit="validate" class="mt-7 loginForm" v-slot="{ errors, isSubmitting }">
|
<Form @submit="validate" class="mt-4 login-form" v-slot="{ errors, isSubmitting }">
|
||||||
<v-text-field v-model="username" label="用户名" class="mt-4 mb-8" required density="comfortable"
|
<v-text-field
|
||||||
hide-details="auto" variant="outlined" color="primary"></v-text-field>
|
v-model="username"
|
||||||
<v-text-field v-model="password" label="密码" required density="comfortable" variant="outlined"
|
label="用户名"
|
||||||
color="primary" hide-details="auto" :append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'"
|
class="mb-6 input-field"
|
||||||
:type="show1 ? 'text' : 'password'" @click:append="show1 = !show1" class="pwdInput"></v-text-field>
|
required
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
variant="outlined"
|
||||||
|
:style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff'}"
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
|
:disabled="loading"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
<small>默认用户名和密码为 astrbot。</small>
|
<v-text-field
|
||||||
<v-btn color="secondary" :loading="isSubmitting" block class="mt-8" variant="flat" size="large" :disabled="valid"
|
v-model="password"
|
||||||
type="submit">
|
label="密码"
|
||||||
登录</v-btn>
|
required
|
||||||
<div v-if="errors.apiError" class="mt-2">
|
density="comfortable"
|
||||||
<v-alert color="error">{{ errors.apiError }}</v-alert>
|
variant="outlined"
|
||||||
|
:style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff'}"
|
||||||
|
hide-details="auto"
|
||||||
|
:append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'"
|
||||||
|
:type="show1 ? 'text' : 'password'"
|
||||||
|
@click:append="show1 = !show1"
|
||||||
|
class="pwd-input"
|
||||||
|
prepend-inner-icon="mdi-lock"
|
||||||
|
:disabled="loading"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-label :style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000aa' : '#ffffffcc'}" class="mt-1 mb-5">
|
||||||
|
<small>默认用户名和密码为 astrbot</small>
|
||||||
|
</v-label>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="secondary"
|
||||||
|
:loading="isSubmitting || loading"
|
||||||
|
block
|
||||||
|
class="login-btn"
|
||||||
|
variant="flat"
|
||||||
|
size="large"
|
||||||
|
:disabled="valid"
|
||||||
|
type="submit"
|
||||||
|
elevation="2"
|
||||||
|
>
|
||||||
|
<span class="login-btn-text">登录</span>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<div v-if="errors.apiError" class="mt-4 error-container">
|
||||||
|
<v-alert
|
||||||
|
color="error"
|
||||||
|
variant="tonal"
|
||||||
|
density="comfortable"
|
||||||
|
icon="mdi-alert-circle"
|
||||||
|
border="start"
|
||||||
|
>
|
||||||
|
{{ errors.apiError }}
|
||||||
|
</v-alert>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.custom-devider {
|
.login-form {
|
||||||
border-color: rgba(0, 0, 0, 0.08) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.googleBtn {
|
|
||||||
border-color: rgba(0, 0, 0, 0.08);
|
|
||||||
margin: 30px 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.outlinedInput .v-field {
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.orbtn {
|
|
||||||
padding: 2px 40px;
|
|
||||||
border-color: rgba(0, 0, 0, 0.08);
|
|
||||||
margin: 20px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwdInput {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.v-input__append {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loginForm {
|
|
||||||
.v-text-field .v-field--active input {
|
.v-text-field .v-field--active input {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-field, .pwd-input {
|
||||||
|
.v-field__field {
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-field__outline {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .v-field__outline {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-field--focused .v-field__outline {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-field__prepend-inner {
|
||||||
|
padding-right: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwd-input {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.v-input__append {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
opacity: 0.7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
margin-top: 12px;
|
||||||
|
height: 48px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(94, 53, 177, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn-text {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
color: var(--v-theme-secondaryText);
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
.v-alert {
|
||||||
|
border-left-width: 4px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-divider {
|
||||||
|
border-color: rgba(0, 0, 0, 0.08) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: #f9fafc;
|
background-color: var(--v-theme-background);
|
||||||
min-height: calc(100vh - 64px);
|
min-height: calc(100vh - 64px);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
@@ -170,13 +170,13 @@ export default {
|
|||||||
.dashboard-title {
|
.dashboard-title {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-subtitle {
|
.dashboard-subtitle {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-row {
|
.notice-row {
|
||||||
@@ -194,18 +194,18 @@ export default {
|
|||||||
|
|
||||||
.plugin-card {
|
.plugin-card {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: white;
|
background-color: var(--v-theme-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-title {
|
.plugin-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-subtitle {
|
.plugin-subtitle {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ export default {
|
|||||||
|
|
||||||
.plugin-version {
|
.plugin-version {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText, #666);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-footer {
|
.dashboard-footer {
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
borderColor: '#f1f1f1',
|
borderColor: "gray100",
|
||||||
row: {
|
row: {
|
||||||
colors: ['transparent', 'transparent'],
|
colors: ['transparent', 'transparent'],
|
||||||
opacity: 0.2
|
opacity: 0.2
|
||||||
@@ -293,12 +293,12 @@ export default {
|
|||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-subtitle {
|
.chart-subtitle {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,31 +315,31 @@ export default {
|
|||||||
|
|
||||||
.stat-box {
|
.stat-box {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: #f5f5f5;
|
background: var(--v-theme-surface);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-number {
|
.stat-number {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-up .stat-number {
|
.trend-up .stat-number {
|
||||||
color: #4caf50;
|
color: var(--v-theme-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-down .stat-number {
|
.trend-down .stat-number {
|
||||||
color: #f44336;
|
color: var(--v-theme-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
@@ -354,7 +354,7 @@ export default {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: var(--v-theme-overlay);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -365,6 +365,6 @@ export default {
|
|||||||
.loading-text {
|
.loading-text {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -132,12 +132,12 @@ export default {
|
|||||||
.platform-title {
|
.platform-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-subtitle {
|
.platform-subtitle {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,16 +171,16 @@ export default {
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #f0f0f0;
|
background-color: var(--v-theme-surface);
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-rank {
|
.top-rank {
|
||||||
background-color: #5e35b1;
|
background-color: var(--v-theme-secondary);
|
||||||
color: white;
|
color: var(--v-theme-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-name {
|
.platform-name {
|
||||||
@@ -195,19 +195,19 @@ export default {
|
|||||||
.count-value {
|
.count-value {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #5e35b1;
|
color: var(--v-theme-secondary);
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.count-label {
|
.count-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-stats-summary {
|
.platform-stats-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--v-theme-containerBg);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -220,13 +220,13 @@ export default {
|
|||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: var(--v-theme-secondaryText);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--v-theme-primaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-chart {
|
.platform-chart {
|
||||||
@@ -246,7 +246,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.no-data-text {
|
.no-data-text {
|
||||||
color: #999;
|
color: var(--v-theme-secondaryText);
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class RstScene(Enum):
|
|||||||
version="4.0.0",
|
version="4.0.0",
|
||||||
)
|
)
|
||||||
class Main(star.Star):
|
class Main(star.Star):
|
||||||
|
|
||||||
def __init__(self, context: star.Context) -> None:
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
cfg = context.get_config()
|
cfg = context.get_config()
|
||||||
@@ -140,7 +139,7 @@ class Main(star.Star):
|
|||||||
{notice}"""
|
{notice}"""
|
||||||
|
|
||||||
event.set_result(MessageEventResult().message(msg).use_t2i(False))
|
event.set_result(MessageEventResult().message(msg).use_t2i(False))
|
||||||
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("llm")
|
@filter.command("llm")
|
||||||
async def llm(self, event: AstrMessageEvent):
|
async def llm(self, event: AstrMessageEvent):
|
||||||
"""开启/关闭 LLM"""
|
"""开启/关闭 LLM"""
|
||||||
@@ -216,9 +215,7 @@ class Main(star.Star):
|
|||||||
"""获取已经安装的插件列表。"""
|
"""获取已经安装的插件列表。"""
|
||||||
plugin_list_info = "已加载的插件:\n"
|
plugin_list_info = "已加载的插件:\n"
|
||||||
for plugin in self.context.get_all_stars():
|
for plugin in self.context.get_all_stars():
|
||||||
plugin_list_info += (
|
plugin_list_info += f"- `{plugin.name}` By {plugin.author}: {plugin.desc}"
|
||||||
f"- `{plugin.name}` By {plugin.author}: {plugin.desc}"
|
|
||||||
)
|
|
||||||
if not plugin.activated:
|
if not plugin.activated:
|
||||||
plugin_list_info += " (未启用)"
|
plugin_list_info += " (未启用)"
|
||||||
plugin_list_info += "\n"
|
plugin_list_info += "\n"
|
||||||
@@ -271,9 +268,7 @@ class Main(star.Star):
|
|||||||
event.set_result(MessageEventResult().message("安装插件成功。"))
|
event.set_result(MessageEventResult().message("安装插件成功。"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"安装插件失败: {e}")
|
logger.error(f"安装插件失败: {e}")
|
||||||
event.set_result(
|
event.set_result(MessageEventResult().message(f"安装插件失败: {e}"))
|
||||||
MessageEventResult().message(f"安装插件失败: {e}")
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@plugin.command("help")
|
@plugin.command("help")
|
||||||
@@ -319,7 +314,6 @@ class Main(star.Star):
|
|||||||
ret += "更多帮助信息请查看插件仓库 README。"
|
ret += "更多帮助信息请查看插件仓库 README。"
|
||||||
event.set_result(MessageEventResult().message(ret).use_t2i(False))
|
event.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||||
|
|
||||||
|
|
||||||
@filter.command("t2i")
|
@filter.command("t2i")
|
||||||
async def t2i(self, event: AstrMessageEvent):
|
async def t2i(self, event: AstrMessageEvent):
|
||||||
"""开关文本转图片"""
|
"""开关文本转图片"""
|
||||||
@@ -420,24 +414,20 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
||||||
|
|
||||||
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("provider")
|
@filter.command("provider")
|
||||||
async def provider(
|
async def provider(
|
||||||
self, event: AstrMessageEvent, idx: Union[str, int] = None, idx2: int = None
|
self, event: AstrMessageEvent, idx: Union[str, int] = None, idx2: int = None
|
||||||
):
|
):
|
||||||
"""查看或者切换 LLM Provider"""
|
"""查看或者切换 LLM Provider"""
|
||||||
|
|
||||||
if not self.context.get_using_provider():
|
|
||||||
event.set_result(
|
|
||||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。")
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if idx is None:
|
if idx is None:
|
||||||
ret = "## 载入的 LLM 提供商\n"
|
ret = "## 载入的 LLM 提供商\n"
|
||||||
for idx, llm in enumerate(self.context.get_all_providers()):
|
for idx, llm in enumerate(self.context.get_all_providers()):
|
||||||
id_ = llm.meta().id
|
id_ = llm.meta().id
|
||||||
ret += f"{idx + 1}. {id_} ({llm.meta().model})"
|
ret += f"{idx + 1}. {id_} ({llm.meta().model})"
|
||||||
if self.context.get_using_provider().meta().id == id_:
|
provider_using = self.context.get_using_provider()
|
||||||
|
if provider_using and provider_using.meta().id == id_:
|
||||||
ret += " (当前使用)"
|
ret += " (当前使用)"
|
||||||
ret += "\n"
|
ret += "\n"
|
||||||
|
|
||||||
@@ -483,8 +473,6 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
id_ = provider.meta().id
|
id_ = provider.meta().id
|
||||||
self.context.provider_manager.curr_tts_provider_inst = provider
|
self.context.provider_manager.curr_tts_provider_inst = provider
|
||||||
sp.put("curr_provider_tts", id_)
|
sp.put("curr_provider_tts", id_)
|
||||||
if not self.context.provider_manager.tts_enabled:
|
|
||||||
self.context.provider_manager.tts_enabled = True
|
|
||||||
event.set_result(
|
event.set_result(
|
||||||
MessageEventResult().message(f"成功切换到 {id_}。")
|
MessageEventResult().message(f"成功切换到 {id_}。")
|
||||||
)
|
)
|
||||||
@@ -499,8 +487,6 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
id_ = provider.meta().id
|
id_ = provider.meta().id
|
||||||
self.context.provider_manager.curr_stt_provider_inst = provider
|
self.context.provider_manager.curr_stt_provider_inst = provider
|
||||||
sp.put("curr_provider_stt", id_)
|
sp.put("curr_provider_stt", id_)
|
||||||
if not self.context.provider_manager.stt_enabled:
|
|
||||||
self.context.provider_manager.stt_enabled = True
|
|
||||||
event.set_result(
|
event.set_result(
|
||||||
MessageEventResult().message(f"成功切换到 {id_}。")
|
MessageEventResult().message(f"成功切换到 {id_}。")
|
||||||
)
|
)
|
||||||
@@ -512,8 +498,6 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
id_ = provider.meta().id
|
id_ = provider.meta().id
|
||||||
self.context.provider_manager.curr_provider_inst = provider
|
self.context.provider_manager.curr_provider_inst = provider
|
||||||
sp.put("curr_provider", id_)
|
sp.put("curr_provider", id_)
|
||||||
if not self.context.provider_manager.provider_enabled:
|
|
||||||
self.context.provider_manager.provider_enabled = True
|
|
||||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||||
else:
|
else:
|
||||||
event.set_result(MessageEventResult().message("无效的参数。"))
|
event.set_result(MessageEventResult().message("无效的参数。"))
|
||||||
@@ -589,6 +573,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
|
|
||||||
message.set_result(MessageEventResult().message(ret))
|
message.set_result(MessageEventResult().message(ret))
|
||||||
|
|
||||||
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("model")
|
@filter.command("model")
|
||||||
async def model_ls(
|
async def model_ls(
|
||||||
self, message: AstrMessageEvent, idx_or_name: Union[int, str] = None
|
self, message: AstrMessageEvent, idx_or_name: Union[int, str] = None
|
||||||
@@ -1033,7 +1018,11 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
message.unified_msg_origin, cid
|
message.unified_msg_origin, cid
|
||||||
)
|
)
|
||||||
if not conversation:
|
if not conversation:
|
||||||
message.set_result(MessageEventResult().message("请先进入一个对话。可以使用 /new 创建。"))
|
message.set_result(
|
||||||
|
MessageEventResult().message(
|
||||||
|
"请先进入一个对话。可以使用 /new 创建。"
|
||||||
|
)
|
||||||
|
)
|
||||||
if not conversation.persona_id and not conversation.persona_id == "[%None]":
|
if not conversation.persona_id and not conversation.persona_id == "[%None]":
|
||||||
curr_persona_name = (
|
curr_persona_name = (
|
||||||
self.context.provider_manager.selected_default_persona["name"]
|
self.context.provider_manager.selected_default_persona["name"]
|
||||||
@@ -1176,7 +1165,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
|||||||
@filter.command("gewe_code")
|
@filter.command("gewe_code")
|
||||||
async def gewe_code(self, event: AstrMessageEvent, code: str):
|
async def gewe_code(self, event: AstrMessageEvent, code: str):
|
||||||
"""保存 gewechat 验证码"""
|
"""保存 gewechat 验证码"""
|
||||||
code_path = os.path.join(get_astrbot_data_path(), "temp","gewe_code")
|
code_path = os.path.join(get_astrbot_data_path(), "temp", "gewe_code")
|
||||||
with open(code_path, "w", encoding="utf-8") as f:
|
with open(code_path, "w", encoding="utf-8") as f:
|
||||||
f.write(code)
|
f.write(code)
|
||||||
yield event.plain_result("验证码已保存。")
|
yield event.plain_result("验证码已保存。")
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
from astrbot.api.event import filter, AstrMessageEvent
|
|
||||||
from astrbot.api.star import Context, Star, register
|
|
||||||
from astrbot.api import logger
|
|
||||||
|
|
||||||
@register("vpet", "AstrBot Team", "虚拟桌宠", "0.0.1")
|
|
||||||
class VPet(Star):
|
|
||||||
def __init__(self, context: Context):
|
|
||||||
super().__init__(context)
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
"""可选择实现异步的插件初始化方法,当实例化该插件类之后会自动调用该方法。"""
|
|
||||||
|
|
||||||
@filter.llm_tool("screenshot")
|
|
||||||
async def screenshot(self, event: AstrMessageEvent):
|
|
||||||
"""Capture the screen and return the image."""
|
|
||||||
|
|
||||||
|
|
||||||
async def terminate(self):
|
|
||||||
"""可选择实现异步的插件销毁方法,当插件被卸载/停用时会调用。"""
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "AstrBot"
|
name = "AstrBot"
|
||||||
version = "3.5.10"
|
version = "3.5.13"
|
||||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -8,6 +8,7 @@ dependencies = [
|
|||||||
"aiocqhttp>=1.4.4",
|
"aiocqhttp>=1.4.4",
|
||||||
"aiodocker>=0.24.0",
|
"aiodocker>=0.24.0",
|
||||||
"aiohttp>=3.11.18",
|
"aiohttp>=3.11.18",
|
||||||
|
"aiosqlite>=0.21.0",
|
||||||
"anthropic>=0.51.0",
|
"anthropic>=0.51.0",
|
||||||
"apscheduler>=3.11.0",
|
"apscheduler>=3.11.0",
|
||||||
"beautifulsoup4>=4.13.4",
|
"beautifulsoup4>=4.13.4",
|
||||||
@@ -19,12 +20,14 @@ dependencies = [
|
|||||||
"defusedxml>=0.7.1",
|
"defusedxml>=0.7.1",
|
||||||
"dingtalk-stream>=0.22.1",
|
"dingtalk-stream>=0.22.1",
|
||||||
"docstring-parser>=0.16",
|
"docstring-parser>=0.16",
|
||||||
|
"faiss-cpu>=1.10.0",
|
||||||
"filelock>=3.18.0",
|
"filelock>=3.18.0",
|
||||||
"google-genai>=1.14.0",
|
"google-genai>=1.14.0",
|
||||||
"googlesearch-python>=1.3.0",
|
"googlesearch-python>=1.3.0",
|
||||||
"lark-oapi>=1.4.15",
|
"lark-oapi>=1.4.15",
|
||||||
"lxml-html-clean>=0.4.2",
|
"lxml-html-clean>=0.4.2",
|
||||||
"mcp>=1.8.0",
|
"mcp>=1.8.0",
|
||||||
|
"nh3>=0.2.21",
|
||||||
"openai>=1.78.0",
|
"openai>=1.78.0",
|
||||||
"ormsgpack>=1.9.1",
|
"ormsgpack>=1.9.1",
|
||||||
"pillow>=11.2.1",
|
"pillow>=11.2.1",
|
||||||
|
|||||||
@@ -35,3 +35,6 @@ click
|
|||||||
filelock
|
filelock
|
||||||
watchfiles
|
watchfiles
|
||||||
websockets
|
websockets
|
||||||
|
faiss-cpu
|
||||||
|
aiosqlite
|
||||||
|
nh3
|
||||||
154
uv.lock
generated
154
uv.lock
generated
@@ -136,6 +136,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosqlite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -192,12 +204,13 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "astrbot"
|
name = "astrbot"
|
||||||
version = "3.5.9"
|
version = "3.5.12"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiocqhttp" },
|
{ name = "aiocqhttp" },
|
||||||
{ name = "aiodocker" },
|
{ name = "aiodocker" },
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
{ name = "aiosqlite" },
|
||||||
{ name = "anthropic" },
|
{ name = "anthropic" },
|
||||||
{ name = "apscheduler" },
|
{ name = "apscheduler" },
|
||||||
{ name = "beautifulsoup4" },
|
{ name = "beautifulsoup4" },
|
||||||
@@ -209,12 +222,14 @@ dependencies = [
|
|||||||
{ name = "defusedxml" },
|
{ name = "defusedxml" },
|
||||||
{ name = "dingtalk-stream" },
|
{ name = "dingtalk-stream" },
|
||||||
{ name = "docstring-parser" },
|
{ name = "docstring-parser" },
|
||||||
|
{ name = "faiss-cpu" },
|
||||||
{ name = "filelock" },
|
{ name = "filelock" },
|
||||||
{ name = "google-genai" },
|
{ name = "google-genai" },
|
||||||
{ name = "googlesearch-python" },
|
{ name = "googlesearch-python" },
|
||||||
{ name = "lark-oapi" },
|
{ name = "lark-oapi" },
|
||||||
{ name = "lxml-html-clean" },
|
{ name = "lxml-html-clean" },
|
||||||
{ name = "mcp" },
|
{ name = "mcp" },
|
||||||
|
{ name = "nh3" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "ormsgpack" },
|
{ name = "ormsgpack" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
@@ -239,6 +254,7 @@ requires-dist = [
|
|||||||
{ name = "aiocqhttp", specifier = ">=1.4.4" },
|
{ name = "aiocqhttp", specifier = ">=1.4.4" },
|
||||||
{ name = "aiodocker", specifier = ">=0.24.0" },
|
{ name = "aiodocker", specifier = ">=0.24.0" },
|
||||||
{ name = "aiohttp", specifier = ">=3.11.18" },
|
{ name = "aiohttp", specifier = ">=3.11.18" },
|
||||||
|
{ name = "aiosqlite", specifier = ">=0.21.0" },
|
||||||
{ name = "anthropic", specifier = ">=0.51.0" },
|
{ name = "anthropic", specifier = ">=0.51.0" },
|
||||||
{ name = "apscheduler", specifier = ">=3.11.0" },
|
{ name = "apscheduler", specifier = ">=3.11.0" },
|
||||||
{ name = "beautifulsoup4", specifier = ">=4.13.4" },
|
{ name = "beautifulsoup4", specifier = ">=4.13.4" },
|
||||||
@@ -250,12 +266,14 @@ requires-dist = [
|
|||||||
{ name = "defusedxml", specifier = ">=0.7.1" },
|
{ name = "defusedxml", specifier = ">=0.7.1" },
|
||||||
{ name = "dingtalk-stream", specifier = ">=0.22.1" },
|
{ name = "dingtalk-stream", specifier = ">=0.22.1" },
|
||||||
{ name = "docstring-parser", specifier = ">=0.16" },
|
{ name = "docstring-parser", specifier = ">=0.16" },
|
||||||
|
{ name = "faiss-cpu", specifier = ">=1.11.0" },
|
||||||
{ name = "filelock", specifier = ">=3.18.0" },
|
{ name = "filelock", specifier = ">=3.18.0" },
|
||||||
{ name = "google-genai", specifier = ">=1.14.0" },
|
{ name = "google-genai", specifier = ">=1.14.0" },
|
||||||
{ name = "googlesearch-python", specifier = ">=1.3.0" },
|
{ name = "googlesearch-python", specifier = ">=1.3.0" },
|
||||||
{ name = "lark-oapi", specifier = ">=1.4.15" },
|
{ name = "lark-oapi", specifier = ">=1.4.15" },
|
||||||
{ name = "lxml-html-clean", specifier = ">=0.4.2" },
|
{ name = "lxml-html-clean", specifier = ">=0.4.2" },
|
||||||
{ name = "mcp", specifier = ">=1.8.0" },
|
{ name = "mcp", specifier = ">=1.8.0" },
|
||||||
|
{ name = "nh3", specifier = ">=0.2.21" },
|
||||||
{ name = "openai", specifier = ">=1.78.0" },
|
{ name = "openai", specifier = ">=1.78.0" },
|
||||||
{ name = "ormsgpack", specifier = ">=1.9.1" },
|
{ name = "ormsgpack", specifier = ">=1.9.1" },
|
||||||
{ name = "pillow", specifier = ">=11.2.1" },
|
{ name = "pillow", specifier = ">=11.2.1" },
|
||||||
@@ -612,6 +630,38 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "faiss-cpu"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/9a/e33fc563f007924dd4ec3c5101fe5320298d6c13c158a24a9ed849058569/faiss_cpu-1.11.0.tar.gz", hash = "sha256:44877b896a2b30a61e35ea4970d008e8822545cb340eca4eff223ac7f40a1db9", size = 70218, upload-time = "2025-04-28T07:48:30.459Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/e5/7490368ec421e44efd60a21aa88d244653c674d8d6ee6bc455d8ee3d02ed/faiss_cpu-1.11.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1995119152928c68096b0c1e5816e3ee5b1eebcf615b80370874523be009d0f6", size = 3307996, upload-time = "2025-04-28T07:47:29.126Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/ac/a94fbbbf4f38c2ad11862af92c071ff346630ebf33f3d36fe75c3817c2f0/faiss_cpu-1.11.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:788d7bf24293fdecc1b93f1414ca5cc62ebd5f2fecfcbb1d77f0e0530621c95d", size = 7886309, upload-time = "2025-04-28T07:47:31.668Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/48/ad79f34f1b9eba58c32399ad4fbedec3f2a717d72fb03648e906aab48a52/faiss_cpu-1.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:73408d52429558f67889581c0c6d206eedcf6fabe308908f2bdcd28fd5e8be4a", size = 3778443, upload-time = "2025-04-28T07:47:33.685Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/67/3c6b94dd3223a8ecaff1c10c11b4ac6f3f13f1ba8ab6b6109c24b6e9b23d/faiss_cpu-1.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1f53513682ca94c76472544fa5f071553e428a1453e0b9755c9673f68de45f12", size = 31295174, upload-time = "2025-04-28T07:47:36.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/2c/d843256aabdb7f20f0f87f61efe3fb7c2c8e7487915f560ba523cfcbab57/faiss_cpu-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:30489de0356d3afa0b492ca55da164d02453db2f7323c682b69334fde9e8d48e", size = 15003860, upload-time = "2025-04-28T07:47:39.381Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/83/8aefc4d07624a868e046cc23ede8a59bebda57f09f72aee2150ef0855a82/faiss_cpu-1.11.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a90d1c81d0ecf2157e1d2576c482d734d10760652a5b2fcfa269916611e41f1c", size = 3307997, upload-time = "2025-04-28T07:47:41.905Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/64/f97e91d89dc6327e08f619fe387d7d9945bc4be3b0f1ca1e494a41c92ebe/faiss_cpu-1.11.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2c39a388b059fb82cd97fbaa7310c3580ced63bf285be531453bfffbe89ea3dd", size = 7886308, upload-time = "2025-04-28T07:47:44.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/0a/7c17b6df017b0bc127c6aa4066b028281e67ab83d134c7433c4e75cd6bb6/faiss_cpu-1.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a4e3433ffc7f9b8707a7963db04f8676a5756868d325644db2db9d67a618b7a0", size = 3778441, upload-time = "2025-04-28T07:47:46.914Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/45/7c85551025d9f0237d891b5cffdc5d4a366011d53b4b0a423b972cc52cea/faiss_cpu-1.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:926645f1b6829623bc88e93bc8ca872504d604718ada3262e505177939aaee0a", size = 31295136, upload-time = "2025-04-28T07:47:49.299Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/9a/accade34b8668b21206c0c4cf0b96cd0b750b693ba5b255c1c10cfee460f/faiss_cpu-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:931db6ed2197c03a7fdf833b057c13529afa2cec8a827aa081b7f0543e4e671b", size = 15003710, upload-time = "2025-04-28T07:47:52.226Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/d3/7178fa07047fd770964a83543329bb5e3fc1447004cfd85186ccf65ec3ee/faiss_cpu-1.11.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:356437b9a46f98c25831cdae70ca484bd6c05065af6256d87f6505005e9135b9", size = 3313807, upload-time = "2025-04-28T07:47:54.533Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/71/25f5f7b70a9f22a3efe19e7288278da460b043a3b60ad98e4e47401ed5aa/faiss_cpu-1.11.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c4a3d35993e614847f3221c6931529c0bac637a00eff0d55293e1db5cb98c85f", size = 7913537, upload-time = "2025-04-28T07:47:56.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/c8/a5cb8466c981ad47750e1d5fda3d4223c82f9da947538749a582b3a2d35c/faiss_cpu-1.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8f9af33e0b8324e8199b93eb70ac4a951df02802a9dcff88e9afc183b11666f0", size = 3785180, upload-time = "2025-04-28T07:47:59.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/37/eaf15a7d80e1aad74f56cf737b31b4547a1a664ad3c6e4cfaf90e82454a8/faiss_cpu-1.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:48b7e7876829e6bdf7333041800fa3c1753bb0c47e07662e3ef55aca86981430", size = 31287630, upload-time = "2025-04-28T07:48:01.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/5c/902a78347e9c47baaf133e47863134e564c39f9afe105795b16ee986b0df/faiss_cpu-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:bdc199311266d2be9d299da52361cad981393327b2b8aa55af31a1b75eaaf522", size = 15005398, upload-time = "2025-04-28T07:48:04.232Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/90/d2329ce56423cc61f4c20ae6b4db001c6f88f28bf5a7ef7f8bbc246fd485/faiss_cpu-1.11.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0c98e5feff83b87348e44eac4d578d6f201780dae6f27f08a11d55536a20b3a8", size = 3313807, upload-time = "2025-04-28T07:48:06.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/14/8af8f996d54e6097a86e6048b1a2c958c52dc985eb4f935027615079939e/faiss_cpu-1.11.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:796e90389427b1c1fb06abdb0427bb343b6350f80112a2e6090ac8f176ff7416", size = 7913539, upload-time = "2025-04-28T07:48:08.338Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/2b/437c2f36c3aa3cffe041479fced1c76420d3e92e1f434f1da3be3e6f32b1/faiss_cpu-1.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2b6e355dda72b3050991bc32031b558b8f83a2b3537a2b9e905a84f28585b47e", size = 3785181, upload-time = "2025-04-28T07:48:10.594Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/75/955527414371843f558234df66fa0b62c6e86e71e4022b1be9333ac6004c/faiss_cpu-1.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6c482d07194638c169b4422774366e7472877d09181ea86835e782e6304d4185", size = 31287635, upload-time = "2025-04-28T07:48:12.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/51/35b7a3f47f7859363a367c344ae5d415ea9eda65db0a7d497c7ea2c0b576/faiss_cpu-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:13eac45299532b10e911bff1abbb19d1bf5211aa9e72afeade653c3f1e50e042", size = 15005455, upload-time = "2025-04-28T07:48:16.173Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.18.0"
|
version = "3.18.0"
|
||||||
@@ -1260,6 +1310,99 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400, upload-time = "2025-04-10T22:20:16.445Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400, upload-time = "2025-04-10T22:20:16.445Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nh3"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581, upload-time = "2025-02-25T13:38:44.619Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678, upload-time = "2025-02-25T13:37:56.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774, upload-time = "2025-02-25T13:37:58.419Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012, upload-time = "2025-02-25T13:38:01.017Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619, upload-time = "2025-02-25T13:38:02.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384, upload-time = "2025-02-25T13:38:04.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908, upload-time = "2025-02-25T13:38:06.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180, upload-time = "2025-02-25T13:38:10.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747, upload-time = "2025-02-25T13:38:12.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908, upload-time = "2025-02-25T13:38:14.059Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133, upload-time = "2025-02-25T13:38:16.601Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328, upload-time = "2025-02-25T13:38:18.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020, upload-time = "2025-02-25T13:38:20.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878, upload-time = "2025-02-25T13:38:22.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460, upload-time = "2025-02-25T13:38:25.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369, upload-time = "2025-02-25T13:38:28.174Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036, upload-time = "2025-02-25T13:38:30.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712, upload-time = "2025-02-25T13:38:32.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559, upload-time = "2025-02-25T13:38:35.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591, upload-time = "2025-02-25T13:38:37.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670, upload-time = "2025-02-25T13:38:38.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093, upload-time = "2025-02-25T13:38:40.249Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623, upload-time = "2025-02-25T13:38:41.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283, upload-time = "2025-02-25T13:38:43.355Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.2.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "1.78.0"
|
version = "1.78.0"
|
||||||
@@ -1328,6 +1471,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b0/60/0ee5d790f13507e1f75ac21fc82dc1ef29afe1f520bd0f249d65b2f4839b/ormsgpack-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:a4bc63fb30db94075611cedbbc3d261dd17cf2aa8ff75a0fd684cd45ca29cb1b", size = 125371, upload-time = "2025-03-28T07:14:25.176Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/60/0ee5d790f13507e1f75ac21fc82dc1ef29afe1f520bd0f249d65b2f4839b/ormsgpack-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:a4bc63fb30db94075611cedbbc3d261dd17cf2aa8ff75a0fd684cd45ca29cb1b", size = 125371, upload-time = "2025-03-28T07:14:25.176Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "11.2.1"
|
version = "11.2.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user