diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py
index 17e8b115..95ac98da 100644
--- a/astrbot/dashboard/routes/chat.py
+++ b/astrbot/dashboard/routes/chat.py
@@ -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)
diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py
index d8c1a1dd..acdc8a49 100644
--- a/astrbot/dashboard/server.py
+++ b/astrbot/dashboard/server.py
@@ -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
diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue
index ad149bcd..7069a81f 100644
--- a/dashboard/src/views/ChatPage.vue
+++ b/dashboard/src/views/ChatPage.vue
@@ -168,7 +168,7 @@ marked.setOptions({
+ :disabled="!prompt && stagedImagesName.length === 0 && !stagedAudioUrl" />
@@ -218,7 +218,8 @@ export default {
messages: [],
conversations: [],
currCid: '',
- stagedImagesUrl: [],
+ stagedImagesName: [], // 用于存储图片**文件名**的数组
+ stagedImagesUrl: [], // 用于存储图片的blob URL数组
loadingChat: false,
inputFieldLabel: '聊天吧!',
@@ -236,7 +237,9 @@ export default {
// Ctrl键长按相关变量
ctrlKeyDown: false,
ctrlKeyTimer: null,
- ctrlKeyLongPressThreshold: 300 // 长按阈值,单位毫秒
+ ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒
+
+ mediaCache: {}, // Add a cache to store media blobs
}
},
@@ -265,9 +268,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', {
@@ -328,17 +353,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: `
`
+ message: `
`
}
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: ``
}
@@ -403,15 +430,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);
}
@@ -430,13 +456,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);
@@ -446,6 +472,7 @@ export default {
},
removeImage(index) {
+ this.stagedImagesName.splice(index, 1);
this.stagedImagesUrl.splice(index, 1);
},
@@ -462,28 +489,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 = `
`
+ const imageUrl = await this.getMediaFile(img);
+ message[i].message = `
`
}
if (message[i].message.startsWith('[RECORD]')) {
let audio = message[i].message.replace('[RECORD]', '');
+ const audioUrl = await this.getMediaFile(audio);
message[i].message = ``
}
if (message[i].image_url && message[i].image_url.length > 0) {
for (let j = 0; j < message[i].image_url.length; j++) {
- message[i].image_url[j] = `/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;
@@ -534,32 +563,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: {
@@ -569,20 +607,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(() => {
@@ -623,6 +660,15 @@ export default {
}
}
},
+
+ cleanupMediaCache() {
+ Object.values(this.mediaCache).forEach(url => {
+ if (url.startsWith('blob:')) {
+ URL.revokeObjectURL(url);
+ }
+ });
+ this.mediaCache = {};
+ },
},
}