feat: 支持聊天消息中的图片预览功能,更新Agent提示以包含图文结果输出

This commit is contained in:
wizardchen
2025-12-16 12:24:31 +08:00
committed by lyingbug
parent 8018bc2763
commit 95dfcf81cd
5 changed files with 129 additions and 12 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ services:
- full
docreader:
image: wechatopenai/weknora-docreader:main
image: wechatopenai/weknora-docreader:latest
container_name: WeKnora-docreader-dev
ports:
- "${DOCREADER_PORT:-50051}:50051"
@@ -208,6 +208,9 @@
</div>
</div>
</Teleport>
<!-- Image Preview -->
<picturePreview :reviewImg="imagePreviewVisible" :reviewUrl="imagePreviewUrl" @closePreImg="closeImagePreview" />
</template>
<script setup lang="ts">
@@ -216,6 +219,7 @@ import { useRouter } from 'vue-router';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import ToolResultRenderer from './ToolResultRenderer.vue';
import picturePreview from '@/components/picture-preview.vue';
import { getChunkByIdOnly } from '@/api/knowledge-base';
import { MessagePlugin } from 'tdesign-vue-next';
import { useUIStore } from '@/stores/ui';
@@ -248,6 +252,19 @@ const getLocalizedToolName = (toolName?: string | null): string => {
// 根元素引用
const rootElement = ref<HTMLElement | null>(null);
// 图片预览状态
const imagePreviewVisible = ref(false);
const imagePreviewUrl = ref('');
const openImagePreview = (url: string) => {
imagePreviewUrl.value = url;
imagePreviewVisible.value = true;
};
const closeImagePreview = () => {
imagePreviewVisible.value = false;
};
// 浮层状态(Web/KB 共用)
const KB_SNIPPET_LIMIT = 600;
@@ -923,6 +940,18 @@ const onRootClick = (e: Event) => {
const target = e.target as HTMLElement;
if (!target) return;
// Handle image clicks -> open preview
if (target.tagName === 'IMG') {
const imgEl = target as HTMLImageElement;
const src = imgEl.getAttribute('src');
if (src) {
e.preventDefault();
e.stopPropagation();
openImagePreview(src);
return;
}
}
// Handle web citation clicks
const webEl = target.closest?.('.citation-web') as HTMLElement | null;
if (webEl && webEl.getAttribute('data-url')) {
@@ -1130,8 +1159,8 @@ const renderMarkdown = (content: any): string => {
if (!html) return '';
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td'],
ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'data-tooltip', 'data-url', 'data-kb-id', 'data-chunk-id', 'data-doc', 'class', 'role', 'tabindex']
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'img', 'figure', 'figcaption'],
ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'data-tooltip', 'data-url', 'data-kb-id', 'data-chunk-id', 'data-doc', 'class', 'role', 'tabindex', 'src', 'alt', 'width', 'height', 'style']
});
} catch (e) {
console.error('Markdown rendering error:', e, 'Content:', contentStr.substring(0, 100));
@@ -1891,6 +1920,24 @@ const handleAddToKnowledge = (answerEvent: any) => {
font-weight: 600;
}
}
:deep(img) {
max-width: 80%;
max-height: 300px;
width: auto;
height: auto;
border-radius: 8px;
display: block;
margin: 8px 0;
border: 0.5px solid #e5e7eb;
object-fit: contain;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
}
}
}
}
@@ -2005,6 +2052,24 @@ const handleAddToKnowledge = (answerEvent: any) => {
font-weight: 600;
}
}
:deep(img) {
max-width: 80%;
max-height: 300px;
width: auto;
height: auto;
border-radius: 8px;
display: block;
margin: 8px 0;
border: 0.5px solid #e5e7eb;
object-fit: contain;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
}
}
}
+57 -7
View File
@@ -34,7 +34,7 @@
</div>
</template>
<script setup>
import { onMounted, watch, computed, ref, reactive, defineProps, nextTick } from 'vue';
import { onMounted, onBeforeUnmount, watch, computed, ref, reactive, defineProps, nextTick } from 'vue';
import { marked } from 'marked';
import docInfo from './docInfo.vue';
import deepThink from './deepThink.vue';
@@ -282,9 +282,35 @@ const handleAddToKnowledge = () => {
MessagePlugin.info(t('chat.editorOpened') || '已打开编辑器,请选择知识库后保存');
};
// 处理 markdown-content 中图片的点击事件
const handleMarkdownImageClick = (e) => {
const target = e.target;
if (target && target.tagName === 'IMG') {
const src = target.getAttribute('src');
if (src) {
e.preventDefault();
e.stopPropagation();
preview(src);
}
}
};
onMounted(async () => {
processedMarkdown.value = splitMarkdownByImages(props.content);
removeImg()
removeImg();
// 为 markdown-content 中的图片添加点击事件
nextTick(() => {
if (parentMd.value) {
parentMd.value.addEventListener('click', handleMarkdownImageClick, true);
}
});
});
onBeforeUnmount(() => {
if (parentMd.value) {
parentMd.value.removeEventListener('click', handleMarkdownImageClick, true);
}
});
</script>
<style lang="less" scoped>
@@ -396,18 +422,42 @@ onMounted(async () => {
background: #fafafa;
}
}
:deep(img) {
max-width: 80%;
max-height: 300px;
width: auto;
height: auto;
border-radius: 8px;
display: block;
margin: 8px 0;
border: 0.5px solid #e5e7eb;
object-fit: contain;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
}
}
.ai-markdown-img {
max-width: 80%;
max-height: 300px;
width: auto;
height: auto;
border-radius: 8px;
display: block;
cursor: pointer;
object-fit: scale-down;
contain: content;
margin-left: 16px;
object-fit: contain;
margin: 8px 0 8px 16px;
border: 0.5px solid #E7E7E7;
max-width: 708px;
height: 230px;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
}
.bot_msg {
+3 -1
View File
@@ -320,6 +320,7 @@ For every retrieval attempt (Phase 1 or Phase 3), follow this exact chain:
* **Sourced(Inline, Proximate Citations):** All factual statements must include a citation immediately after the relevant claim—within the same sentence or paragraph where the fact appears: <kb doc="..." chunk_id="..." /> or <web url="..." title="..." />.
Citations may not be placed at the end of the answer. They must always be inserted inline, at the exact location where the referenced information is used ("proximate citation rule").
* **Structured:** Clear hierarchy and logic.
* **Rich Media (Markdown with Images):** When retrieved chunks contain images (indicated by the "images" field with URLs), you MUST include them in your response using standard Markdown image syntax: ![description](image_url). Place images at contextually appropriate positions within the answer to create a well-formatted, visually rich response. Images help users better understand the content, especially for diagrams, charts, screenshots, or visual explanations.
### System Status
Current Time: {{current_time}}
@@ -399,9 +400,10 @@ For every information seeking step, strictly follow this 3-step atomic unit:
### Final Output Standards
1. **Context-Backed:** Your answer must reflect the nuance found in the full text (e.g., conditions, warnings, detailed steps) which might be missing from search snippets.
2 **Sourced(Inline, Proximate Citations):** All factual statements must include a citation immediately after the relevant claim—within the same sentence or paragraph where the fact appears: <kb doc="..." chunk_id="..." />.
2. **Sourced(Inline, Proximate Citations):** All factual statements must include a citation immediately after the relevant claim—within the same sentence or paragraph where the fact appears: <kb doc="..." chunk_id="..." />.
Citations may not be placed at the end of the answer. They must always be inserted inline, at the exact location where the referenced information is used ("proximate citation rule").
3. **Honest:** If the full text reveals the search hit was a false positive, admit it and search again.
4. **Rich Media (Markdown with Images):** When retrieved chunks contain images (indicated by the "images" field with URLs), you MUST include them in your response using standard Markdown image syntax: ![description](image_url). Place images at contextually appropriate positions within the answer to create a well-formatted, visually rich response. Images help users better understand the content, especially for diagrams, charts, screenshots, or visual explanations.
### System Status
Current Time: {{current_time}}
@@ -59,8 +59,8 @@ func (p *PluginChatCompletionStream) OnEvent(ctx context.Context,
pipelineInfo(ctx, "Stream", "messages_ready", map[string]interface{}{
"message_count": len(chatMessages),
"system_prompt": chatMessages[0].Content,
"user_content": chatMessages[len(chatMessages)-1].Content,
})
logger.Infof(ctx, "user message: %s", chatMessages[len(chatMessages)-1].Content)
// EventBus is required for event-driven streaming
if chatManage.EventBus == nil {
pipelineError(ctx, "Stream", "eventbus_missing", map[string]interface{}{