diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py
index 17e8b115e..b61a60f42 100644
--- a/astrbot/dashboard/routes/chat.py
+++ b/astrbot/dashboard/routes/chat.py
@@ -60,11 +60,23 @@ class ChatRoute(Route):
if not filename:
return Response().error("Missing key: filename").__dict__
+ # Prevent path traversal attacks by extracting just the basename
+ filename_ = os.path.basename(filename)
+
+ # Check if the filename contains suspicious patterns
+ if (
+ filename_ != filename
+ or ".." in filename
+ or "/" in filename
+ or "\\" in filename
+ ):
+ return Response().error("Invalid filename").__dict__
+
try:
- with open(os.path.join(self.imgs_dir, filename), "rb") as f:
- if filename.endswith(".wav"):
+ with open(os.path.join(self.imgs_dir, filename_), "rb") as f:
+ if filename_.endswith(".wav"):
return QuartResponse(f.read(), mimetype="audio/wav")
- elif filename.split(".")[-1] in self.supported_imgs:
+ elif filename_.split(".")[-1] in self.supported_imgs:
return QuartResponse(f.read(), mimetype="image/jpeg")
else:
return QuartResponse(f.read())
diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py
index d8c1a1dd9..acdc8a49f 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 ad149bcd0..da8030105 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: ``
}
@@ -411,7 +438,7 @@ export default {
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);
}
@@ -436,7 +463,8 @@ export default {
});
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 +474,7 @@ export default {
},
removeImage(index) {
+ this.stagedImagesName.splice(index, 1);
this.stagedImagesUrl.splice(index, 1);
},
@@ -462,28 +491,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 +565,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 +609,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 +662,15 @@ export default {
}
}
},
+
+ cleanupMediaCache() {
+ Object.values(this.mediaCache).forEach(url => {
+ if (url.startsWith('blob:')) {
+ URL.revokeObjectURL(url);
+ }
+ });
+ this.mediaCache = {};
+ },
},
}