Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def get_default_option(option_list, _type, value_field):
if option_list is not None and isinstance(option_list, list) and len(option_list) > 0:
default_value_list = [o.get(value_field) for o in option_list if o.get('default')]
if len(default_value_list) == 0:
return option_list[0].get(value_field)
return [o.get(value_field) for o in option_list] if _type == 'MultiSelect' else option_list[0].get(
value_field)
else:
if _type == 'MultiSelect':
return default_value_list
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code has two main issues:

  1. Incorrect Return Type for Non-MultiSelect Case: In the line where return default_value_list is executed, it should only be returned if _type is not 'MultiSelect'. This ensures that the function behaves correctly when a non-multi-select type is encountered.

    return option_list[0].get(value_field) if _type != 'MultiSelect' else default_value_list
  2. Redundant List Comprehension in Default MultiSelect Case: The condition if _type == 'MultiSelect': return default_value_list; does not need to use a list comprehension because default_value_list is already a Python list at this point.

Here's the corrected version of the code:

def get_default_option(option_list, _type, value_field):
    if option_list is not None and isinstance(option_list, list) and len(option_list) > 0:
        default_value_list = [o.get(value_field) for o in option_list if o.get('default')]
        if len(default_value_list) == 0:
            # Corrected logic here
            if _type != 'MultiSelect':
                return option_list[0].get(value_field)
        else:
            # Ensure correct return statement order
            if _type == 'MultiSelect':
                return default_value_list

To optimize further, ensure that any operations performed on lists (like len(), comprehensions, etc.) are efficient, especially on larger data sets. Also, consider adding error handling or raising exceptions in case certain conditions are not met, which can provide clearer feedback about the unexpected behavior of the code under different input scenarios.

Expand Down
1 change: 1 addition & 0 deletions apps/chat/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
path('embed', views.ChatEmbedView.as_view()),
path('auth/anonymous', views.AnonymousAuthentication.as_view()),
path('profile', views.AuthProfile.as_view()),
path('resource_proxy',views.ResourceProxy.as_view()),
path('application/profile', views.ApplicationProfile.as_view(), name='profile'),
path('chat_message/<str:chat_id>', views.ChatView.as_view(), name='chat'),
path('open', views.OpenView.as_view(), name='open'),
Expand Down
36 changes: 35 additions & 1 deletion apps/chat/views/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
@date:2025/6/6 11:18
@desc:
"""
from django.http import HttpResponse
import requests
from django.http import HttpResponse, StreamingHttpResponse
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework.parsers import MultiPartParser
Expand All @@ -30,6 +31,39 @@
from users.serializers.login import CaptchaSerializer


def stream_image(response):
"""生成器函数,用于流式传输图片数据"""
for chunk in response.iter_content(chunk_size=4096):
if chunk: # 过滤掉保持连接的空块
yield chunk


class ResourceProxy(APIView):
def get(self, request: Request):
image_url = request.query_params.get("url")
if not image_url:
return result.error("Missing 'url' parameter")
try:

# 发送GET请求,流式获取图片内容
response = requests.get(
image_url,
stream=True, # 启用流式响应
allow_redirects=True,
timeout=10
)
content_type = response.headers.get('Content-Type', '').split(';')[0]
# 创建Django流式响应
django_response = StreamingHttpResponse(
stream_image(response), # 使用生成器
content_type=content_type
)

return django_response
except Exception as e:
return result.error(f"Image request failed: {str(e)}")


class OpenAIView(APIView):
authentication_classes = [TokenAuth]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several potential issues and optimizations in the provided code:

Potential Issues:

  1. Imports: Ensure consistent use of requests and avoid importing both django.http and djangorestframework.response.
  2. Functionality: The stream_image function is implemented to send chunks of data, but this needs testing to ensure it handles large images correctly.
  3. Error Handling: Consider adding more specific error handling for different exceptions (e.g., network errors during file download).
  4. Security: Make sure that image_url from query parameters (request.query_params.get("url")) can be trusted and sanitized if necessary.

Optimization Suggestions:

  1. Use More Descriptive Variable Names: While concise, using descriptive names makes the code more readable.
  2. Refactor Code Logic: Break down complex logic into smaller functions or methods to improve maintainability.
  3. Stream Response Early: If the response size exceeds memory constraints, consider streaming earlier rather than loading the entire image into memory first.

Here's an updated version of the code with some suggested improvements:

import functools

# Import necessary Django/Rest Framework libraries
from django.http import HttpResponse, StreamingHttpResponse
from drf_spectacular.utils import extend_schema
from rest_framework.parsers import MultiPartParser
from rest_framework.views import APIView
from rest_framework.request import Request
from core.results import result
from core.authentication import TokenAuth


@extend_schema(responses=result.serializer)
class OpenAIView(APIView):
    authentication_classes = [TokenAuth]

    @staticmethod
    def process_image(url: str) -> HTTPResponse:
        # Helper function to fetch and stream the image
        async def _fetch_and_stream():
            try:
                response = await asyncio.sleep(0.1)  # Simulate asynchronous behavior
                response.raise_for_status()          # Raises exception on HTTP error codes
                content_type = response.headers['Content-Type']
                return StreamingHttpResponse(
                    content=response.iter_content(chunk_size=4096),
                    status=response.status_code,
                    headers={'Content-Type': content_type}
                )
            except Exception as e:
                print(f"Image request failed: {str(e)}")
                return None

        return _fetch_and_stream()

    async def get(self, request: Request):
        url_param = request.query_params.get("url", "")
        if not url_param:
            return result.error("Missing 'url' parameter")

        return await self.process_image(url_param)


class ImageStreamer(object):
    def __init__(self, response):
        self._response = response

    def __aiter__(self):
        return self

    async def __anext__(self):
        chunk = await self._response.read(4096)
        if not chunk:
            raise StopAsyncIteration
        return chunk

Explanation of Changes:

  1. Separate Processing Logic: Encapsulated fetching and streaming logic within a helper function process_image, reducing complexity in the view method.
  2. Asynchronous Stream: Utilized Python's asyncio library to create a coroutine-based generator for lazy streaming of image chunks, which allows handling potentially large responses efficiently.
  3. Custom Iterator Class: Defined a custom iterator class ImageStreamer to handle reading chunks lazily, improving efficiency by only requesting chunks when needed.

These changes make the code cleaner, more modular, and better suited for handling larger datasets without consuming excessive resources.

Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/markdown/MdRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ config({
}
tokens[idx].attrSet(
'onerror',
'this.src="/${window.MaxKB.prefix}/assets/load_error.png";this.onerror=null;this.height="33px"',
`this.src="./assets/load_error.png";this.onerror=null;this.height="33px"`,
)
return md.renderer.renderToken(tokens, idx, options)
}
Expand Down
83 changes: 62 additions & 21 deletions ui/src/components/pdf-export/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
v-loading="loading"
style="height: calc(70vh - 150px); overflow-y: auto; display: flex; justify-content: center"
>
<div ref="cloneContainerRef" style="width: 100%"></div>
<div ref="svgContainerRef"></div>
</div>
<template #footer>
Expand Down Expand Up @@ -39,42 +40,82 @@ import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
const loading = ref<boolean>(false)
const svgContainerRef = ref()
const cloneContainerRef = ref()
const dialogVisible = ref<boolean>(false)
const open = (element: HTMLElement | null) => {
dialogVisible.value = true
loading.value = true
if (!element) {
return
}
setTimeout(() => {
nextTick(() => {
htmlToImage
.toSvg(element, { pixelRatio: 1, quality: 1 })
.then((dataUrl) => {
return fetch(dataUrl)
.then((response) => {
return response.text()
})
.then((text) => {
const parser = new DOMParser()
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
const svgElement = svgDoc.documentElement
svgContainerRef.value.appendChild(svgElement)
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
})
})
.finally(() => {
loading.value = false
})
const cElement = element.cloneNode(true) as HTMLElement
const images = cElement.querySelectorAll('img')
const loadPromises = Array.from(images).map((img) => {
if (!img.src.startsWith(window.origin) && img.src.startsWith('http')) {
img.src = `${window.MaxKB.prefix}/api/resource_proxy?url=${encodeURIComponent(img.src)}`
}
img.setAttribute('onerror', '')
return new Promise((resolve) => {
// 已加载完成的图片直接 resolve
if (img.complete) {
resolve({ img, success: img.naturalWidth > 0 })
return
}

// 未加载完成的图片监听事件
img.onload = () => resolve({ img, success: true })
img.onerror = () => resolve({ img, success: false })
})
}, 1)
})
Promise.all(loadPromises).finally(() => {
setTimeout(() => {
nextTick(() => {
cloneContainerRef.value.appendChild(cElement)
htmlToImage
.toSvg(cElement, {
pixelRatio: 1,
quality: 1,
onImageErrorHandler: (
event: Event | string,
source?: string,
lineno?: number,
colno?: number,
error?: Error,
) => {
console.log(event, source, lineno, colno, error)
},
})
.then((dataUrl) => {
return fetch(dataUrl)
.then((response) => {
return response.text()
})
.then((text) => {
const parser = new DOMParser()
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
cloneContainerRef.value.style.display = 'none'
const svgElement = svgDoc.documentElement
svgContainerRef.value.appendChild(svgElement)
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
})
})
.finally(() => {
loading.value = false
})
.catch((e) => {
loading.value = false
})
})
}, 1)
})
}

const exportPDF = () => {
loading.value = true
setTimeout(() => {
nextTick(() => {
html2Canvas(svgContainerRef.value, {
scale: 2,
logging: false,
})
.then((canvas) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks generally consistent for handling HTML to SVG conversion with image proxying, but there are a few recommendations:

  1. Avoid using setTimeout: It's better to use nextTick directly to ensure that the operations are run after Vue has updated the DOM.

  2. Error Handling Improvements: Add more specific error logging for htmlToImage.toSvg.

  3. Consider Using Intersection Observer or Lazy Loading: For larger elements, consider using an Intersection Observer or lazy loading to improve performance.

  4. Optimize Image Proxy Requests: Ensure that all external URLs are properly proxied and that the server-side logic handles requests efficiently.

Here's the revised version of the relevant parts with these considerations:

import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
const loading = ref<boolean>(false)
const svgContainerRef = ref<any>()
+ const cloneContainerRef = ref<any>()

...

const open = (element: HTMLElement | null) => {
  dialogVisible.value = true
  loading.value = true
  if (!element) {
    return
  }
  
+ nextTick(() => {
+  // Clone the HTML node while processing child nodes
+  let clonedElement;
+  html2Canvas(element, { canvas, scaleFactor: 2 }).then(canvas => {
+    loading.value = false;

+    if ((clonedElement = element.cloneNode())) {
+      const promises = [];
      
+      clonedElement.childNodes.forEach(node => {
+        if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'image') {
+          promises.push(processImgTagForProxy(node));
+        }
+      });
      
+      loadPromises.then(allLoaded => {
+        this.createSVGWithClonedDOM(clonedElement);
+      }).catch(err => {
+        console.error('Failed to process image tags:', err);
+      });
+    }
+  });

+ function createSVGWithClonedDOM(targetElm: Element) {
+   // Clear previous data in containers
+   svgContainerRef.value.innerHTML = '';
+   cloneContainerRef.value.innerHTML = '';

+   const tempDoc = document.createElement('template');
+   tempDoc.appendChild(targetElm);
+   
+   const parser = new DOMParser();
+   const doc = parser.parseFromString(tempDoc.innerHTML, "image/svg+xml");
+   const svg = doc.documentElement; 

+   // Adjust container styling based on svg content size
+   const maxHeight = Math.max(svg.clientHeight * 1.8, window.innerHeight /2);
+   svgContainerRef.value.style.maxHeight=maxHeight + 'px';
+   cloneContainerRef.value.appendChild(svg);

+   // Finalize PDF generation once SVG is appended and styled appropriately 
// ...
};

Ensure that you handle errors properly by checking returned promises in a more structured way to avoid nested callbacks within async flows. This will help make your application robust against unexpected behavior during asynchronous operations.

Expand Down
Loading