Merge branch 'AstrBotDevs:master' into master
This commit is contained in:
@@ -13,6 +13,7 @@ import traceback
|
||||
from types import ModuleType
|
||||
from typing import List
|
||||
|
||||
import nh3
|
||||
import yaml
|
||||
|
||||
from astrbot.core import logger, pip_installer, sp
|
||||
@@ -638,12 +639,13 @@ class PluginManager:
|
||||
try:
|
||||
with open(readme_path, "r", encoding="utf-8") as f:
|
||||
readme_content = f.read()
|
||||
cleaned_content = nh3.clean(readme_content)
|
||||
except Exception as e:
|
||||
logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {str(e)}")
|
||||
|
||||
plugin_info = None
|
||||
if plugin:
|
||||
plugin_info = {"repo": plugin.repo, "readme": readme_content}
|
||||
plugin_info = {"repo": plugin.repo, "readme": cleaned_content}
|
||||
|
||||
return plugin_info
|
||||
|
||||
|
||||
@@ -61,16 +61,25 @@ class ChatRoute(Route):
|
||||
return Response().error("Missing key: filename").__dict__
|
||||
|
||||
try:
|
||||
with open(os.path.join(self.imgs_dir, filename), "rb") as f:
|
||||
if filename.endswith(".wav"):
|
||||
file_path = os.path.join(self.imgs_dir, os.path.basename(filename))
|
||||
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")
|
||||
elif filename.split(".")[-1] in self.supported_imgs:
|
||||
elif filename_ext[1:] in self.supported_imgs:
|
||||
return QuartResponse(f.read(), mimetype="image/jpeg")
|
||||
else:
|
||||
return QuartResponse(f.read())
|
||||
|
||||
except FileNotFoundError:
|
||||
return Response().error("File not found").__dict__
|
||||
except (FileNotFoundError, OSError):
|
||||
return Response().error("File access error").__dict__
|
||||
|
||||
async def post_image(self):
|
||||
post_data = await request.files
|
||||
@@ -126,17 +135,15 @@ class ChatRoute(Route):
|
||||
|
||||
self.curr_user_cid[username] = conversation_id
|
||||
|
||||
await web_chat_queue.put(
|
||||
(
|
||||
username,
|
||||
conversation_id,
|
||||
{
|
||||
"message": message,
|
||||
"image_url": image_url, # list
|
||||
"audio_url": audio_url,
|
||||
},
|
||||
)
|
||||
)
|
||||
await web_chat_queue.put((
|
||||
username,
|
||||
conversation_id,
|
||||
{
|
||||
"message": message,
|
||||
"image_url": image_url, # list
|
||||
"audio_url": audio_url,
|
||||
},
|
||||
))
|
||||
|
||||
# 持久化
|
||||
conversation = self.db.get_conversation_by_user_id(username, conversation_id)
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
import ssl
|
||||
import certifi
|
||||
import nh3
|
||||
|
||||
from .route import Route, Response, RouteContext
|
||||
from astrbot.core import logger
|
||||
@@ -148,9 +149,7 @@ class PluginRoute(Route):
|
||||
if handler.event_type == EventType.AdapterMessageEvent:
|
||||
# 处理平台适配器消息事件
|
||||
has_admin = False
|
||||
for (
|
||||
filter
|
||||
) in (
|
||||
for filter in (
|
||||
handler.event_filters
|
||||
): # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高
|
||||
if isinstance(filter, CommandFilter):
|
||||
@@ -363,9 +362,11 @@ class PluginRoute(Route):
|
||||
with open(readme_path, "r", encoding="utf-8") as f:
|
||||
readme_content = f.read()
|
||||
|
||||
cleaned_content = nh3.clean(readme_content)
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok({"content": readme_content}, "成功获取README内容")
|
||||
.ok({"content": cleaned_content}, "成功获取README内容")
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -386,14 +387,12 @@ class PluginRoute(Route):
|
||||
platform_type = platform.get("type", "")
|
||||
platform_id = platform.get("id", "")
|
||||
|
||||
platforms.append(
|
||||
{
|
||||
"name": platform_id, # 使用type作为name,这是系统内部使用的平台名称
|
||||
"id": platform_id, # 保留id字段以便前端可以显示
|
||||
"type": platform_type,
|
||||
"display_name": f"{platform_type}({platform_id})",
|
||||
}
|
||||
)
|
||||
platforms.append({
|
||||
"name": platform_id, # 使用type作为name,这是系统内部使用的平台名称
|
||||
"id": platform_id, # 保留id字段以便前端可以显示
|
||||
"type": platform_type,
|
||||
"display_name": f"{platform_type}({platform_id})",
|
||||
})
|
||||
|
||||
adjusted_platform_enable = {}
|
||||
for platform_id, plugins in platform_enable.items():
|
||||
@@ -402,13 +401,11 @@ class PluginRoute(Route):
|
||||
# 获取所有插件,包括系统内部插件
|
||||
plugins = []
|
||||
for plugin in self.plugin_manager.context.get_all_stars():
|
||||
plugins.append(
|
||||
{
|
||||
"name": plugin.name,
|
||||
"desc": plugin.desc,
|
||||
"reserved": plugin.reserved, # 添加reserved标志
|
||||
}
|
||||
)
|
||||
plugins.append({
|
||||
"name": plugin.name,
|
||||
"desc": plugin.desc,
|
||||
"reserved": plugin.reserved, # 添加reserved标志
|
||||
})
|
||||
|
||||
logger.debug(
|
||||
f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}"
|
||||
@@ -416,13 +413,11 @@ class PluginRoute(Route):
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"platforms": platforms,
|
||||
"plugins": plugins,
|
||||
"platform_enable": adjusted_platform_enable,
|
||||
}
|
||||
)
|
||||
.ok({
|
||||
"platforms": platforms,
|
||||
"plugins": plugins,
|
||||
"platform_enable": adjusted_platform_enable,
|
||||
})
|
||||
.__dict__
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
@@ -70,13 +70,13 @@ class AstrBotDashboard:
|
||||
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 await view_handler(*args, **kwargs)
|
||||
return jsonify(Response().error("未找到该路由").__dict__)
|
||||
|
||||
async def auth_middleware(self):
|
||||
if not request.path.startswith("/api"):
|
||||
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):
|
||||
return
|
||||
# claim jwt
|
||||
|
||||
@@ -165,7 +165,7 @@ marked.setOptions({
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" @click="sendMessage" class="send-btn" icon="mdi-send"
|
||||
variant="text" color="deep-purple"
|
||||
:disabled="!prompt && stagedImagesUrl.length === 0 && !stagedAudioUrl" />
|
||||
:disabled="!prompt && stagedImagesName.length === 0 && !stagedAudioUrl" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
@@ -215,7 +215,8 @@ export default {
|
||||
messages: [],
|
||||
conversations: [],
|
||||
currCid: '',
|
||||
stagedImagesUrl: [],
|
||||
stagedImagesName: [], // 用于存储图片**文件名**的数组
|
||||
stagedImagesUrl: [], // 用于存储图片的blob URL数组
|
||||
loadingChat: false,
|
||||
|
||||
inputFieldLabel: '聊天吧!',
|
||||
@@ -233,7 +234,9 @@ export default {
|
||||
// Ctrl键长按相关变量
|
||||
ctrlKeyDown: false,
|
||||
ctrlKeyTimer: null,
|
||||
ctrlKeyLongPressThreshold: 300 // 长按阈值,单位毫秒
|
||||
ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒
|
||||
|
||||
mediaCache: {}, // Add a cache to store media blobs
|
||||
}
|
||||
},
|
||||
|
||||
@@ -262,9 +265,31 @@ export default {
|
||||
|
||||
// 移除keyup事件监听
|
||||
document.removeEventListener('keyup', this.handleInputKeyUp);
|
||||
|
||||
// Cleanup blob URLs
|
||||
this.cleanupMediaCache();
|
||||
},
|
||||
|
||||
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() {
|
||||
const response = await fetch('/api/chat/listen', {
|
||||
@@ -325,17 +350,19 @@ export default {
|
||||
|
||||
if (chunk_json.type === 'image') {
|
||||
let img = chunk_json.data.replace('[IMAGE]', '');
|
||||
const imageUrl = await this.getMediaFile(img);
|
||||
let bot_resp = {
|
||||
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);
|
||||
} else if (chunk_json.type === 'record') {
|
||||
let audio = chunk_json.data.replace('[RECORD]', '');
|
||||
const audioUrl = await this.getMediaFile(audio);
|
||||
let bot_resp = {
|
||||
type: 'bot',
|
||||
message: `<audio controls class="audio-player">
|
||||
<source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
|
||||
<source src="${audioUrl}" type="audio/wav">
|
||||
您的浏览器不支持音频播放。
|
||||
</audio>`
|
||||
}
|
||||
@@ -400,15 +427,14 @@ export default {
|
||||
try {
|
||||
const response = await axios.post('/api/chat/post_file', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
const audio = response.data.data.filename;
|
||||
console.log('Audio uploaded:', audio);
|
||||
|
||||
this.stagedAudioUrl = `/api/chat/get_file?filename=${audio}`;
|
||||
this.stagedAudioUrl = audio; // Store just the filename
|
||||
} catch (err) {
|
||||
console.error('Error uploading audio:', err);
|
||||
}
|
||||
@@ -427,13 +453,13 @@ export default {
|
||||
try {
|
||||
const response = await axios.post('/api/chat/post_image', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
console.error('Error uploading image:', err);
|
||||
@@ -443,6 +469,7 @@ export default {
|
||||
},
|
||||
|
||||
removeImage(index) {
|
||||
this.stagedImagesName.splice(index, 1);
|
||||
this.stagedImagesUrl.splice(index, 1);
|
||||
},
|
||||
|
||||
@@ -459,28 +486,30 @@ export default {
|
||||
getConversationMessages(cid) {
|
||||
if (!cid[0])
|
||||
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];
|
||||
let message = JSON.parse(response.data.data.history);
|
||||
for (let i = 0; i < message.length; i++) {
|
||||
if (message[i].message.startsWith('[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]')) {
|
||||
let audio = message[i].message.replace('[RECORD]', '');
|
||||
const audioUrl = await this.getMediaFile(audio);
|
||||
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>`
|
||||
}
|
||||
if (message[i].image_url && message[i].image_url.length > 0) {
|
||||
for (let j = 0; j < message[i].image_url.length; j++) {
|
||||
message[i].image_url[j] = `/api/chat/get_file?filename=${message[i].image_url[j]}`;
|
||||
message[i].image_url[j] = await this.getMediaFile(message[i].image_url[j]);
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -531,32 +560,41 @@ export default {
|
||||
await this.newConversation();
|
||||
}
|
||||
|
||||
this.messages.push({
|
||||
// Create a message object with actual URLs for display
|
||||
const userMessage = {
|
||||
type: 'user',
|
||||
message: this.prompt,
|
||||
image_url: this.stagedImagesUrl,
|
||||
audio_url: this.stagedAudioUrl
|
||||
});
|
||||
image_url: [],
|
||||
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();
|
||||
|
||||
// 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;
|
||||
|
||||
|
||||
fetch('/api/chat/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -566,20 +604,19 @@ export default {
|
||||
body: JSON.stringify({
|
||||
message: this.prompt,
|
||||
conversation_id: this.currCid,
|
||||
image_url: image_filenames,
|
||||
audio_url: audio_filenames
|
||||
}) // 发送请求体
|
||||
})
|
||||
.then(response => {
|
||||
this.prompt = '';
|
||||
this.stagedImagesUrl = [];
|
||||
this.stagedAudioUrl = "";
|
||||
|
||||
this.loadingChat = false;
|
||||
image_url: this.stagedImagesName, // Already contains just filenames
|
||||
audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [] // Already contains just filename
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
})
|
||||
.then(response => {
|
||||
this.prompt = '';
|
||||
this.stagedImagesName = [];
|
||||
this.stagedAudioUrl = "";
|
||||
this.loadingChat = false;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
@@ -620,6 +657,15 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
cleanupMediaCache() {
|
||||
Object.values(this.mediaCache).forEach(url => {
|
||||
if (url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
this.mediaCache = {};
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -27,6 +27,7 @@ dependencies = [
|
||||
"lark-oapi>=1.4.15",
|
||||
"lxml-html-clean>=0.4.2",
|
||||
"mcp>=1.8.0",
|
||||
"nh3>=0.2.21",
|
||||
"openai>=1.78.0",
|
||||
"ormsgpack>=1.9.1",
|
||||
"pillow>=11.2.1",
|
||||
|
||||
Reference in New Issue
Block a user