From 4dad2a593bd6bedfad6218fd597765a5a1d4dcb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Sun, 17 Aug 2025 00:41:48 +0800 Subject: [PATCH] fix(export): robustly export reasoning and handle errors (#9221) * fix(export): robustly export reasoning and handle errors * fix(export): normalize
to newline before notion parsing * feat(i18n): add notion truncation and unify export warn keys * refactor(export): add typing, state guards, and error logging * fix(export): preserve existing
in reasoning when convert to html * feat(export): add DOMPurify sanitization for reasoning content * chore(deps): remove unused @types/dompurify dev dep * chore(deps): remove dompurify dependency Remove dompurify from package.json and yarn. The changes delete dompurify entries and simplify the lockfile resolution so the project no longer declares dompurify as a direct dependency. This cleans up unused dependency declarations and prevents installing dompurify when it is not required. --- package.json | 1 - src/renderer/src/i18n/locales/en-us.json | 16 +- src/renderer/src/i18n/locales/ja-jp.json | 16 +- src/renderer/src/i18n/locales/ru-ru.json | 16 +- src/renderer/src/i18n/locales/zh-cn.json | 16 +- src/renderer/src/i18n/locales/zh-tw.json | 16 +- src/renderer/src/i18n/translate/el-gr.json | 58 ++- src/renderer/src/i18n/translate/es-es.json | 58 ++- src/renderer/src/i18n/translate/fr-fr.json | 58 ++- src/renderer/src/i18n/translate/pt-pt.json | 58 ++- src/renderer/src/utils/export.ts | 405 +++++++++++++++------ yarn.lock | 3 +- 12 files changed, 511 insertions(+), 210 deletions(-) diff --git a/package.json b/package.json index 70ed89485c..a824984845 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,6 @@ "dexie-react-hooks": "^1.1.7", "diff": "^7.0.0", "docx": "^9.0.2", - "dompurify": "^3.2.6", "dotenv-cli": "^7.4.2", "electron": "37.2.3", "electron-builder": "26.0.15", diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 86717e01b5..eb83ad5e8f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -840,6 +840,9 @@ "created": "Created", "last_updated": "Last Updated", "messages": "Messages", + "notion": { + "reasoning_truncated": "Chain of thought cannot be chunked and has been truncated." + }, "user": "User" }, "files": { @@ -1257,7 +1260,8 @@ }, "notion": { "export": "Failed to export to Notion. Please check connection status and configuration according to documentation", - "no_api_key": "Notion ApiKey or Notion DatabaseID is not configured" + "no_api_key": "Notion ApiKey or Notion DatabaseID is not configured", + "no_content": "There is nothing to export to Notion." }, "siyuan": { "export": "Failed to export to Siyuan Note, please check connection status and configuration according to documentation", @@ -1383,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "Exporting to Notion, please do not request export repeatedly!" - }, - "siyuan": { - "exporting": "Exporting to Siyuan Note, please do not request export repeatedly!" - }, - "yuque": { - "exporting": "Exporting to Yuque, please do not request export repeatedly!" + "export": { + "exporting": "Another export is in progress. Please wait for the previous export to complete and then try again." } }, "warning": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index b41b10c4c7..fbee0004dd 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -840,6 +840,9 @@ "created": "作成日", "last_updated": "最終更新日", "messages": "メッセージ", + "notion": { + "reasoning_truncated": "思考過程がブロック分割できません。切り捨てられています。" + }, "user": "ユーザー" }, "files": { @@ -1257,7 +1260,8 @@ }, "notion": { "export": "Notionへのエクスポートに失敗しました。接続状態と設定を確認してください", - "no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません" + "no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません", + "no_content": "Notionにエクスポートできる内容がありません。" }, "siyuan": { "export": "思源ノートのエクスポートに失敗しました。接続状態を確認し、ドキュメントに従って設定を確認してください", @@ -1383,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! " - }, - "siyuan": { - "exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!" - }, - "yuque": { - "exporting": "語雀にエクスポート中です。重複してエクスポートしないでください!" + "export": { + "exporting": "他のエクスポートが実行中です。前のエクスポートが完了するまでお待ちください。" } }, "warning": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index e689aecebf..e9705991ff 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -840,6 +840,9 @@ "created": "Создано", "last_updated": "Последнее обновление", "messages": "Сообщения", + "notion": { + "reasoning_truncated": "Цепочка мыслей не может быть разбита на блоки, обрезана" + }, "user": "Пользователь" }, "files": { @@ -1257,7 +1260,8 @@ }, "notion": { "export": "Ошибка экспорта в Notion, пожалуйста, проверьте состояние подключения и настройки в документации", - "no_api_key": "Notion ApiKey или Notion DatabaseID не настроен" + "no_api_key": "Notion ApiKey или Notion DatabaseID не настроен", + "no_content": "Нет содержимого для экспорта в Notion" }, "siyuan": { "export": "Ошибка экспорта в Siyuan, пожалуйста, проверьте состояние подключения и настройки в документации", @@ -1383,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!" - }, - "siyuan": { - "exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!" - }, - "yuque": { - "exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!" + "export": { + "exporting": "Выполняется другая экспортация, подождите завершения предыдущей операции экспорта и повторите попытку" } }, "warning": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 9c0ee36467..ae65e9360a 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -840,6 +840,9 @@ "created": "创建时间", "last_updated": "最后更新", "messages": "消息数", + "notion": { + "reasoning_truncated": "思维链无法分块,已截断" + }, "user": "用户" }, "files": { @@ -1257,7 +1260,8 @@ }, "notion": { "export": "导出 Notion 错误,请检查连接状态并对照文档检查配置", - "no_api_key": "未配置 Notion API Key 或 Notion Database ID" + "no_api_key": "未配置 Notion API Key 或 Notion Database ID", + "no_content": "无可导出到 Notion 的内容" }, "siyuan": { "export": "导出思源笔记失败,请检查连接状态并对照文档检查配置", @@ -1383,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "正在导出到 Notion, 请勿重复请求导出!" - }, - "siyuan": { - "exporting": "正在导出到思源笔记,请勿重复请求导出!" - }, - "yuque": { - "exporting": "正在导出语雀,请勿重复请求导出!" + "export": { + "exporting": "正在进行其他导出,请等待上一导出完成后重试" } }, "warning": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8a32912fd9..442f957bf1 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -840,6 +840,9 @@ "created": "建立時間", "last_updated": "最後更新", "messages": "訊息數", + "notion": { + "reasoning_truncated": "思維鏈無法分塊,已截斷" + }, "user": "使用者" }, "files": { @@ -1257,7 +1260,8 @@ }, "notion": { "export": "匯出 Notion 錯誤,請檢查連接狀態並對照文件檢查設定", - "no_api_key": "未設定 Notion API Key 或 Notion Database ID" + "no_api_key": "未設定 Notion API Key 或 Notion Database ID", + "no_content": "沒有可匯出至 Notion 的內容" }, "siyuan": { "export": "導出思源筆記失敗,請檢查連接狀態並對照文檔檢查配置", @@ -1383,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "正在匯出到 Notion,請勿重複請求匯出!" - }, - "siyuan": { - "exporting": "正在導出到思源筆記,請勿重複請求導出!" - }, - "yuque": { - "exporting": "正在導出語雀,請勿重複請求導出!" + "export": { + "exporting": "正在進行其他匯出,請等待上一次匯出完成後再試" } }, "warning": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index d3e74849f9..a35bed49d3 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -648,6 +648,31 @@ }, "translate": "Μετάφραση" }, + "code": { + "auto_update_to_latest": "Έλεγχος για ενημερώσεις και εγκατάσταση της τελευταίας έκδοσης", + "bun_required_message": "Για τη λειτουργία του εργαλείου CLI πρέπει να εγκαταστήσετε το περιβάλλον Bun", + "cli_tool": "Εργαλείο CLI", + "cli_tool_placeholder": "Επιλέξτε το CLI εργαλείο που θέλετε να χρησιμοποιήσετε", + "description": "Εκκίνηση γρήγορα πολλών εργαλείων CLI κώδικα, για αύξηση της αποδοτικότητας ανάπτυξης", + "folder_placeholder": "Επιλέξτε κατάλογο εργασίας", + "install_bun": "Εγκατάσταση Bun", + "installing_bun": "Εγκατάσταση...", + "launch": { + "bun_required": "Παρακαλώ εγκαταστήστε πρώτα το περιβάλλον Bun πριν εκκινήσετε το εργαλείο CLI", + "error": "Η εκκίνηση απέτυχε, παρακαλώ δοκιμάστε ξανά", + "label": "Εκκίνηση", + "success": "Επιτυχής εκκίνηση", + "validation_error": "Συμπληρώστε όλα τα υποχρεωτικά πεδία: εργαλείο CLI, μοντέλο και κατάλογος εργασίας" + }, + "launching": "Εκκίνηση...", + "model": "μοντέλο", + "model_placeholder": "Επιλέξτε το μοντέλο που θα χρησιμοποιήσετε", + "model_required": "Επιλέξτε μοντέλο", + "select_folder": "Επιλογή φακέλου", + "title": "Εργαλεία κώδικα", + "update_options": "Ενημέρωση επιλογών", + "working_directory": "κατάλογος εργασίας" + }, "code_block": { "collapse": "συμπεριληφθείς", "copy": { @@ -815,6 +840,9 @@ "created": "Ημερομηνία Δημιουργίας", "last_updated": "Τελευταία ενημέρωση", "messages": "Αριθμός Μηνυμάτων", + "notion": { + "reasoning_truncated": "Η αλυσίδα σκέψης δεν μπορεί να διαιρεθεί, έχει κοπεί" + }, "user": "Χρήστης" }, "files": { @@ -1232,7 +1260,8 @@ }, "notion": { "export": "Σφάλμα στην εξαγωγή του Notion, παρακαλείστε να ελέγξετε τη σύνδεση και τη διαμόρφωση κατά τη διατύπωση του χειρισμού", - "no_api_key": "Δεν έχετε διαθέσιμο το API Key του Notion ή το ID της βάσης του Notion" + "no_api_key": "Δεν έχετε διαθέσιμο το API Key του Notion ή το ID της βάσης του Notion", + "no_content": "Δεν υπάρχει περιεχόμενο για εξαγωγή στο Notion" }, "siyuan": { "export": "Η έκθεση σημειώσεων Siyuan απέτυχε, ελέγξτε την κατάσταση σύνδεσης και τις ρυθμίσεις σύμφωνα με τα έγγραφα", @@ -1358,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "Εξαγωγή στο Notion, μην επαναλάβετε την διαδικασία εξαγωγής!" - }, - "siyuan": { - "exporting": "Γίνεται εξαγωγή στις σημειώσεις Siyuan· μην ξαναζητήσετε την έκθεση!" - }, - "yuque": { - "exporting": "Γίνεται έκθεση Yuque· μην ξαναζητήσετε την έκθεση!" + "export": { + "exporting": "Παρακαλώ περιμένετε την ολοκλήρωση της προηγούμενης εξαγωγής. Εκτελείται άλλη εξαγωγή." } }, "warning": { @@ -2690,6 +2713,17 @@ "title": "Εκκίνηση", "totray": "Εισαγωγή στην συνδρομή κατά την εκκίνηση" }, + "math": { + "engine": { + "label": "Μηχανισμός μαθηματικών τύπων", + "none": "κανένα" + }, + "single_dollar": { + "label": "ενεργοποίηση $...$", + "tip": "Επεξεργασία μαθηματικών τύπων που περικλείονται σε ένα μόνο σύμβολο δολαρίου $...$, προεπιλογή ενεργοποιημένη." + }, + "title": "Ρύθμιση μαθηματικών τύπων" + }, "mcp": { "actions": "Ενέργειες", "active": "Ενεργοποίηση", @@ -2920,10 +2954,6 @@ "title": "Ρυθμίσεις εισαγωγής" }, "markdown_rendering_input_message": "Markdown Rendering Input Message", - "math_engine": { - "label": "Μηχανική μαθηματικών εξισώσεων", - "none": "Κανένα" - }, "metrics": "Χρόνος πρώτου χαρακτήρα {{time_first_token_millsec}}ms | {{token_speed}} tokens ανά δευτερόλεπτο", "model": { "title": "Ρυθμίσεις μοντέλου" @@ -2935,6 +2965,7 @@ "none": "Χωρίς εμφάνιση" }, "prompt": "Λήμμα προτροπής", + "show_message_outline": "Εμφάνιση πλαισίου μηνύματος", "title": "Ρυθμίσεις μηνυμάτων", "use_serif_font": "Χρήση μορφής Serif" }, @@ -3561,6 +3592,7 @@ "title": { "agents": "Πράκτορες", "apps": "Εφαρμογές", + "code": "Κώδικας", "files": "Αρχεία", "home": "Αρχική Σελίδα", "knowledge": "Βάση Γνώσης", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 9352f4635d..559eedc98d 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -648,6 +648,31 @@ }, "translate": "Traducir" }, + "code": { + "auto_update_to_latest": "Comprobar actualizaciones e instalar la versión más reciente", + "bun_required_message": "Se requiere instalar el entorno Bun para ejecutar la herramienta de línea de comandos", + "cli_tool": "Herramienta de línea de comandos", + "cli_tool_placeholder": "Seleccione la herramienta de línea de comandos que desea utilizar", + "description": "Inicia rápidamente múltiples herramientas de línea de comandos para código, aumentando la eficiencia del desarrollo", + "folder_placeholder": "Seleccionar directorio de trabajo", + "install_bun": "Instalar Bun", + "installing_bun": "Instalando...", + "launch": { + "bun_required": "Instale el entorno Bun antes de iniciar la herramienta de línea de comandos", + "error": "Error al iniciar, intente nuevamente", + "label": "Iniciar", + "success": "Inicio exitoso", + "validation_error": "Complete all required fields: CLI tool, model, and working directory" + }, + "launching": "Iniciando...", + "model": "modelo", + "model_placeholder": "Seleccionar el modelo que se va a utilizar", + "model_required": "Seleccione el modelo", + "select_folder": "Seleccionar carpeta", + "title": "Herramientas de código", + "update_options": "Opciones de actualización", + "working_directory": "directorio de trabajo" + }, "code_block": { "collapse": "Replegar", "copy": { @@ -815,6 +840,9 @@ "created": "Fecha de creación", "last_updated": "Última actualización", "messages": "Mensajes", + "notion": { + "reasoning_truncated": "La cadena de pensamiento no se puede dividir en bloques, ha sido truncada" + }, "user": "Usuario" }, "files": { @@ -1232,7 +1260,8 @@ }, "notion": { "export": "Error de exportación de Notion, verifique el estado de conexión y la configuración según la documentación", - "no_api_key": "No se ha configurado la clave API de Notion o la ID de la base de datos de Notion" + "no_api_key": "No se ha configurado la clave API de Notion o la ID de la base de datos de Notion", + "no_content": "No hay contenido que exportar a Notion" }, "siyuan": { "export": "Error al exportar la nota de Siyuan, verifique el estado de la conexión y revise la configuración según la documentación", @@ -1358,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "Se está exportando a Notion, ¡no solicite nuevamente la exportación!" - }, - "siyuan": { - "exporting": "Exportando a Siyuan, ¡no solicite la exportación nuevamente!" - }, - "yuque": { - "exporting": "Exportando Yuque, ¡no solicite la exportación nuevamente!" + "export": { + "exporting": "Realizando otra exportación, espere a que finalice la anterior para intentarlo de nuevo" } }, "warning": { @@ -2690,6 +2713,17 @@ "title": "Inicio", "totray": "Minimizar a la bandeja al iniciar" }, + "math": { + "engine": { + "label": "Motor de fórmulas matemáticas", + "none": "sin contenido" + }, + "single_dollar": { + "label": "habilitar $...$", + "tip": "Renderiza fórmulas matemáticas encerradas entre un único símbolo de dólar $...$, habilitado por defecto." + }, + "title": "Configuración de fórmulas matemáticas" + }, "mcp": { "actions": "Acciones", "active": "Activar", @@ -2920,10 +2954,6 @@ "title": "Configuración de entrada" }, "markdown_rendering_input_message": "Renderizar mensajes de entrada en Markdown", - "math_engine": { - "label": "Motor de fórmulas matemáticas", - "none": "Ninguno" - }, "metrics": "Retraso inicial {{time_first_token_millsec}}ms | {{token_speed}} tokens por segundo", "model": { "title": "Configuración del modelo" @@ -2935,6 +2965,7 @@ "none": "No mostrar" }, "prompt": "Palabra de indicación", + "show_message_outline": "Mostrar esquema del mensaje", "title": "Configuración de mensajes", "use_serif_font": "Usar fuente serif" }, @@ -3561,6 +3592,7 @@ "title": { "agents": "Agentes", "apps": "Aplicaciones", + "code": "Código", "files": "Archivos", "home": "Inicio", "knowledge": "Base de conocimiento", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index b0c4beee9b..b2e996a7ad 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -648,6 +648,31 @@ }, "translate": "Traduire" }, + "code": { + "auto_update_to_latest": "Vérifier les mises à jour et installer la dernière version", + "bun_required_message": "L'exécution de l'outil en ligne de commande nécessite l'installation de l'environnement Bun", + "cli_tool": "Outil CLI", + "cli_tool_placeholder": "Sélectionnez l'outil CLI à utiliser", + "description": "Lancer rapidement plusieurs outils CLI de code pour améliorer l'efficacité du développement", + "folder_placeholder": "Sélectionner le répertoire de travail", + "install_bun": "Installer Bun", + "installing_bun": "Installation en cours...", + "launch": { + "bun_required": "Veuillez d'abord installer l'environnement Bun avant de lancer l'outil en ligne de commande", + "error": "Échec du démarrage, veuillez réessayer", + "label": "Démarrer", + "success": "Démarrage réussi", + "validation_error": "Veuillez remplir tous les champs obligatoires : outil CLI, modèle et répertoire de travail" + }, + "launching": "En cours de démarrage...", + "model": "modèle", + "model_placeholder": "Sélectionnez le modèle à utiliser", + "model_required": "Veuillez sélectionner le modèle", + "select_folder": "Sélectionner le dossier", + "title": "Outils de code", + "update_options": "Options de mise à jour", + "working_directory": "répertoire de travail" + }, "code_block": { "collapse": "Réduire", "copy": { @@ -815,6 +840,9 @@ "created": "Date de création", "last_updated": "Dernière mise à jour", "messages": "Messages", + "notion": { + "reasoning_truncated": "La chaîne de pensée ne peut pas être fractionnée, elle a été tronquée." + }, "user": "Utilisateur" }, "files": { @@ -1232,7 +1260,8 @@ }, "notion": { "export": "Erreur lors de l'exportation vers Notion, veuillez vérifier l'état de la connexion et la configuration dans la documentation", - "no_api_key": "Aucune clé API Notion ou ID de base de données Notion configurée" + "no_api_key": "Aucune clé API Notion ou ID de base de données Notion configurée", + "no_content": "Aucun contenu à exporter vers Notion" }, "siyuan": { "export": "Échec de l'exportation de la note Siyuan, veuillez vérifier l'état de la connexion et la configuration indiquée dans le document", @@ -1358,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "Exportation en cours vers Notion, veuillez ne pas faire plusieurs demandes d'exportation!" - }, - "siyuan": { - "exporting": "Exportation vers Siyuan en cours, veuillez ne pas demander à exporter à nouveau !" - }, - "yuque": { - "exporting": "Exportation Yuque en cours, veuillez ne pas demander à exporter à nouveau !" + "export": { + "exporting": "Une autre exportation est en cours, veuillez patienter jusqu'à la fin de l'exportation précédente pour réessayer." } }, "warning": { @@ -2690,6 +2713,17 @@ "title": "Démarrage", "totray": "Minimiser dans la barre d'état système au démarrage" }, + "math": { + "engine": { + "label": "Moteur de formules mathématiques", + "none": "Aucun" + }, + "single_dollar": { + "label": "activer $...$", + "tip": "Rendu des formules mathématiques encapsulées par un seul symbole dollar $...$, activé par défaut." + }, + "title": "Configuration des formules mathématiques" + }, "mcp": { "actions": "Actions", "active": "Activer", @@ -2920,10 +2954,6 @@ "title": "Paramètres d'entrée" }, "markdown_rendering_input_message": "Rendu Markdown des messages d'entrée", - "math_engine": { - "label": "Moteur de formules mathématiques", - "none": "Aucun" - }, "metrics": "Latence initiale {{time_first_token_millsec}}ms | Vitesse de tokenisation {{token_speed}} tokens/s", "model": { "title": "Paramètres du modèle" @@ -2935,6 +2965,7 @@ "none": "Ne pas afficher" }, "prompt": "Mot-clé d'affichage", + "show_message_outline": "Afficher le plan du message", "title": "Paramètres des messages", "use_serif_font": "Utiliser une police serif" }, @@ -3561,6 +3592,7 @@ "title": { "agents": "Agent intelligent", "apps": "Mini-programmes", + "code": "Code", "files": "Fichiers", "home": "Page d'accueil", "knowledge": "Base de connaissances", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 72f2dd5998..be07031b5f 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -648,6 +648,31 @@ }, "translate": "Traduzir" }, + "code": { + "auto_update_to_latest": "Verificar atualizações e instalar a versão mais recente", + "bun_required_message": "Executar a ferramenta CLI requer a instalação do ambiente Bun", + "cli_tool": "Ferramenta de linha de comando", + "cli_tool_placeholder": "Selecione a ferramenta de linha de comando a ser utilizada", + "description": "Inicie rapidamente várias ferramentas de linha de comando de código, aumentando a eficiência do desenvolvimento", + "folder_placeholder": "Selecionar diretório de trabalho", + "install_bun": "Instalar o Bun", + "installing_bun": "Instalando...", + "launch": { + "bun_required": "Instale o ambiente Bun antes de iniciar a ferramenta de linha de comando", + "error": "Falha ao iniciar, tente novamente", + "label": "iniciar", + "success": "Início bem-sucedido", + "validation_error": "Preencha todos os campos obrigatórios: ferramenta CLI, modelo e diretório de trabalho" + }, + "launching": "Iniciando...", + "model": "modelo", + "model_placeholder": "Selecione o modelo a ser utilizado", + "model_required": "Selecione o modelo", + "select_folder": "Selecionar pasta", + "title": "Ferramenta de código", + "update_options": "Opções de atualização", + "working_directory": "diretório de trabalho" + }, "code_block": { "collapse": "Recolher", "copy": { @@ -815,6 +840,9 @@ "created": "Criado em", "last_updated": "Última Atualização", "messages": "Mensagens", + "notion": { + "reasoning_truncated": "A cadeia de pensamento não pode ser dividida em partes, foi interrompida" + }, "user": "Usuário" }, "files": { @@ -1232,7 +1260,8 @@ }, "notion": { "export": "Erro ao exportar Notion, verifique o status da conexão e a configuração de acordo com a documentação", - "no_api_key": "API Key ou Notion Database ID não configurados" + "no_api_key": "API Key ou Notion Database ID não configurados", + "no_content": "Nenhum conteúdo para exportar para o Notion" }, "siyuan": { "export": "Falha ao exportar nota do Siyuan, verifique o estado da conexão e confira a configuração no documento", @@ -1358,14 +1387,8 @@ } }, "warn": { - "notion": { - "exporting": "Exportando para Notion, não solicite novamente a exportação!" - }, - "siyuan": { - "exporting": "Exportando para o Siyuan, por favor não solicite a exportação novamente!" - }, - "yuque": { - "exporting": "Exportando para Yuque, por favor não solicite a exportação novamente!" + "export": { + "exporting": "A exportação de outros arquivos está em andamento, aguarde a conclusão da exportação anterior e tente novamente." } }, "warning": { @@ -2690,6 +2713,17 @@ "title": "Inicialização", "totray": "Minimizar para bandeja ao iniciar" }, + "math": { + "engine": { + "label": "Motor de fórmulas matemáticas", + "none": "sem conteúdo" + }, + "single_dollar": { + "label": "ativar $...$", + "tip": "Renderiza fórmulas matemáticas delimitadas por um único sinal de dólar $...$, habilitado por padrão." + }, + "title": "Configuração de fórmulas matemáticas" + }, "mcp": { "actions": "Ações", "active": "Ativar", @@ -2920,10 +2954,6 @@ "title": "Configurações de entrada" }, "markdown_rendering_input_message": "Renderização de markdown na entrada de mensagens", - "math_engine": { - "label": "Motor de fórmulas matemáticas", - "none": "Nenhum" - }, "metrics": "Atraso inicial {{time_first_token_millsec}}ms | Taxa de token por segundo {{token_speed}} tokens", "model": { "title": "Configurações de modelo" @@ -2935,6 +2965,7 @@ "none": "Não mostrar" }, "prompt": "Exibir palavra-chave", + "show_message_outline": "Exibir esboço da mensagem", "title": "Configurações de mensagem", "use_serif_font": "Usar fonte serif" }, @@ -3561,6 +3592,7 @@ "title": { "agents": "Agentes", "apps": "Miniaplicativos", + "code": "Código", "files": "Arquivos", "home": "Página Inicial", "knowledge": "Base de Conhecimento", diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index bc7d9b20ad..a7f1a03a6e 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -12,10 +12,112 @@ import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdow import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find' import { markdownToBlocks } from '@tryfabric/martian' import dayjs from 'dayjs' +import DOMPurify from 'dompurify' import { appendBlocks } from 'notion-helper' // 引入 notion-helper 的 appendBlocks 函数 const logger = loggerService.withContext('Utils:export') +// 全局的导出状态获取函数 +const getExportState = () => store.getState().runtime.export.isExporting + +// 全局的导出状态设置函数,使用 dispatch 保障 Redux 状态更新正确 +const setExportingState = (isExporting: boolean) => { + store.dispatch(setExportState({ isExporting })) +} + +/** + * 安全地处理思维链内容,保留安全的 HTML 标签如
,移除危险内容 + * + * 支持的标签: + * - 结构:br, p, div, span, h1-h6, blockquote + * - 格式:strong, b, em, i, u, s, del, mark, small, sup, sub + * - 列表:ul, ol, li + * - 代码:code, pre, kbd, var, samp + * - 表格:table, thead, tbody, tfoot, tr, td, th + * + * @param content 原始思维链内容 + * @returns 安全处理后的内容 + */ +const sanitizeReasoningContent = (content: string): string => { + // 先处理换行符转换为
+ const contentWithBr = content.replace(/\n/g, '
') + + // 使用 DOMPurify 清理内容,保留常用的安全标签和属性 + const cleanContent = DOMPurify.sanitize(contentWithBr, { + ALLOWED_TAGS: [ + // 换行和基础结构 + 'br', + 'p', + 'div', + 'span', + // 文本格式化 + 'strong', + 'b', + 'em', + 'i', + 'u', + 's', + 'del', + 'mark', + 'small', + // 上标下标(数学公式、引用等) + 'sup', + 'sub', + // 标题 + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + // 引用 + 'blockquote', + // 列表 + 'ul', + 'ol', + 'li', + // 代码相关 + 'code', + 'pre', + 'kbd', + 'var', + 'samp', + // 表格(AI输出中可能包含表格) + 'table', + 'thead', + 'tbody', + 'tfoot', + 'tr', + 'td', + 'th', + // 分隔线 + 'hr' + ], + ALLOWED_ATTR: [ + // 安全的通用属性 + 'class', + 'title', + 'lang', + 'dir', + // code 标签的语言属性 + 'data-language', + // 表格属性 + 'colspan', + 'rowspan', + // 列表属性 + 'start', + 'type' + ], + KEEP_CONTENT: true, // 保留被移除标签的文本内容 + RETURN_DOM: false, + SANITIZE_DOM: true, + // 允许的协议(预留,虽然目前没有允许链接标签) + ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i + }) + + return cleanContent +} + /** * 获取话题的消息列表,使用TopicManager确保消息被正确加载 * 这样可以避免从未打开过的话题导出为空的问题 @@ -33,7 +135,7 @@ async function fetchTopicMessages(topicId: string): Promise { * @param {number} [length=80] 标题最大长度,默认为 80 * @returns {string} 提取的标题 */ -export function getTitleFromString(str: string, length: number = 80) { +export function getTitleFromString(str: string, length: number = 80): string { let title = str.trimStart().split('\n')[0] if (title.includes('。')) { @@ -57,7 +159,7 @@ export function getTitleFromString(str: string, length: number = 80) { return title } -const getRoleText = (role: string, modelName?: string, providerId?: string) => { +const getRoleText = (role: string, modelName?: string, providerId?: string): string => { const { showModelNameInMarkdown, showModelProviderInMarkdown } = store.getState().settings if (role === 'user') { @@ -166,7 +268,7 @@ const createBaseMarkdown = ( includeReasoning: boolean = false, excludeCitations: boolean = false, normalizeCitations: boolean = true -) => { +): { titleSection: string; reasoningSection: string; contentSection: string; citation: string } => { const { forceDollarMathInMarkdown } = store.getState().settings const roleText = getRoleText(message.role, message.model?.name, message.model?.provider) const titleSection = `## ${roleText}` @@ -180,13 +282,8 @@ const createBaseMarkdown = ( } else if (reasoningContent.startsWith('')) { reasoningContent = reasoningContent.substring(7) } - reasoningContent = reasoningContent - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\n/g, '
') + // 使用 DOMPurify 安全地处理思维链内容 + reasoningContent = sanitizeReasoningContent(reasoningContent) if (forceDollarMathInMarkdown) { reasoningContent = convertMathFormula(reasoningContent) } @@ -216,7 +313,7 @@ const createBaseMarkdown = ( return { titleSection, reasoningSection, contentSection: processedContent, citation } } -export const messageToMarkdown = (message: Message, excludeCitations?: boolean) => { +export const messageToMarkdown = (message: Message, excludeCitations?: boolean): string => { const { excludeCitationsInExport, standardizeCitationsInExport } = store.getState().settings const shouldExcludeCitations = excludeCitations ?? excludeCitationsInExport const { titleSection, contentSection, citation } = createBaseMarkdown( @@ -228,7 +325,7 @@ export const messageToMarkdown = (message: Message, excludeCitations?: boolean) return [titleSection, '', contentSection, citation].join('\n') } -export const messageToMarkdownWithReasoning = (message: Message, excludeCitations?: boolean) => { +export const messageToMarkdownWithReasoning = (message: Message, excludeCitations?: boolean): string => { const { excludeCitationsInExport, standardizeCitationsInExport } = store.getState().settings const shouldExcludeCitations = excludeCitations ?? excludeCitationsInExport const { titleSection, reasoningSection, contentSection, citation } = createBaseMarkdown( @@ -237,10 +334,14 @@ export const messageToMarkdownWithReasoning = (message: Message, excludeCitation shouldExcludeCitations, standardizeCitationsInExport ) - return [titleSection, '', reasoningSection + contentSection, citation].join('\n') + return [titleSection, '', reasoningSection, contentSection, citation].join('\n') } -export const messagesToMarkdown = (messages: Message[], exportReasoning?: boolean, excludeCitations?: boolean) => { +export const messagesToMarkdown = ( + messages: Message[], + exportReasoning?: boolean, + excludeCitations?: boolean +): string => { return messages .map((message) => exportReasoning @@ -266,7 +367,11 @@ const messagesToPlainText = (messages: Message[]): string => { return messages.map(formatMessageAsPlainText).join('\n\n') } -export const topicToMarkdown = async (topic: Topic, exportReasoning?: boolean, excludeCitations?: boolean) => { +export const topicToMarkdown = async ( + topic: Topic, + exportReasoning?: boolean, + excludeCitations?: boolean +): Promise => { const topicName = `# ${topic.name}` const messages = await fetchTopicMessages(topic.id) @@ -290,7 +395,18 @@ export const topicToPlainText = async (topic: Topic): Promise => { return topicName } -export const exportTopicAsMarkdown = async (topic: Topic, exportReasoning?: boolean, excludeCitations?: boolean) => { +export const exportTopicAsMarkdown = async ( + topic: Topic, + exportReasoning?: boolean, + excludeCitations?: boolean +): Promise => { + if (getExportState()) { + window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'markdown-exporting' }) + return + } + + setExportingState(true) + const { markdownExportPath } = store.getState().settings if (!markdownExportPath) { try { @@ -305,7 +421,9 @@ export const exportTopicAsMarkdown = async (topic: Topic, exportReasoning?: bool } } catch (error: any) { window.message.error({ content: i18n.t('message.error.markdown.export.specified'), key: 'markdown-error' }) - logger.debug(error) + logger.error('Failed to export topic as markdown:', error) + } finally { + setExportingState(false) } } else { try { @@ -316,7 +434,9 @@ export const exportTopicAsMarkdown = async (topic: Topic, exportReasoning?: bool window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' }) } catch (error: any) { window.message.error({ content: i18n.t('message.error.markdown.export.preconf'), key: 'markdown-error' }) - logger.debug(error) + logger.error('Failed to export topic as markdown:', error) + } finally { + setExportingState(false) } } } @@ -325,7 +445,14 @@ export const exportMessageAsMarkdown = async ( message: Message, exportReasoning?: boolean, excludeCitations?: boolean -) => { +): Promise => { + if (getExportState()) { + window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'markdown-exporting' }) + return + } + + setExportingState(true) + const { markdownExportPath } = store.getState().settings if (!markdownExportPath) { try { @@ -343,7 +470,9 @@ export const exportMessageAsMarkdown = async ( } } catch (error: any) { window.message.error({ content: i18n.t('message.error.markdown.export.specified'), key: 'markdown-error' }) - logger.debug(error) + logger.error('Failed to export message as markdown:', error) + } finally { + setExportingState(false) } } else { try { @@ -357,12 +486,14 @@ export const exportMessageAsMarkdown = async ( window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' }) } catch (error: any) { window.message.error({ content: i18n.t('message.error.markdown.export.preconf'), key: 'markdown-error' }) - logger.debug(error) + logger.error('Failed to export message as markdown:', error) + } finally { + setExportingState(false) } } } -const convertMarkdownToNotionBlocks = async (markdown: string) => { +const convertMarkdownToNotionBlocks = async (markdown: string): Promise => { return markdownToBlocks(markdown) } @@ -371,77 +502,109 @@ const convertThinkingToNotionBlocks = async (thinkingContent: string): Promise标签转换为真正的换行符 + const processedContent = thinkingContent.replace(//g, '\n') - return thinkingBlocks + // 使用 markdownToBlocks 处理思维链内容 + const childrenBlocks = markdownToBlocks(processedContent) + + return [ + { + object: 'block', + type: 'toggle', + toggle: { + rich_text: [ + { + type: 'text', + text: { + content: '🤔 ' + i18n.t('common.reasoning_content') + }, + annotations: { + bold: true + } + } + ], + children: childrenBlocks + } + } + ] + } catch (error) { + logger.error('failed to process reasoning content:', error as Error) + // 发生错误时,回退到简单的段落处理 + return [ + { + object: 'block', + type: 'toggle', + toggle: { + rich_text: [ + { + type: 'text', + text: { + content: '🤔 ' + i18n.t('common.reasoning_content') + }, + annotations: { + bold: true + } + } + ], + children: [ + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [ + { + type: 'text', + text: { + content: + thinkingContent.length > 1800 + ? thinkingContent.substring(0, 1800) + '...\n' + i18n.t('export.notion.reasoning_truncated') + : thinkingContent + } + } + ] + } + } + ] + } + } + ] + } } -const executeNotionExport = async (title: string, allBlocks: any[]): Promise => { - const { isExporting } = store.getState().runtime.export - if (isExporting) { - window.message.warning({ content: i18n.t('message.warn.notion.exporting'), key: 'notion-exporting' }) - return null +const executeNotionExport = async (title: string, allBlocks: any[]): Promise => { + if (getExportState()) { + window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'notion-exporting' }) + return false } - setExportState({ isExporting: true }) - - title = title.slice(0, 29) + '...' - const { notionDatabaseID, notionApiKey } = store.getState().settings if (!notionApiKey || !notionDatabaseID) { window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' }) - setExportState({ isExporting: false }) - return null + return false + } + + if (allBlocks.length === 0) { + window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-no-content-error' }) + return false + } + + setExportingState(true) + + // 限制标题长度 + if (title.length > 32) { + title = title.slice(0, 29) + '...' } try { const notion = new Client({ auth: notionApiKey }) - if (allBlocks.length === 0) { - throw new Error('No content to export') - } - window.message.loading({ content: i18n.t('message.loading.notion.preparing'), key: 'notion-preparing', duration: 0 }) - let mainPageResponse: any = null - let parentBlockId: string | null = null const response = await notion.pages.create({ parent: { database_id: notionDatabaseID }, @@ -451,34 +614,37 @@ const executeNotionExport = async (title: string, allBlocks: any[]): Promise 0) { - await appendBlocks({ - block_id: parentBlockId, - children: allBlocks, - client: notion - }) - } + + await appendBlocks({ + block_id: response.id, + children: allBlocks, + client: notion + }) + window.message.destroy('notion-exporting') window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-success' }) - return mainPageResponse + return true } catch (error: any) { - window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-export-progress' }) - logger.debug(error) - return null + // 清理可能存在的loading消息 + window.message.destroy('notion-preparing') + window.message.destroy('notion-exporting') + + logger.error('Notion export failed:', error) + window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-export-error' }) + return false } finally { - setExportState({ isExporting: false }) + setExportingState(false) } } -export const exportMessageToNotion = async (title: string, content: string, message?: Message) => { +export const exportMessageToNotion = async (title: string, content: string, message?: Message): Promise => { const { notionExportReasoning } = store.getState().settings const notionBlocks = await convertMarkdownToNotionBlocks(content) @@ -498,7 +664,7 @@ export const exportMessageToNotion = async (title: string, content: string, mess return executeNotionExport(title, notionBlocks) } -export const exportTopicToNotion = async (topic: Topic) => { +export const exportTopicToNotion = async (topic: Topic): Promise => { const { notionExportReasoning, excludeCitationsInExport } = store.getState().settings const topicMessages = await fetchTopicMessages(topic.id) @@ -532,12 +698,11 @@ export const exportTopicToNotion = async (topic: Topic) => { return executeNotionExport(topic.name, allBlocks) } -export const exportMarkdownToYuque = async (title: string, content: string) => { - const { isExporting } = store.getState().runtime.export +export const exportMarkdownToYuque = async (title: string, content: string): Promise => { const { yuqueToken, yuqueRepoId } = store.getState().settings - if (isExporting) { - window.message.warning({ content: i18n.t('message.warn.yuque.exporting'), key: 'yuque-exporting' }) + if (getExportState()) { + window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'yuque-exporting' }) return } @@ -546,7 +711,7 @@ export const exportMarkdownToYuque = async (title: string, content: string) => { return } - setExportState({ isExporting: true }) + setExportingState(true) try { const response = await fetch(`https://www.yuque.com/api/v2/repos/${yuqueRepoId}/docs`, { @@ -602,7 +767,7 @@ export const exportMarkdownToYuque = async (title: string, content: string) => { }) return null } finally { - setExportState({ isExporting: false }) + setExportingState(false) } } @@ -617,7 +782,14 @@ export const exportMarkdownToYuque = async (title: string, content: string) => { * @param attributes.folder 选择的文件夹路径或文件路径 * @param attributes.vault 选择的Vault名称 */ -export const exportMarkdownToObsidian = async (attributes: any) => { +export const exportMarkdownToObsidian = async (attributes: any): Promise => { + if (getExportState()) { + window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'obsidian-exporting' }) + return + } + + setExportingState(true) + try { // 从参数获取Vault名称 const obsidianVault = attributes.vault @@ -669,8 +841,10 @@ export const exportMarkdownToObsidian = async (attributes: any) => { window.open(obsidianUrl) window.message.success(i18n.t('chat.topics.export.obsidian_export_success')) } catch (error) { - logger.error('导出到Obsidian失败:', error as Error) + logger.error('Failed to export to Obsidian:', error as Error) window.message.error(i18n.t('chat.topics.export.obsidian_export_failed')) + } finally { + setExportingState(false) } } @@ -719,14 +893,24 @@ function transformObsidianFileName(fileName: string): string { return sanitized } -export const exportMarkdownToJoplin = async (title: string, contentOrMessages: string | Message | Message[]) => { +export const exportMarkdownToJoplin = async ( + title: string, + contentOrMessages: string | Message | Message[] +): Promise => { const { joplinUrl, joplinToken, joplinExportReasoning, excludeCitationsInExport } = store.getState().settings + if (getExportState()) { + window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'joplin-exporting' }) + return + } + if (!joplinUrl || !joplinToken) { window.message.error(i18n.t('message.error.joplin.no_config')) return } + setExportingState(true) + let content: string if (typeof contentOrMessages === 'string') { content = contentOrMessages @@ -763,11 +947,13 @@ export const exportMarkdownToJoplin = async (title: string, contentOrMessages: s } window.message.success(i18n.t('message.success.joplin.export')) - return + return data } catch (error: any) { + logger.error('Failed to export to Joplin:', error) window.message.error(i18n.t('message.error.joplin.export')) - logger.debug(error) - return + return null + } finally { + setExportingState(false) } } @@ -776,12 +962,11 @@ export const exportMarkdownToJoplin = async (title: string, contentOrMessages: s * @param title 笔记标题 * @param content 笔记内容 */ -export const exportMarkdownToSiyuan = async (title: string, content: string) => { - const { isExporting } = store.getState().runtime.export +export const exportMarkdownToSiyuan = async (title: string, content: string): Promise => { const { siyuanApiUrl, siyuanToken, siyuanBoxId, siyuanRootPath } = store.getState().settings - if (isExporting) { - window.message.warning({ content: i18n.t('message.warn.siyuan.exporting'), key: 'siyuan-exporting' }) + if (getExportState()) { + window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'siyuan-exporting' }) return } @@ -790,7 +975,7 @@ export const exportMarkdownToSiyuan = async (title: string, content: string) => return } - setExportState({ isExporting: true }) + setExportingState(true) try { // test connection @@ -826,13 +1011,13 @@ export const exportMarkdownToSiyuan = async (title: string, content: string) => key: 'siyuan-success' }) } catch (error) { - logger.error('导出到思源笔记失败:', error as Error) + logger.error('Failed to export to Siyuan:', error as Error) window.message.error({ content: i18n.t('message.error.siyuan.export') + (error instanceof Error ? `: ${error.message}` : ''), key: 'siyuan-error' }) } finally { - setExportState({ isExporting: false }) + setExportingState(false) } } /** diff --git a/yarn.lock b/yarn.lock index 3a27d98704..276a523034 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8552,7 +8552,6 @@ __metadata: dexie-react-hooks: "npm:^1.1.7" diff: "npm:^7.0.0" docx: "npm:^9.0.2" - dompurify: "npm:^3.2.6" dotenv-cli: "npm:^7.4.2" electron: "npm:37.2.3" electron-builder: "npm:26.0.15" @@ -11446,7 +11445,7 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:^3.2.5, dompurify@npm:^3.2.6": +"dompurify@npm:^3.2.5": version: 3.2.6 resolution: "dompurify@npm:3.2.6" dependencies: