fix(export): robustly export reasoning and handle errors (#9221)

* fix(export): robustly export reasoning and handle errors

* fix(export): normalize <br> 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 <br> 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.
This commit is contained in:
George·Dong 2025-08-17 00:41:48 +08:00 committed by GitHub
parent 8b5a3f734c
commit 4dad2a593b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 511 additions and 210 deletions

View File

@ -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",

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": "Βάση Γνώσης",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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>
*
*
* - 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 => {
// 先处理换行符转换为 <br>
const contentWithBr = content.replace(/\n/g, '<br>')
// 使用 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<Message[]> {
* @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('<think>')) {
reasoningContent = reasoningContent.substring(7)
}
reasoningContent = reasoningContent
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/\n/g, '<br>')
// 使用 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<string> => {
const topicName = `# ${topic.name}`
const messages = await fetchTopicMessages(topic.id)
@ -290,7 +395,18 @@ export const topicToPlainText = async (topic: Topic): Promise<string> => {
return topicName
}
export const exportTopicAsMarkdown = async (topic: Topic, exportReasoning?: boolean, excludeCitations?: boolean) => {
export const exportTopicAsMarkdown = async (
topic: Topic,
exportReasoning?: boolean,
excludeCitations?: boolean
): Promise<void> => {
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<void> => {
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<any[]> => {
return markdownToBlocks(markdown)
}
@ -371,77 +502,109 @@ const convertThinkingToNotionBlocks = async (thinkingContent: string): Promise<a
return []
}
const thinkingBlocks = [
{
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
}
}
]
}
}
]
}
}
]
try {
// 预处理思维链内容将HTML的<br>标签转换为真正的换行符
const processedContent = thinkingContent.replace(/<br\s*\/?>/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<any> => {
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<boolean> => {
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<any
}
}
})
mainPageResponse = response
parentBlockId = response.id
window.message.destroy('notion-preparing')
window.message.loading({
content: i18n.t('message.loading.notion.exporting_progress'),
key: 'notion-exporting',
duration: 0
})
if (allBlocks.length > 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<boolean> => {
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<boolean> => {
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<any | null> => {
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<void> => {
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<any | null> => {
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<void> => {
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)
}
}
/**

View File

@ -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: