Merge pull request #2027 from IGCrystal/Branch-2
🐞 fix(WebUI): 解决XSS注入的问题
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"js-md5": "^0.8.3",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "^15.0.7",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "2.1.6",
|
||||
"remixicon": "3.5.0",
|
||||
"vee-validate": "4.11.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
@@ -74,29 +74,28 @@ function openRepoInNewTab() {
|
||||
}
|
||||
}
|
||||
|
||||
// 配置markdown-it,启用代码高亮
|
||||
const md = new MarkdownIt({
|
||||
html: true, // 启用HTML标签
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false, // 禁用智能引号
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染Markdown内容
|
||||
function renderMarkdown(content) {
|
||||
if (!content) return '';
|
||||
|
||||
// 配置marked使用highlight.js进行语法高亮
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
gfm: true, // GitHub Flavored Markdown
|
||||
breaks: true, // Convert \n to <br>
|
||||
headerIds: true, // Add id attributes to headers
|
||||
mangle: false // Don't mangle email addresses
|
||||
});
|
||||
|
||||
return marked(content);
|
||||
return md.render(content);
|
||||
}
|
||||
|
||||
// 刷新README内容
|
||||
@@ -120,7 +119,7 @@ const _show = computed({
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h5">{{ t('core.common.readme.title') }}</span>
|
||||
<v-btn icon @click="$emit('update:show', false)">
|
||||
<v-btn icon @click="$emit('update:show', false)" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"copy": "Copy",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
@@ -32,6 +33,7 @@
|
||||
"longPress": "Long press",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"imagePreview": "Image Preview",
|
||||
"dialog": {
|
||||
"confirmTitle": "Confirm Action",
|
||||
"confirmMessage": "Are you sure you want to perform this action?",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"close": "关闭",
|
||||
"copy": "复制",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"add": "添加",
|
||||
@@ -32,6 +33,7 @@
|
||||
"longPress": "长按",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"imagePreview": "图片预览",
|
||||
"dialog": {
|
||||
"confirmTitle": "确认操作",
|
||||
"confirmMessage": "你确定要执行此操作吗?",
|
||||
|
||||
@@ -7,9 +7,17 @@ import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||
import {md5} from 'js-md5';
|
||||
import {useAuthStore} from '@/stores/auth';
|
||||
import {useCommonStore} from '@/stores/common';
|
||||
import {marked} from 'marked';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
html: true, // 启用HTML标签
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false // 禁用智能引号
|
||||
});
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const { t } = useI18n();
|
||||
let dialog = ref(false);
|
||||
@@ -323,7 +331,7 @@ commonStore.getStartTime();
|
||||
|
||||
<div v-if="releaseMessage"
|
||||
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="md.render(releaseMessage)" class="markdown-content">
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-4">
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.type == 'user'" class="user-message">
|
||||
<div class="message-bubble user-bubble"
|
||||
:class="{ 'has-audio': msg.audio_url }"
|
||||
:style="{ backgroundColor: isDark ? '#2d2e30' : '#e7ebf4' }">
|
||||
<span>{{ msg.message }}</span>
|
||||
|
||||
@@ -156,7 +157,7 @@
|
||||
<div class="image-attachments" v-if="msg.image_url && msg.image_url.length > 0">
|
||||
<div v-for="(img, index) in msg.image_url" :key="index"
|
||||
class="image-attachment">
|
||||
<img :src="img" class="attached-image" />
|
||||
<img :src="img" class="attached-image" @click="openImagePreview(img)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,7 +178,7 @@
|
||||
</v-avatar>
|
||||
<div class="bot-message-content">
|
||||
<div class="message-bubble bot-bubble">
|
||||
<div v-html="marked(msg.message)" class="markdown-content"></div>
|
||||
<div v-html="md.render(msg.message)" class="markdown-content"></div>
|
||||
</div>
|
||||
<div class="message-actions">
|
||||
<v-btn :icon="getCopyIcon(index)" size="small" variant="text"
|
||||
@@ -256,12 +257,25 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
|
||||
<v-card class="image-preview-card" elevation="8">
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||
<span>{{ t('core.common.imagePreview') }}</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" />
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center pa-4">
|
||||
<img :src="previewImageUrl" class="preview-image-large" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { router } from '@/router';
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { ref } from 'vue';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
@@ -270,8 +284,11 @@ import ProviderModelSelector from '@/components/chat/ProviderModelSelector.vue';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
// 配置markdown-it,启用代码高亮
|
||||
const md = new MarkdownIt({
|
||||
html: false, // 禁用HTML标签,防XSS
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
highlight: function (code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
@@ -303,7 +320,7 @@ export default {
|
||||
t,
|
||||
tm,
|
||||
router,
|
||||
marked,
|
||||
md,
|
||||
ref
|
||||
};
|
||||
},
|
||||
@@ -354,6 +371,10 @@ export default {
|
||||
copySuccessMessage: null,
|
||||
copySuccessTimeout: null,
|
||||
copiedMessages: new Set(), // 存储已复制的消息索引
|
||||
|
||||
// 图片预览相关变量
|
||||
imagePreviewDialog: false,
|
||||
previewImageUrl: ''
|
||||
}
|
||||
},
|
||||
|
||||
@@ -559,6 +580,25 @@ export default {
|
||||
this.stagedAudioUrl = null;
|
||||
},
|
||||
|
||||
openImagePreview(imageUrl) {
|
||||
this.previewImageUrl = imageUrl;
|
||||
this.imagePreviewDialog = true;
|
||||
},
|
||||
|
||||
initImageClickEvents() {
|
||||
this.$nextTick(() => {
|
||||
// 查找所有动态生成的图片(在markdown-content中)
|
||||
const images = document.querySelectorAll('.markdown-content img');
|
||||
images.forEach((img) => {
|
||||
if (!img.hasAttribute('data-click-enabled')) {
|
||||
img.style.cursor = 'pointer';
|
||||
img.setAttribute('data-click-enabled', 'true');
|
||||
img.onclick = () => this.openImagePreview(img.src);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
checkStatus() {
|
||||
axios.get('/api/chat/status').then(response => {
|
||||
console.log(response.data);
|
||||
@@ -705,6 +745,7 @@ export default {
|
||||
}
|
||||
this.messages = message;
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
@@ -923,8 +964,9 @@ export default {
|
||||
}
|
||||
} else if (chunk_json.type === 'end') {
|
||||
in_streaming = false;
|
||||
// 在消息流结束后初始化代码复制按钮
|
||||
// 在消息流结束后初始化代码复制按钮和图片点击事件
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
continue;
|
||||
} else if (chunk_json.type === 'update_title') {
|
||||
// 更新对话标题
|
||||
@@ -964,8 +1006,9 @@ export default {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.messageContainer;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
// 在滚动后初始化代码复制按钮
|
||||
// 在滚动后初始化代码复制按钮和图片点击事件
|
||||
this.initCodeCopyButtons();
|
||||
this.initImageClickEvents();
|
||||
});
|
||||
},
|
||||
handleInputKeyDown(e) {
|
||||
@@ -1518,13 +1561,59 @@ export default {
|
||||
|
||||
.attached-image:hover {
|
||||
transform: scale(1.02);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 图片预览对话框样式 */
|
||||
.image-preview-card {
|
||||
background-color: var(--v-theme-surface) !important;
|
||||
border: 1px solid var(--v-theme-border);
|
||||
}
|
||||
|
||||
/* 亮色主题下的图片预览对话框 */
|
||||
.v-theme--light .image-preview-card,
|
||||
.v-theme--PurpleTheme .image-preview-card {
|
||||
background-color: #ffffff !important;
|
||||
border-color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
/* 暗色主题下的图片预览对话框 */
|
||||
.v-theme--dark .image-preview-card,
|
||||
.v-theme--PurpleThemeDark .image-preview-card {
|
||||
background-color: #1e1e1e !important;
|
||||
border-color: #333333 !important;
|
||||
}
|
||||
|
||||
/* 确保对话框标题栏和内容区域的背景色 */
|
||||
.image-preview-card .v-card-title {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.image-preview-card .v-card-text {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.preview-image-large {
|
||||
max-width: 100%;
|
||||
max-height: 75vh;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.audio-attachment {
|
||||
margin-top: 8px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
/* 包含音频的消息气泡最小宽度 */
|
||||
.message-bubble.has-audio {
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
@@ -318,12 +318,16 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import { marked } from 'marked';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
html: false, // 禁用HTML标签(关键!)
|
||||
breaks: true, // 换行转<br>
|
||||
linkify: true, // 自动转链接
|
||||
typographer: false // 禁用智能引号(避免干扰)
|
||||
});
|
||||
|
||||
export default {
|
||||
@@ -879,8 +883,9 @@ export default {
|
||||
// 处理字符串内容
|
||||
final_content = content;
|
||||
} else if (!final_content) return this.tm('status.emptyContent');
|
||||
// 使用marked处理Markdown格式
|
||||
return marked(final_content);
|
||||
|
||||
// 使用markdown-it处理,默认安全(html: false会禁用HTML标签)
|
||||
return md.render(final_content);
|
||||
},
|
||||
|
||||
// 显示成功消息
|
||||
|
||||
Reference in New Issue
Block a user