From 36f33fed75c26f92e8127e91aad2f747dff9fe08 Mon Sep 17 00:00:00 2001 From: one Date: Thu, 5 Jun 2025 09:33:26 +0800 Subject: [PATCH 01/10] fix: use monospace font for theme colorpicker (#6816) --- src/renderer/src/context/AntdProvider.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/src/context/AntdProvider.tsx b/src/renderer/src/context/AntdProvider.tsx index 737b26044..570da9fc4 100644 --- a/src/renderer/src/context/AntdProvider.tsx +++ b/src/renderer/src/context/AntdProvider.tsx @@ -45,6 +45,9 @@ const AntdProvider: FC = ({ children }) => { }, Tooltip: { fontSize: 13 + }, + ColorPicker: { + fontFamily: 'var(--code-font-family)' } }, token: { From 74f72fa5b61edea6e0c1ef8172eaf56a8190720a Mon Sep 17 00:00:00 2001 From: SuYao Date: Thu, 5 Jun 2025 09:33:40 +0800 Subject: [PATCH 02/10] fix(AnthropicProvider): update usage and metrics handling to prevent TypeError (#6813) --- .../providers/AiProvider/AnthropicProvider.ts | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts index 1f53f5d05..855c9adc6 100644 --- a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts +++ b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts @@ -445,6 +445,14 @@ export default class AnthropicProvider extends BaseProvider { ) } + if (thinking_content) { + onChunk({ + type: ChunkType.THINKING_COMPLETE, + text: thinking_content, + thinking_millsec: new Date().getTime() - time_first_token_millsec + }) + } + userMessages.push({ role: message.role, content: message.content @@ -464,18 +472,31 @@ export default class AnthropicProvider extends BaseProvider { } } - finalUsage.prompt_tokens += message.usage?.input_tokens || 0 - finalUsage.completion_tokens += message.usage?.output_tokens || 0 - finalUsage.total_tokens += finalUsage.prompt_tokens + finalUsage.completion_tokens - finalMetrics.completion_tokens = finalUsage.completion_tokens - finalMetrics.time_completion_millsec += new Date().getTime() - start_time_millsec - finalMetrics.time_first_token_millsec = time_first_token_millsec - start_time_millsec + // 直接修改finalUsage对象会报错,TypeError: Cannot assign to read only property 'prompt_tokens' of object '#' + // 暂未找到原因 + const updatedUsage: Usage = { + ...finalUsage, + prompt_tokens: finalUsage.prompt_tokens + (message.usage?.input_tokens || 0), + completion_tokens: finalUsage.completion_tokens + (message.usage?.output_tokens || 0) + } + updatedUsage.total_tokens = updatedUsage.prompt_tokens + updatedUsage.completion_tokens + + const updatedMetrics: Metrics = { + ...finalMetrics, + completion_tokens: updatedUsage.completion_tokens, + time_completion_millsec: + finalMetrics.time_completion_millsec + (new Date().getTime() - start_time_millsec), + time_first_token_millsec: time_first_token_millsec - start_time_millsec + } + + Object.assign(finalUsage, updatedUsage) + Object.assign(finalMetrics, updatedMetrics) onChunk({ type: ChunkType.BLOCK_COMPLETE, response: { - usage: finalUsage, - metrics: finalMetrics + usage: updatedUsage, + metrics: updatedMetrics } }) resolve() @@ -488,7 +509,9 @@ export default class AnthropicProvider extends BaseProvider { } onChunk({ type: ChunkType.LLM_RESPONSE_CREATED }) const start_time_millsec = new Date().getTime() - await processStream(body, 0).finally(cleanup) + await processStream(body, 0).finally(() => { + cleanup() + }) } /** From dcdf49a5ce24cc7433778c3cc0aab265c805d5eb Mon Sep 17 00:00:00 2001 From: Murphy <69335326+MurphyLo@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:44:11 +0800 Subject: [PATCH 03/10] fix: sync active topic after rename (#6804) Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com> --- src/renderer/src/pages/home/Tabs/TopicsTab.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 6f5b95484..490cfb3c2 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -174,7 +174,9 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic if (messages.length >= 2) { const summaryText = await fetchMessagesSummary({ messages, assistant }) if (summaryText) { - updateTopic({ ...topic, name: summaryText, isNameManuallyEdited: false }) + const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false } + updateTopic(updatedTopic) + topic.id === activeTopic.id && setActiveTopic(updatedTopic) } else { window.message?.error(t('message.error.fetchTopicName')) } @@ -192,7 +194,9 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic defaultValue: topic?.name || '' }) if (name && topic?.name !== name) { - updateTopic({ ...topic, name, isNameManuallyEdited: true }) + const updatedTopic = { ...topic, name, isNameManuallyEdited: true } + updateTopic(updatedTopic) + topic.id === activeTopic.id && setActiveTopic(updatedTopic) } } }, From f6462ef998f8a7bf31ceb49d4e8e14008959d654 Mon Sep 17 00:00:00 2001 From: Wang Jiyuan <59059173+EurFelux@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:09:37 +0800 Subject: [PATCH 04/10] fix: OpenAI provider api check doesn't handle error (#6769) --- .../AiProvider/OpenAIResponseProvider.ts | 44 ++++++++++--------- src/renderer/src/services/ApiService.ts | 11 ++++- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts index 6b2896533..d4375b08d 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts @@ -944,28 +944,32 @@ export abstract class BaseOpenAIProvider extends BaseProvider { if (!model) { return { valid: false, error: new Error('No model found') } } - if (stream) { - const response = await this.sdk.responses.create({ - model: model.id, - input: [{ role: 'user', content: 'hi' }], - stream: true - }) - for await (const chunk of response) { - if (chunk.type === 'response.output_text.delta') { - return { valid: true, error: null } + try { + if (stream) { + const response = await this.sdk.responses.create({ + model: model.id, + input: [{ role: 'user', content: 'hi' }], + stream: true + }) + for await (const chunk of response) { + if (chunk.type === 'response.output_text.delta') { + return { valid: true, error: null } + } } + return { valid: false, error: new Error('No streaming response') } + } else { + const response = await this.sdk.responses.create({ + model: model.id, + input: [{ role: 'user', content: 'hi' }], + stream: false + }) + if (!response.output_text) { + return { valid: false, error: new Error('No response') } + } + return { valid: true, error: null } } - throw new Error('Empty streaming response') - } else { - const response = await this.sdk.responses.create({ - model: model.id, - input: [{ role: 'user', content: 'hi' }], - stream: false - }) - if (!response.output_text) { - throw new Error('Empty response') - } - return { valid: true, error: null } + } catch (error: any) { + return { valid: false, error: error } } } diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 57f1e104b..d0ebdb4c2 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -471,7 +471,7 @@ export function checkApiProvider(provider: Provider): { } } -export async function checkApi(provider: Provider, model: Model) { +export async function checkApi(provider: Provider, model: Model): Promise<{ valid: boolean; error: Error | null }> { const validation = checkApiProvider(provider) if (!validation.valid) { return { @@ -484,9 +484,16 @@ export async function checkApi(provider: Provider, model: Model) { // Try streaming check first const result = await ai.check(model, true) + if (result.valid && !result.error) { return result } - return ai.check(model, false) + // 不应该假设错误由流式引发。多次发起检测请求可能触发429,掩盖了真正的问题。 + // 但这里错误类型做的很粗糙,暂时先这样 + if (result.error && result.error.message.includes('stream')) { + return ai.check(model, false) + } else { + return result + } } From e479ee3dbc3d60f775c7844be62474158c603289 Mon Sep 17 00:00:00 2001 From: SuYao Date: Thu, 5 Jun 2025 12:32:28 +0800 Subject: [PATCH 05/10] =?UTF-8?q?feat(constants):=20expand=20supported=20f?= =?UTF-8?q?ile=20extensions=20and=20categorize=20text=E2=80=A6=20(#6815)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(constants): expand supported file extensions and categorize text file types * refactor(constants): remove binary file extensions * refactor(constants): remove Xcode project --- packages/shared/config/constant.ts | 491 +++++++++++++++++++++-------- 1 file changed, 362 insertions(+), 129 deletions(-) diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 911a3cf90..e1fca4e6d 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -4,135 +4,368 @@ export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac'] export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods'] export const thirdPartyApplicationExts = ['.draftsExport'] export const bookExts = ['.epub'] -export const textExts = [ - '.txt', // 普通文本文件 - '.md', // Markdown 文件 - '.mdx', // Markdown 文件 - '.html', // HTML 文件 - '.htm', // HTML 文件的另一种扩展名 - '.xml', // XML 文件 - '.json', // JSON 文件 - '.yaml', // YAML 文件 - '.yml', // YAML 文件的另一种扩展名 - '.csv', // 逗号分隔值文件 - '.tsv', // 制表符分隔值文件 - '.ini', // 配置文件 - '.log', // 日志文件 - '.rtf', // 富文本格式文件 - '.org', // org-mode 文件 - '.wiki', // VimWiki 文件 - '.tex', // LaTeX 文件 - '.bib', // BibTeX 文件 - '.srt', // 字幕文件 - '.xhtml', // XHTML 文件 - '.nfo', // 信息文件(主要用于场景发布) - '.conf', // 配置文件 - '.config', // 配置文件 - '.env', // 环境变量文件 - '.rst', // reStructuredText 文件 - '.php', // PHP 脚本文件,包含嵌入的 HTML - '.js', // JavaScript 文件(部分是文本,部分可能包含代码) - '.ts', // TypeScript 文件 - '.jsp', // JavaServer Pages 文件 - '.aspx', // ASP.NET 文件 - '.bat', // Windows 批处理文件 - '.sh', // Unix/Linux Shell 脚本文件 - '.py', // Python 脚本文件 - '.ipynb', // Jupyter 笔记本格式 - '.rb', // Ruby 脚本文件 - '.pl', // Perl 脚本文件 - '.sql', // SQL 脚本文件 - '.css', // Cascading Style Sheets 文件 - '.less', // Less CSS 预处理器文件 - '.scss', // Sass CSS 预处理器文件 - '.sass', // Sass 文件 - '.styl', // Stylus CSS 预处理器文件 - '.coffee', // CoffeeScript 文件 - '.ino', // Arduino 代码文件 - '.asm', // Assembly 语言文件 - '.go', // Go 语言文件 - '.scala', // Scala 语言文件 - '.swift', // Swift 语言文件 - '.kt', // Kotlin 语言文件 - '.rs', // Rust 语言文件 - '.lua', // Lua 语言文件 - '.groovy', // Groovy 语言文件 - '.dart', // Dart 语言文件 - '.hs', // Haskell 语言文件 - '.clj', // Clojure 语言文件 - '.cljs', // ClojureScript 语言文件 - '.elm', // Elm 语言文件 - '.erl', // Erlang 语言文件 - '.ex', // Elixir 语言文件 - '.exs', // Elixir 脚本文件 - '.pug', // Pug (formerly Jade) 模板文件 - '.haml', // Haml 模板文件 - '.slim', // Slim 模板文件 - '.tpl', // 模板文件(通用) - '.ejs', // Embedded JavaScript 模板文件 - '.hbs', // Handlebars 模板文件 - '.mustache', // Mustache 模板文件 - '.jade', // Jade 模板文件 (已重命名为 Pug) - '.twig', // Twig 模板文件 - '.blade', // Blade 模板文件 (Laravel) - '.vue', // Vue.js 单文件组件 - '.jsx', // React JSX 文件 - '.tsx', // React TSX 文件 - '.graphql', // GraphQL 查询语言文件 - '.gql', // GraphQL 查询语言文件 - '.proto', // Protocol Buffers 文件 - '.thrift', // Thrift 文件 - '.toml', // TOML 配置文件 - '.edn', // Clojure 数据表示文件 - '.cake', // CakePHP 配置文件 - '.ctp', // CakePHP 视图文件 - '.cfm', // ColdFusion 标记语言文件 - '.cfc', // ColdFusion 组件文件 - '.m', // Objective-C 或 MATLAB 源文件 - '.mm', // Objective-C++ 源文件 - '.gradle', // Gradle 构建文件 - '.groovy', // Gradle 构建文件 - '.kts', // Kotlin Script 文件 - '.java', // Java 代码文件 - '.cs', // C# 代码文件 - '.cpp', // C++ 代码文件 - '.c', // C++ 代码文件 - '.h', // C++ 头文件 - '.hpp', // C++ 头文件 - '.cc', // C++ 源文件 - '.cxx', // C++ 源文件 - '.cppm', // C++20 模块接口文件 - '.ipp', // 模板实现文件 - '.ixx', // C++20 模块实现文件 - '.f90', // Fortran 90 源文件 - '.f', // Fortran 固定格式源代码文件 - '.f03', // Fortran 2003+ 源代码文件 - '.ahk', // AutoHotKey 语言文件 - '.tcl', // Tcl 脚本 - '.do', // Questa 或 Modelsim Tcl 脚本 - '.v', // Verilog 源文件 - '.sv', // SystemVerilog 源文件 - '.svh', // SystemVerilog 头文件 - '.vhd', // VHDL 源文件 - '.vhdl', // VHDL 源文件 - '.lef', // Library Exchange Format - '.def', // Design Exchange Format - '.edif', // Electronic Design Interchange Format - '.sdf', // Standard Delay Format - '.sdc', // Synopsys Design Constraints - '.xdc', // Xilinx Design Constraints - '.rpt', // 报告文件 - '.lisp', // Lisp 脚本 - '.il', // Cadence SKILL 脚本 - '.ils', // Cadence SKILL++ 脚本 - '.sp', // SPICE netlist 文件 - '.spi', // SPICE netlist 文件 - '.cir', // SPICE netlist 文件 - '.net', // SPICE netlist 文件 - '.scs', // Spectre netlist 文件 - '.asc', // LTspice netlist schematic 文件 - '.tf' // Technology File -] +const textExtsByCategory = new Map([ + [ + 'language', + [ + '.js', + '.mjs', + '.cjs', + '.ts', + '.jsx', + '.tsx', // JavaScript/TypeScript + '.py', // Python + '.java', // Java + '.cs', // C# + '.cpp', + '.c', + '.h', + '.hpp', + '.cc', + '.cxx', + '.cppm', + '.ipp', + '.ixx', // C/C++ + '.php', // PHP + '.rb', // Ruby + '.pl', // Perl + '.go', // Go + '.rs', // Rust + '.swift', // Swift + '.kt', + '.kts', // Kotlin + '.scala', // Scala + '.lua', // Lua + '.groovy', // Groovy + '.dart', // Dart + '.hs', // Haskell + '.clj', + '.cljs', // Clojure + '.elm', // Elm + '.erl', // Erlang + '.ex', + '.exs', // Elixir + '.ml', + '.mli', // OCaml + '.fs', // F# + '.r', + '.R', // R + '.sol', // Solidity + '.awk', // AWK + '.cob', // COBOL + '.asm', + '.s', // Assembly + '.lisp', + '.lsp', // Lisp + '.coffee', // CoffeeScript + '.ino', // Arduino + '.jl', // Julia + '.nim', // Nim + '.zig', // Zig + '.d', // D语言 + '.pas', // Pascal + '.vb', // Visual Basic + '.rkt', // Racket + '.scm', // Scheme + '.hx', // Haxe + '.as', // ActionScript + '.pde', // Processing + '.f90', + '.f', + '.f03', + '.for', + '.f95', // Fortran + '.adb', + '.ads', // Ada + '.pro', // Prolog + '.m', + '.mm', // Objective-C/MATLAB + '.rpy', // Ren'Py + '.ets', // OpenHarmony, + '.uniswap', // DeFi + '.vy', // Vyper + '.shader', + '.glsl', + '.frag', + '.vert', + '.gd' // Godot + ] + ], + [ + 'script', + [ + '.sh', // Shell + '.bat', + '.cmd', // Windows批处理 + '.ps1', // PowerShell + '.tcl', + '.do', // Tcl + '.ahk', // AutoHotkey + '.zsh', // Zsh + '.fish', // Fish shell + '.csh', // C shell + '.vbs', // VBScript + '.applescript', // AppleScript + '.au3', // AutoIt + '.bash', + '.nu' + ] + ], + [ + 'style', + [ + '.css', // CSS + '.less', // Less + '.scss', + '.sass', // Sass + '.styl', // Stylus + '.pcss', // PostCSS + '.postcss' // PostCSS + ] + ], + [ + 'template', + [ + '.vue', // Vue.js + '.pug', + '.jade', // Pug/Jade + '.haml', // Haml + '.slim', // Slim + '.tpl', // 通用模板 + '.ejs', // EJS + '.hbs', // Handlebars + '.mustache', // Mustache + '.twig', // Twig + '.blade', // Blade (Laravel) + '.liquid', // Liquid + '.jinja', + '.jinja2', + '.j2', // Jinja + '.erb', // ERB + '.vm', // Velocity + '.ftl', // FreeMarker + '.svelte', // Svelte + '.astro' // Astro + ] + ], + [ + 'config', + [ + '.ini', // INI配置 + '.conf', + '.config', // 通用配置 + '.env', // 环境变量 + '.toml', // TOML + '.cfg', // 通用配置 + '.properties', // Java属性 + '.desktop', // Linux桌面文件 + '.service', // systemd服务 + '.rc', + '.bashrc', + '.zshrc', // Shell配置 + '.fishrc', // Fish shell配置 + '.vimrc', // Vim配置 + '.htaccess', // Apache配置 + '.robots', // robots.txt + '.editorconfig', // EditorConfig + '.eslintrc', // ESLint + '.prettierrc', // Prettier + '.babelrc', // Babel + '.npmrc', // npm + '.dockerignore', // Docker ignore + '.npmignore', + '.yarnrc', + '.prettierignore', + '.eslintignore', + '.browserslistrc', + '.json5', + '.tfvars' + ] + ], + [ + 'document', + [ + '.txt', + '.text', // 纯文本 + '.md', + '.mdx', // Markdown + '.html', + '.htm', + '.xhtml', // HTML + '.xml', // XML + '.org', // Org-mode + '.wiki', // Wiki + '.tex', + '.bib', // LaTeX + '.rst', // reStructuredText + '.rtf', // 富文本 + '.nfo', // 信息文件 + '.adoc', + '.asciidoc', // AsciiDoc + '.pod', // Perl文档 + '.1', + '.2', + '.3', + '.4', + '.5', + '.6', + '.7', + '.8', + '.9', // man页面 + '.man', // man页面 + '.texi', + '.texinfo', // Texinfo + '.readme', + '.me', // README + '.changelog', // 变更日志 + '.license', // 许可证 + '.authors', // 作者文件 + '.po', + '.pot' + ] + ], + [ + 'data', + [ + '.json', // JSON + '.jsonc', // JSON with comments + '.yaml', + '.yml', // YAML + '.csv', + '.tsv', // 分隔值文件 + '.edn', // Clojure数据 + '.jsonl', + '.ndjson', // 换行分隔JSON + '.geojson', // GeoJSON + '.gpx', // GPS Exchange + '.kml', // Keyhole Markup + '.rss', + '.atom', // Feed格式 + '.vcf', // vCard + '.ics', // iCalendar + '.ldif', // LDAP数据交换 + '.pbtxt', + '.map' + ] + ], + [ + 'build', + [ + '.gradle', // Gradle + '.make', + '.mk', // Make + '.cmake', // CMake + '.sbt', // SBT + '.rake', // Rake + '.spec', // RPM spec + '.pom', + '.build', // Meson + '.bazel' // Bazel + ] + ], + [ + 'database', + [ + '.sql', // SQL + '.ddl', + '.dml', // DDL/DML + '.plsql', // PL/SQL + '.psql', // PostgreSQL + '.cypher', // Cypher + '.sparql' // SPARQL + ] + ], + [ + 'web', + [ + '.graphql', + '.gql', // GraphQL + '.proto', // Protocol Buffers + '.thrift', // Thrift + '.wsdl', // WSDL + '.raml', // RAML + '.swagger', + '.openapi' // API文档 + ] + ], + [ + 'version', + [ + '.gitignore', // Git ignore + '.gitattributes', // Git attributes + '.gitconfig', // Git config + '.hgignore', // Mercurial ignore + '.bzrignore', // Bazaar ignore + '.svnignore', // SVN ignore + '.githistory' // Git history + ] + ], + [ + 'subtitle', + [ + '.srt', + '.sub', + '.ass' // 字幕格式 + ] + ], + [ + 'log', + [ + '.log', + '.rpt' // 日志和报告 (移除了.out,因为通常是二进制可执行文件) + ] + ], + [ + 'eda', + [ + '.v', + '.sv', + '.svh', // Verilog/SystemVerilog + '.vhd', + '.vhdl', // VHDL + '.lef', + '.def', // LEF/DEF + '.edif', // EDIF + '.sdf', // SDF + '.sdc', + '.xdc', // 约束文件 + '.sp', + '.spi', + '.cir', + '.net', // SPICE + '.scs', // Spectre + '.asc', // LTspice + '.tf', // Technology File + '.il', + '.ils' // SKILL + ] + ], + [ + 'game', + [ + '.mtl', // Material Template Library + '.x3d', // X3D文件 + '.gltf', // glTF JSON + '.prefab', // Unity预制体 (YAML格式) + '.meta' // Unity元数据文件 (YAML格式) + ] + ], + [ + 'other', + [ + '.mcfunction', // Minecraft函数 + '.jsp', // JSP + '.aspx', // ASP.NET + '.ipynb', // Jupyter Notebook + '.cake', + '.ctp', // CakePHP + '.cfm', + '.cfc' // ColdFusion + ] + ] +]) + +export const textExts = Array.from(textExtsByCategory.values()).flat() export const ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5] From e2a08e31e84420220c80591e0536b81800ae0632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=8F=AF=E7=8B=B8?= Date: Thu, 5 Jun 2025 12:46:20 +0800 Subject: [PATCH 06/10] feat(Settings): Add token count display toggle (#6772) * feat(Settings): add token count toggle * fix(i18n): update token usage messages for zh-cn and zh-tw locales * fix(InstallNpxUv): optimize checkBinaries function with useCallback for better performance --------- Co-authored-by: Pleasurecruise <3196812536@qq.com> --- src/renderer/src/hooks/useSettings.ts | 4 ++++ src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/ja-jp.json | 1 + src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/pages/home/Messages/MessageTokens.tsx | 6 ++++-- src/renderer/src/pages/home/Tabs/SettingsTab.tsx | 9 ++++++++- .../src/pages/settings/MCPSettings/InstallNpxUv.tsx | 8 ++++---- src/renderer/src/store/settings.ts | 6 ++++++ 10 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts index 66d9615ee..c058a9523 100644 --- a/src/renderer/src/hooks/useSettings.ts +++ b/src/renderer/src/hooks/useSettings.ts @@ -8,6 +8,7 @@ import { setLaunchToTray, setPinTopicsToTop, setSendMessageShortcut as _setSendMessageShortcut, + setShowTokens, setSidebarIcons, setTargetLanguage, setTheme, @@ -83,6 +84,9 @@ export function useSettings() { }, setAssistantIconType(assistantIconType: AssistantIconType) { dispatch(setAssistantIconType(assistantIconType)) + }, + setShowTokens(showTokens: boolean) { + dispatch(setShowTokens(showTokens)) } } } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 3fd4886f9..e6afa7fb4 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1498,6 +1498,7 @@ "advancedSettings": "Advanced Settings" }, "messages.prompt": "Show prompt", + "messages.tokens": "Show token usage", "messages.divider": "Show divider between messages", "messages.grid_columns": "Message grid display columns", "messages.grid_popover_trigger": "Grid detail trigger", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 92488e5bc..5f5a1d11f 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1494,6 +1494,7 @@ "advancedSettings": "詳細設定" }, "messages.prompt": "プロンプト表示", + "messages.tokens": "トークン使用量を表示", "messages.divider": "メッセージ間に区切り線を表示", "messages.grid_columns": "メッセージグリッドの表示列数", "messages.grid_popover_trigger": "グリッド詳細トリガー", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 942fc2f2d..a26b78ba3 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1494,6 +1494,7 @@ "advancedSettings": "Расширенные настройки" }, "messages.prompt": "Показывать подсказки", + "messages.tokens": "Показать использование токенов", "messages.divider": "Показывать разделитель между сообщениями", "messages.grid_columns": "Количество столбцов сетки сообщений", "messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 437d8db25..a34454931 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1498,6 +1498,7 @@ "advancedSettings": "高级设置" }, "messages.prompt": "显示提示词", + "messages.tokens": "显示Token用量", "messages.divider": "消息分割线", "messages.grid_columns": "消息网格展示列数", "messages.grid_popover_trigger": "网格详情触发", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3546889be..ce1a1a540 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1497,6 +1497,7 @@ "advancedSettings": "高級設定" }, "messages.prompt": "提示詞顯示", + "messages.tokens": "Token用量顯示", "messages.divider": "訊息間顯示分隔線", "messages.grid_columns": "訊息網格展示列數", "messages.grid_popover_trigger": "網格詳細資訊觸發", diff --git a/src/renderer/src/pages/home/Messages/MessageTokens.tsx b/src/renderer/src/pages/home/Messages/MessageTokens.tsx index 2695767f4..26c5cdc0c 100644 --- a/src/renderer/src/pages/home/Messages/MessageTokens.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTokens.tsx @@ -1,4 +1,5 @@ // import { useRuntime } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import type { Message } from '@renderer/types/newMessage' import { Popover } from 'antd' @@ -11,6 +12,7 @@ interface MessageTokensProps { } const MessgeTokens: React.FC = ({ message }) => { + const { showTokens } = useSettings() // const { generating } = useRuntime() const locateMessage = () => { EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false) @@ -23,7 +25,7 @@ const MessgeTokens: React.FC = ({ message }) => { if (message.role === 'user') { return ( - Tokens: {message?.usage?.total_tokens} + {showTokens && `Tokens: ${message?.usage?.total_tokens}`} ) } @@ -54,7 +56,7 @@ const MessgeTokens: React.FC = ({ message }) => { {hasMetrics ? ( - {tokensInfo} + {showTokens && tokensInfo} ) : ( tokensInfo diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 06792f789..e3f2f72cd 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -46,6 +46,7 @@ import { setShowInputEstimatedTokens, setShowMessageDivider, setShowPrompt, + setShowTokens, setShowTranslateConfirm, setThoughtAutoCollapse } from '@renderer/store/settings' @@ -113,7 +114,8 @@ const SettingsTab: FC = (props) => { messageNavigation, enableQuickPanelTriggers, enableBackspaceDeleteModel, - showTranslateConfirm + showTranslateConfirm, + showTokens } = useSettings() const onUpdateAssistantSettings = (settings: Partial) => { @@ -336,6 +338,11 @@ const SettingsTab: FC = (props) => { dispatch(setShowPrompt(checked))} /> + + {t('settings.messages.tokens')} + dispatch(setShowTokens(checked))} /> + + {t('settings.messages.divider')} = ({ mini = false }) => { const [binariesDir, setBinariesDir] = useState(null) const { t } = useTranslation() const navigate = useNavigate() - const checkBinaries = async () => { + const checkBinaries = useCallback(async () => { const uvExists = await window.api.isBinaryExist('uv') const bunExists = await window.api.isBinaryExist('bun') const { uvPath, bunPath, dir } = await window.api.mcp.getInstallInfo() @@ -36,7 +36,7 @@ const InstallNpxUv: FC = ({ mini = false }) => { setUvPath(uvPath) setBunPath(bunPath) setBinariesDir(dir) - } + }, [dispatch]) const installUV = async () => { try { @@ -69,7 +69,7 @@ const InstallNpxUv: FC = ({ mini = false }) => { useEffect(() => { checkBinaries() - }, []) + }, [checkBinaries]) if (mini) { const installed = isUvInstalled && isBunInstalled diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index d8e778866..70e91e56a 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -47,6 +47,7 @@ export interface SettingsState { proxyUrl?: string userName: string showPrompt: boolean + showTokens: boolean showMessageDivider: boolean messageFont: 'system' | 'serif' showInputEstimatedTokens: boolean @@ -191,6 +192,7 @@ export const initialState: SettingsState = { proxyUrl: undefined, userName: '', showPrompt: true, + showTokens: true, showMessageDivider: true, messageFont: 'system', showInputEstimatedTokens: false, @@ -355,6 +357,9 @@ const settingsSlice = createSlice({ setShowPrompt: (state, action: PayloadAction) => { state.showPrompt = action.payload }, + setShowTokens: (state, action: PayloadAction) => { + state.showTokens = action.payload + }, setShowMessageDivider: (state, action: PayloadAction) => { state.showMessageDivider = action.payload }, @@ -672,6 +677,7 @@ export const { setProxyUrl, setUserName, setShowPrompt, + setShowTokens, setShowMessageDivider, setMessageFont, setShowInputEstimatedTokens, From acbec213e8e4a8a85122d64eee5d9a92726e6c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=86=8A=E5=8F=AF=E7=8B=B8?= Date: Thu, 5 Jun 2025 14:02:09 +0800 Subject: [PATCH 07/10] hotfix: ensure show token usage setting defaults to true (#6828) Hotfix: ensure show token usage setting defaults to true --- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 2d910c6f2..5ce6c4601 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -50,7 +50,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 109, + version: 110, blacklist: ['runtime', 'messages', 'messageBlocks'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index a17587c3d..b4709c064 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1473,6 +1473,14 @@ const migrateConfig = { } catch (error) { return state } + }, + '110': (state: RootState) => { + try { + state.settings.showTokens = true + return state + } catch (error) { + return state + } } } From 9195a0324e9711f9fa8b865cdf74e3401e0dc706 Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:28:50 +0800 Subject: [PATCH 08/10] fix(SelectionAssistant): ignore ctrl pressing when user is zooming in/out (#6822) * fix(SelectionService): ignore ctrl pressing when user is zomming in/out * chore: rename function * fix: reset listener status --- src/main/services/SelectionService.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index e4f33c2cf..f785683b5 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -304,7 +304,12 @@ export class SelectionService { if (!this.selectionHook) return false this.selectionHook.stop() - this.selectionHook.cleanup() + this.selectionHook.cleanup() //already remove all listeners + + //reset the listener states + this.isCtrlkeyListenerActive = false + this.isHideByMouseKeyListenerActive = false + if (this.toolbarWindow) { this.toolbarWindow.close() this.toolbarWindow = null @@ -836,6 +841,8 @@ export class SelectionService { //ctrlkey pressed if (this.lastCtrlkeyDownTime === 0) { this.lastCtrlkeyDownTime = Date.now() + //add the mouse-wheel listener, detect if user is zooming in/out + this.selectionHook!.on('mouse-wheel', this.handleMouseWheelCtrlkeyMode) return } @@ -859,9 +866,20 @@ export class SelectionService { */ private handleKeyUpCtrlkeyMode = (data: KeyboardEventData) => { if (!this.isCtrlkey(data.vkCode)) return + //remove the mouse-wheel listener + this.selectionHook!.off('mouse-wheel', this.handleMouseWheelCtrlkeyMode) this.lastCtrlkeyDownTime = 0 } + /** + * Handle mouse wheel events in ctrlkey trigger mode + * ignore CtrlKey pressing when mouse wheel is used + * because user is zooming in/out + */ + private handleMouseWheelCtrlkeyMode = () => { + this.lastCtrlkeyDownTime = -1 + } + //check if the key is ctrl key private isCtrlkey(vkCode: number) { return vkCode === 162 || vkCode === 163 From 1215bcb04610fc2b3a2453f45aea696847db2b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:41:53 +0800 Subject: [PATCH 09/10] refactor: enhance export functions (#5854) * feat(markdown-export): add option to show model name in export * refactor(export): Refactor the Obsidian export modal to Ant Design style * refactor(obsidian-export): export to obsidian using markdown interface & support COT * feat(markdown-export): optimize COT export style, support export model & provider name Add a new setting to toggle displaying the model provider alongside the model name in markdown exports. Update the export logic to include the provider name when enabled, improving context and clarity of exported messages. Also fix invalid filename character removal regex for Mac. * feat(export): add option to export reasoning in Joplin notes Introduce a new setting to toggle exporting reasoning details when exporting topics or messages to Joplin. Update the export function to handle raw messages and convert them to markdown with or without reasoning based on the setting. This improves the export feature by allowing users to include more detailed context in their Joplin notes. * feat(export): update i18n for new export options * fix(settings): remove duplicate showModelNameInMarkdown state * feat(export): add CoT export for notion & optmize notion export * feat(export): update Notion settings i18n * fix(utils): correct citation markdown formatting Swap citation title and URL positions in markdown links to ensure the link text displays the title (or URL if title is missing) and the link points to the correct URL. This improves citation clarity. --- .../src/components/ObsidianExportDialog.tsx | 126 ++++----- .../components/Popups/ObsidianExportPopup.tsx | 70 +++-- src/renderer/src/i18n/locales/en-us.json | 18 +- src/renderer/src/i18n/locales/ja-jp.json | 48 ++-- src/renderer/src/i18n/locales/ru-ru.json | 16 +- src/renderer/src/i18n/locales/zh-cn.json | 20 +- src/renderer/src/i18n/locales/zh-tw.json | 16 +- .../pages/home/Messages/MessageMenubar.tsx | 10 +- .../src/pages/home/Tabs/TopicsTab.tsx | 7 +- .../settings/DataSettings/JoplinSettings.tsx | 19 +- .../DataSettings/MarkdownExportSettings.tsx | 28 ++ .../settings/DataSettings/NotionSettings.tsx | 56 +--- src/renderer/src/store/settings.ts | 34 ++- src/renderer/src/utils/export.ts | 249 ++++++++++++------ src/renderer/src/utils/messageUtils/find.ts | 2 +- 15 files changed, 420 insertions(+), 299 deletions(-) diff --git a/src/renderer/src/components/ObsidianExportDialog.tsx b/src/renderer/src/components/ObsidianExportDialog.tsx index 267473f48..631c085ca 100644 --- a/src/renderer/src/components/ObsidianExportDialog.tsx +++ b/src/renderer/src/components/ObsidianExportDialog.tsx @@ -1,26 +1,36 @@ import i18n from '@renderer/i18n' import store from '@renderer/store' -import { exportMarkdownToObsidian } from '@renderer/utils/export' -import { Alert, Empty, Form, Input, Modal, Select, Spin, TreeSelect } from 'antd' +import type { Topic } from '@renderer/types' +import type { Message } from '@renderer/types/newMessage' +import { + exportMarkdownToObsidian, + messagesToMarkdown, + messageToMarkdown, + messageToMarkdownWithReasoning, + topicToMarkdown +} from '@renderer/utils/export' +import { Alert, Empty, Form, Input, Modal, Select, Spin, Switch, TreeSelect } from 'antd' import React, { useEffect, useState } from 'react' const { Option } = Select -interface ObsidianExportDialogProps { - title: string - markdown: string - open: boolean - onClose: (success: boolean) => void - obsidianTags: string | null - processingMethod: string | '3' //默认新增(存在就覆盖) -} - interface FileInfo { path: string type: 'folder' | 'markdown' name: string } +interface PopupContainerProps { + title: string + obsidianTags: string | null + processingMethod: string | '3' + open: boolean + resolve: (success: boolean) => void + message?: Message + messages?: Message[] + topic?: Topic +} + // 转换文件信息数组为树形结构 const convertToTreeData = (files: FileInfo[]) => { const treeData: any[] = [ @@ -113,13 +123,15 @@ const convertToTreeData = (files: FileInfo[]) => { return treeData } -const ObsidianExportDialog: React.FC = ({ +const PopupContainer: React.FC = ({ title, - markdown, - open, - onClose, obsidianTags, - processingMethod + processingMethod, + open, + resolve, + message, + messages, + topic }) => { const defaultObsidianVault = store.getState().settings.defaultObsidianVault const [state, setState] = useState({ @@ -130,8 +142,6 @@ const ObsidianExportDialog: React.FC = ({ processingMethod: processingMethod, folder: '' }) - - // 是否手动编辑过标题 const [hasTitleBeenManuallyEdited, setHasTitleBeenManuallyEdited] = useState(false) const [vaults, setVaults] = useState>([]) const [files, setFiles] = useState([]) @@ -139,8 +149,8 @@ const ObsidianExportDialog: React.FC = ({ const [selectedVault, setSelectedVault] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [exportReasoning, setExportReasoning] = useState(false) - // 处理文件数据转为树形结构 useEffect(() => { if (files.length > 0) { const treeData = convertToTreeData(files) @@ -157,28 +167,21 @@ const ObsidianExportDialog: React.FC = ({ } }, [files]) - // 组件加载时获取Vault列表 useEffect(() => { const fetchVaults = async () => { try { setLoading(true) setError(null) const vaultsData = await window.obsidian.getVaults() - if (vaultsData.length === 0) { setError(i18n.t('chat.topics.export.obsidian_no_vaults')) setLoading(false) return } - setVaults(vaultsData) - - // 如果没有选择的vault,使用默认值或第一个 const vaultToUse = defaultObsidianVault || vaultsData[0]?.name if (vaultToUse) { setSelectedVault(vaultToUse) - - // 获取选中vault的文件和文件夹 const filesData = await window.obsidian.getFiles(vaultToUse) setFiles(filesData) } @@ -189,11 +192,9 @@ const ObsidianExportDialog: React.FC = ({ setLoading(false) } } - fetchVaults() }, [defaultObsidianVault]) - // 当选择的vault变化时,获取其文件和文件夹 useEffect(() => { if (selectedVault) { const fetchFiles = async () => { @@ -209,7 +210,6 @@ const ObsidianExportDialog: React.FC = ({ setLoading(false) } } - fetchFiles() } }, [selectedVault]) @@ -219,82 +219,71 @@ const ObsidianExportDialog: React.FC = ({ setError(i18n.t('chat.topics.export.obsidian_no_vault_selected')) return } - - //构建content 并复制到粘贴板 + let markdown = '' + if (topic) { + markdown = await topicToMarkdown(topic, exportReasoning) + } else if (messages && messages.length > 0) { + markdown = messagesToMarkdown(messages, exportReasoning) + } else if (message) { + markdown = exportReasoning ? messageToMarkdownWithReasoning(message) : messageToMarkdown(message) + } else { + markdown = '' + } let content = '' if (state.processingMethod !== '3') { content = `\n---\n${markdown}` } else { - content = `--- - \ntitle: ${state.title} - \ncreated: ${state.createdAt} - \nsource: ${state.source} - \ntags: ${state.tags} - \n---\n${markdown}` + content = `---\n\ntitle: ${state.title}\ncreated: ${state.createdAt}\nsource: ${state.source}\ntags: ${state.tags}\n---\n${markdown}` } if (content === '') { window.message.error(i18n.t('chat.topics.export.obsidian_export_failed')) return } - await navigator.clipboard.writeText(content) - - // 导出到Obsidian exportMarkdownToObsidian({ ...state, folder: state.folder, vault: selectedVault }) - - onClose(true) + setOpen(false) + resolve(true) } + const [openState, setOpen] = useState(open) + useEffect(() => { + setOpen(open) + }, [open]) + const handleCancel = () => { - onClose(false) + setOpen(false) + resolve(false) } const handleChange = (key: string, value: any) => { setState((prevState) => ({ ...prevState, [key]: value })) } - - // 处理title输入变化 const handleTitleInputChange = (newTitle: string) => { handleChange('title', newTitle) setHasTitleBeenManuallyEdited(true) } - const handleVaultChange = (value: string) => { setSelectedVault(value) - // 文件夹会通过useEffect自动获取 - setState((prevState) => ({ - ...prevState, - folder: '' - })) + setState((prevState) => ({ ...prevState, folder: '' })) } - - // 处理文件选择 const handleFileSelect = (value: string) => { - // 更新folder值 handleChange('folder', value) - - // 检查是否选中md文件 if (value) { const selectedFile = files.find((file) => file.path === value) if (selectedFile) { if (selectedFile.type === 'markdown') { - // 如果是md文件,自动设置标题为文件名并设置处理方式为1(追加) const fileName = selectedFile.name const titleWithoutExt = fileName.endsWith('.md') ? fileName.substring(0, fileName.length - 3) : fileName handleChange('title', titleWithoutExt) - // 重置手动编辑标记,因为这是非用户设置的title setHasTitleBeenManuallyEdited(false) handleChange('processingMethod', '1') } else { - // 如果是文件夹,自动设置标题为话题名并设置处理方式为3(新建) handleChange('processingMethod', '3') - // 仅当用户未手动编辑过 title 时,才将其重置为 props.title if (!hasTitleBeenManuallyEdited) { - // title 是 props.title handleChange('title', title) } } @@ -305,7 +294,7 @@ const ObsidianExportDialog: React.FC = ({ return ( = ({ type: 'primary', disabled: vaults.length === 0 || loading || !!error }} - okText={i18n.t('chat.topics.export.obsidian_btn')}> + okText={i18n.t('chat.topics.export.obsidian_btn')} + afterClose={() => setOpen(open)}> {error && } -
= ({ placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')} /> - {vaults.length > 0 ? ( = ({ placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')} /> - + + +
) } -export default ObsidianExportDialog +export { PopupContainer } diff --git a/src/renderer/src/components/Popups/ObsidianExportPopup.tsx b/src/renderer/src/components/Popups/ObsidianExportPopup.tsx index 23a7da2dc..49dc320c7 100644 --- a/src/renderer/src/components/Popups/ObsidianExportPopup.tsx +++ b/src/renderer/src/components/Popups/ObsidianExportPopup.tsx @@ -1,44 +1,38 @@ -import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog' -import { createRoot } from 'react-dom/client' +import { PopupContainer } from '@renderer/components/ObsidianExportDialog' +import { TopView } from '@renderer/components/TopView' +import type { Topic } from '@renderer/types' +import type { Message } from '@renderer/types/newMessage' interface ObsidianExportOptions { title: string - markdown: string - processingMethod: string | '3' // 默认新增(存在就覆盖) + processingMethod: string | '3' + topic?: Topic + message?: Message + messages?: Message[] } -/** - * 配置Obsidian 笔记属性弹窗 - * @param options.title 标题 - * @param options.markdown markdown内容 - * @param options.processingMethod 处理方式 - * @returns - */ -const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise => { - return new Promise((resolve) => { - const div = document.createElement('div') - document.body.appendChild(div) - const root = createRoot(div) - - const handleClose = (success: boolean) => { - root.unmount() - document.body.removeChild(div) - resolve(success) - } - // 不再从store中获取tag配置 - root.render( - - ) - }) -} - -export default { - show: showObsidianExportDialog +export default class ObsidianExportPopup { + static hide() { + TopView.hide('ObsidianExportPopup') + } + static show(options: ObsidianExportOptions): Promise { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + ObsidianExportPopup.hide() + }} + />, + 'ObsidianExportPopup' + ) + }) + } } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index e6afa7fb4..593ed6f95 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -322,6 +322,7 @@ "translate": "Translate", "topics.export.siyuan": "Export to Siyuan Note", "topics.export.wait_for_title_naming": "Generating title...", + "topics.export.obsidian_reasoning": "Include Reasoning Chain", "topics.export.title_naming_success": "Title generated successfully", "topics.export.title_naming_failed": "Failed to generate title, using default title", "input.translating": "Translating...", @@ -1104,7 +1105,9 @@ "token": "Joplin Authorization Token", "token_placeholder": "Joplin Authorization Token", "url": "Joplin Web Clipper Service URL", - "url_placeholder": "http://127.0.0.1:41184/" + "url_placeholder": "http://127.0.0.1:41184/", + "export_reasoning.title": "Include Reasoning Chain in Export", + "export_reasoning.help": "When enabled, the exported content will include the reasoning chain (thought process) generated by the assistant." }, "markdown_export.force_dollar_math.help": "When enabled, $$ will be forcibly used to mark LaTeX formulas when exporting to Markdown. Note: This option also affects all export methods through Markdown, such as Notion, Yuque, etc.", "markdown_export.force_dollar_math.title": "Force $$ for LaTeX formulas", @@ -1113,12 +1116,14 @@ "markdown_export.path_placeholder": "Export Path", "markdown_export.select": "Select", "markdown_export.title": "Markdown Export", + "markdown_export.show_model_name.title": "Use Model Name on Export", + "markdown_export.show_model_name.help": "When enabled, the topic-naming model will be used to create titles for exported messages. Note: This option also affects all export methods through Markdown, such as Notion, Yuque, etc.", + "markdown_export.show_model_provider.title": "Show Model Provider", + "markdown_export.show_model_provider.help": "Display the model provider (e.g., OpenAI, Gemini) when exporting to Markdown", "minute_interval_one": "{{count}} minute", "minute_interval_other": "{{count}} minutes", "notion.api_key": "Notion API Key", "notion.api_key_placeholder": "Enter Notion API Key", - "notion.auto_split": "Auto split when exporting", - "notion.auto_split_tip": "Automatically split pages when exporting long topics to Notion", "notion.check": { "button": "Check", "empty_api_key": "API key is not configured", @@ -1132,10 +1137,9 @@ "notion.help": "Notion Configuration Documentation", "notion.page_name_key": "Page Title Field Name", "notion.page_name_key_placeholder": "Enter page title field name, default is Name", - "notion.split_size": "Split size", - "notion.split_size_help": "Recommended: 90 for Free plan, 24990 for Plus plan, default is 90", - "notion.split_size_placeholder": "Enter block limit per page (default 90)", - "notion.title": "Notion Configuration", + "notion.title": "Notion Settings", + "notion.export_reasoning.title": "Include Reasoning Chain in Export", + "notion.export_reasoning.help": "When enabled, exported content will include reasoning chain (thought process).", "title": "Data Settings", "webdav": { "autoSync": "Auto Backup", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 5f5a1d11f..c1341a90d 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -322,6 +322,7 @@ "translate": "翻訳", "topics.export.siyuan": "思源笔记にエクスポート", "topics.export.wait_for_title_naming": "タイトルを生成中...", + "topics.export.obsidian_reasoning": "思考過程を含める", "topics.export.title_naming_success": "タイトルの生成に成功しました", "topics.export.title_naming_failed": "タイトルの生成に失敗しました。デフォルトのタイトルを使用します", "input.translating": "翻訳中...", @@ -1102,7 +1103,9 @@ "token": "Joplin 認証トークン", "token_placeholder": "Joplin 認証トークンを入力してください", "url": "Joplin 剪輯服務 URL", - "url_placeholder": "http://127.0.0.1:41184/" + "url_placeholder": "http://127.0.0.1:41184/", + "export_reasoning.title": "エクスポート時に思考過程を含める", + "export_reasoning.help": "有効にすると、エクスポートされる内容にアシスタントが生成した思考過程(リースニングチェーン)が含まれます。" }, "markdown_export.force_dollar_math.help": "有効にすると、Markdownにエクスポートする際にLaTeX数式を$$で強制的にマークします。注意:この設定はNotion、Yuqueなど、Markdownを通じたすべてのエクスポート方法にも影響します。", "markdown_export.force_dollar_math.title": "LaTeX数式に$$を強制使用", @@ -1111,29 +1114,32 @@ "markdown_export.path_placeholder": "エクスポートパス", "markdown_export.select": "選択", "markdown_export.title": "Markdown エクスポート", + "markdown_export.show_model_name.title": "エクスポート時にモデル名を使用", + "markdown_export.show_model_name.help": "有効にすると、トピック命名モデルがエクスポートされたメッセージのタイトル作成に使用されます。注意:この設定はNotion、Yuqueなど、Markdownを通じたすべてのエクスポート方法にも影響します。", + "markdown_export.show_model_provider.title": "モデルプロバイダーを表示", + "markdown_export.show_model_provider.help": "Markdownエクスポート時にモデルプロバイダー(例:OpenAI、Geminiなど)を表示します。", "minute_interval_one": "{{count}} 分", "minute_interval_other": "{{count}} 分", - "notion.api_key": "Notion APIキー", - "notion.api_key_placeholder": "Notion APIキーを入力してください", - "notion.auto_split": "ダイアログをエクスポートすると自動ページ分割", - "notion.auto_split_tip": "ダイアログが長い場合、Notionに自動的にページ分割してエクスポートします", - "notion.check": { - "button": "確認", - "empty_api_key": "Api_keyが設定されていません", - "empty_database_id": "Database_idが設定されていません", - "error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください", - "fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください", - "success": "接続に成功しました。" + "notion": { + "api_key": "Notion APIキー", + "api_key_placeholder": "Notion APIキーを入力してください", + "check": { + "button": "確認", + "empty_api_key": "Api_keyが設定されていません", + "empty_database_id": "Database_idが設定されていません", + "error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください", + "fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください", + "success": "接続に成功しました。" + }, + "database_id": "Notion データベースID", + "database_id_placeholder": "Notion データベースIDを入力してください", + "help": "Notion 設定ドキュメント", + "page_name_key": "ページタイトルフィールド名", + "page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です", + "title": "Notion 設定", + "export_reasoning.title": "エクスポート時に思考チェーンを含める", + "export_reasoning.help": "有効にすると、Notionにエクスポートする際に思考チェーンの内容が含まれます。" }, - "notion.database_id": "Notion データベースID", - "notion.database_id_placeholder": "Notion データベースIDを入力してください", - "notion.help": "Notion 設定ドキュメント", - "notion.page_name_key": "ページタイトルフィールド名", - "notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です", - "notion.split_size": "自動ページ分割サイズ", - "notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90", - "notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)", - "notion.title": "Notion 設定", "title": "データ設定", "webdav": { "autoSync": "自動バックアップ", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index a26b78ba3..019726f8d 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -322,6 +322,7 @@ "translate": "Перевести", "topics.export.siyuan": "Экспорт в Siyuan Note", "topics.export.wait_for_title_naming": "Создание заголовка...", + "topics.export.obsidian_reasoning": "Включить цепочку рассуждений", "topics.export.title_naming_success": "Заголовок успешно создан", "topics.export.title_naming_failed": "Не удалось создать заголовок, используется заголовок по умолчанию", "input.translating": "Перевод...", @@ -1102,7 +1103,9 @@ "token": "Токен Joplin", "token_placeholder": "Введите токен Joplin", "url": "URL Joplin", - "url_placeholder": "http://127.0.0.1:41184/" + "url_placeholder": "http://127.0.0.1:41184/", + "export_reasoning.title": "Включить цепочку рассуждений при экспорте", + "export_reasoning.help": "Если включено, экспортируемый контент будет содержать цепочку рассуждений, сгенерированную ассистентом." }, "markdown_export.force_dollar_math.help": "Если включено, при экспорте в Markdown для обозначения формул LaTeX будет принудительно использоваться $$. Примечание: Эта опция также влияет на все методы экспорта через Markdown, такие как Notion, Yuque и т.д.", "markdown_export.force_dollar_math.title": "Принудительно использовать $$ для формул LaTeX", @@ -1111,12 +1114,14 @@ "markdown_export.path_placeholder": "Путь экспорта", "markdown_export.select": "Выбрать", "markdown_export.title": "Экспорт в Markdown", + "markdown_export.show_model_name.title": "Использовать имя модели при экспорте", + "markdown_export.show_model_name.help": "Если включено, для создания заголовков экспортируемых сообщений будет использоваться модель именования темы. Примечание: Эта опция также влияет на все методы экспорта через Markdown, такие как Notion, Yuque и т.д.", + "markdown_export.show_model_provider.title": "Показать поставщика модели", + "markdown_export.show_model_provider.help": "Показывать поставщика модели (например, OpenAI, Gemini) при экспорте в Markdown", "minute_interval_one": "{{count}} минута", "minute_interval_other": "{{count}} минут", "notion.api_key": "Ключ API Notion", "notion.api_key_placeholder": "Введите ключ API Notion", - "notion.auto_split": "Автоматическое разбиение на страницы при экспорте диалога", - "notion.auto_split_tip": "Автоматическое разбиение на страницы при экспорте в Notion, если тема слишком длинная", "notion.check": { "button": "Проверить", "empty_api_key": "Не настроен API key", @@ -1130,10 +1135,9 @@ "notion.help": "Документация по настройке Notion", "notion.page_name_key": "Название поля заголовка страницы", "notion.page_name_key_placeholder": "Введите название поля заголовка страницы, по умолчанию Name", - "notion.split_size": "Размер автоматического разбиения", - "notion.split_size_help": "Рекомендуется 90 для пользователей бесплатной версии Notion, 24990 для пользователей премиум-версии, значение по умолчанию — 90", - "notion.split_size_placeholder": "Введите ограничение количества блоков на странице (по умолчанию 90)", "notion.title": "Настройки Notion", + "notion.export_reasoning.title": "Включить цепочку рассуждений при экспорте", + "notion.export_reasoning.help": "При включении, содержимое цепочки рассуждений будет включено при экспорте в Notion.", "title": "Настройки данных", "webdav": { "autoSync": "Автоматическое резервное копирование", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index a34454931..0d04e66e7 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -325,6 +325,7 @@ "topics.export.obsidian_no_vault_selected": "请先选择一个保管库", "topics.export.obsidian_select_vault_first": "请先选择保管库", "topics.export.obsidian_root_directory": "根目录", + "topics.export.obsidian_reasoning": "导出思维链", "topics.export.title": "导出", "topics.export.word": "导出为 Word", "topics.export.yuque": "导出到语雀", @@ -654,7 +655,7 @@ "group.delete.content": "删除分组消息会删除用户提问和所有助手的回答", "group.delete.title": "删除分组消息", "ignore.knowledge.base": "联网模式开启,忽略知识库", - "info.notion.block_reach_limit": "对话过长,正在分页导出到Notion", + "info.notion.block_reach_limit": "对话过长,正在分段导出到Notion", "loading.notion.exporting_progress": "正在导出到Notion ({{current}}/{{total}})...", "loading.notion.preparing": "正在准备导出到Notion...", "mention.title": "切换模型回答", @@ -1104,7 +1105,9 @@ "token": "Joplin 授权令牌", "token_placeholder": "请输入 Joplin 授权令牌", "url": "Joplin 剪裁服务监听 URL", - "url_placeholder": "http://127.0.0.1:41184/" + "url_placeholder": "http://127.0.0.1:41184/", + "export_reasoning.title": "导出时包含思维链", + "export_reasoning.help": "开启后,导出到Joplin时会包含思维链内容。" }, "markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等", "markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式", @@ -1113,14 +1116,16 @@ "markdown_export.path_placeholder": "导出路径", "markdown_export.select": "选择", "markdown_export.title": "Markdown 导出", + "markdown_export.show_model_name.title": "导出时使用模型名称", + "markdown_export.show_model_name.help": "开启后,使用话题命名模型为导出的消息创建标题。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等。", + "markdown_export.show_model_provider.title": "显示模型供应商", + "markdown_export.show_model_provider.help": "在导出Markdown时显示模型供应商,如OpenAI、Gemini等", "message_title.use_topic_naming.title": "使用话题命名模型为导出的消息创建标题", "message_title.use_topic_naming.help": "开启后,使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式", "minute_interval_one": "{{count}} 分钟", "minute_interval_other": "{{count}} 分钟", "notion.api_key": "Notion 密钥", "notion.api_key_placeholder": "请输入Notion 密钥", - "notion.auto_split": "导出对话时自动分页", - "notion.auto_split_tip": "当要导出的话题过长时自动分页导出到Notion", "notion.check": { "button": "检测", "empty_api_key": "未配置 API key", @@ -1134,10 +1139,9 @@ "notion.help": "Notion 配置文档", "notion.page_name_key": "页面标题字段名", "notion.page_name_key_placeholder": "请输入页面标题字段名,默认为 Name", - "notion.split_size": "自动分页大小", - "notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90", - "notion.split_size_placeholder": "请输入每页块数限制(默认90)", - "notion.title": "Notion 配置", + "notion.title": "Notion 设置", + "notion.export_reasoning.title": "导出时包含思维链", + "notion.export_reasoning.help": "开启后,导出到Notion时会包含思维链内容。", "title": "数据设置", "webdav": { "autoSync": "自动备份", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index ce1a1a540..73867953d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -322,6 +322,7 @@ "translate": "翻譯", "topics.export.siyuan": "匯出到思源筆記", "topics.export.wait_for_title_naming": "正在生成標題...", + "topics.export.obsidian_reasoning": "包含思維鏈", "topics.export.title_naming_success": "標題生成成功", "topics.export.title_naming_failed": "標題生成失敗,使用預設標題", "input.translating": "翻譯中...", @@ -1104,7 +1105,9 @@ "token": "Joplin 授權Token", "token_placeholder": "請輸入 Joplin 授權Token", "url": "Joplin 剪輯服務 URL", - "url_placeholder": "http://127.0.0.1:41184/" + "url_placeholder": "http://127.0.0.1:41184/", + "export_reasoning.title": "匯出時包含思維鏈", + "export_reasoning.help": "啟用後,匯出內容將包含助手生成的思維鏈(思考過程)。" }, "markdown_export.force_dollar_math.help": "開啟後,匯出Markdown時會強制使用$$來標記LaTeX公式。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等", "markdown_export.force_dollar_math.title": "LaTeX公式強制使用$$", @@ -1113,12 +1116,14 @@ "markdown_export.path_placeholder": "匯出路徑", "markdown_export.select": "選擇", "markdown_export.title": "Markdown 匯出", + "markdown_export.show_model_name.title": "匯出時使用模型名稱", + "markdown_export.show_model_name.help": "啟用後,將以主題命名模型為匯出的訊息建立標題。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等。", + "markdown_export.show_model_provider.title": "顯示模型供應商", + "markdown_export.show_model_provider.help": "在匯出Markdown時顯示模型供應商,如OpenAI、Gemini等", "minute_interval_one": "{{count}} 分鐘", "minute_interval_other": "{{count}} 分鐘", "notion.api_key": "Notion 金鑰", "notion.api_key_placeholder": "請輸入 Notion 金鑰", - "notion.auto_split": "匯出對話時自動分頁", - "notion.auto_split_tip": "當要匯出的話題過長時自動分頁匯出到 Notion", "notion.check": { "button": "檢查", "empty_api_key": "未設定 API key", @@ -1132,10 +1137,9 @@ "notion.help": "Notion 設定文件", "notion.page_name_key": "頁面標題欄位名稱", "notion.page_name_key_placeholder": "請輸入頁面標題欄位名稱,預設為 Name", - "notion.split_size": "自動分頁大小", - "notion.split_size_help": "Notion 免費版使用者建議設定為 90,進階版使用者建議設定為 24990,預設值為 90", - "notion.split_size_placeholder": "請輸入每頁塊數限制 (預設 90)", "notion.title": "Notion 設定", + "notion.export_reasoning.title": "匯出時包含思維鏈", + "notion.export_reasoning.help": "啟用後,匯出到Notion時會包含思維鏈內容。", "title": "資料設定", "webdav": { "autoSync": "自動備份", diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 7df542588..0073eee79 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -16,10 +16,10 @@ import type { Message } from '@renderer/types/newMessage' import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils' import { exportMarkdownToJoplin, - exportMarkdownToNotion, exportMarkdownToSiyuan, exportMarkdownToYuque, exportMessageAsMarkdown, + exportMessageToNotion, messageToMarkdown } from '@renderer/utils/export' // import { withMessageThought } from '@renderer/utils/formats' @@ -244,7 +244,7 @@ const MessageMenubar: FC = (props) => { onClick: async () => { const title = await getMessageTitle(message) const markdown = messageToMarkdown(message) - exportMarkdownToNotion(title, markdown) + exportMessageToNotion(title, markdown, message) } }, exportMenuOptions.yuque && { @@ -260,9 +260,8 @@ const MessageMenubar: FC = (props) => { label: t('chat.topics.export.obsidian'), key: 'obsidian', onClick: async () => { - const markdown = messageToMarkdown(message) const title = topic.name?.replace(/\//g, '_') || 'Untitled' - await ObsidianExportPopup.show({ title, markdown, processingMethod: '1' }) + await ObsidianExportPopup.show({ title, message, processingMethod: '1' }) } }, exportMenuOptions.joplin && { @@ -270,8 +269,7 @@ const MessageMenubar: FC = (props) => { key: 'joplin', onClick: async () => { const title = await getMessageTitle(message) - const markdown = messageToMarkdown(message) - exportMarkdownToJoplin(title, markdown) + exportMarkdownToJoplin(title, message) } }, exportMenuOptions.siyuan && { diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 490cfb3c2..34652e72b 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -312,16 +312,15 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic label: t('chat.topics.export.obsidian'), key: 'obsidian', onClick: async () => { - const markdown = await topicToMarkdown(topic) - await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' }) + await ObsidianExportPopup.show({ title: topic.name, topic, processingMethod: '3' }) } }, exportMenuOptions.joplin && { label: t('chat.topics.export.joplin'), key: 'joplin', onClick: async () => { - const markdown = await topicToMarkdown(topic) - exportMarkdownToJoplin(topic.name, markdown) + const topicMessages = await TopicManager.getTopicMessages(topic.id) + exportMarkdownToJoplin(topic.name, topicMessages) } }, exportMenuOptions.siyuan && { diff --git a/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx b/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx index 322fdd883..3574c808d 100644 --- a/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx @@ -3,14 +3,14 @@ import { HStack } from '@renderer/components/Layout' import { useTheme } from '@renderer/context/ThemeProvider' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { RootState, useAppDispatch } from '@renderer/store' -import { setJoplinToken, setJoplinUrl } from '@renderer/store/settings' -import { Button, Tooltip } from 'antd' +import { setJoplinExportReasoning, setJoplinToken, setJoplinUrl } from '@renderer/store/settings' +import { Button, Switch, Tooltip } from 'antd' import Input from 'antd/es/input/Input' import { FC } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' const JoplinSettings: FC = () => { const { t } = useTranslation() @@ -20,6 +20,7 @@ const JoplinSettings: FC = () => { const joplinToken = useSelector((state: RootState) => state.settings.joplinToken) const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl) + const joplinExportReasoning = useSelector((state: RootState) => state.settings.joplinExportReasoning) const handleJoplinTokenChange = (e: React.ChangeEvent) => { dispatch(setJoplinToken(e.target.value)) @@ -72,6 +73,10 @@ const JoplinSettings: FC = () => { }) } + const handleToggleJoplinExportReasoning = (checked: boolean) => { + dispatch(setJoplinExportReasoning(checked)) + } + return ( {t('settings.data.joplin.title')} @@ -111,6 +116,14 @@ const JoplinSettings: FC = () => {
+ + + {t('settings.data.joplin.export_reasoning.title')} + + + + {t('settings.data.joplin.export_reasoning.help')} + ) } diff --git a/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx b/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx index e4c816927..2ed7cb7f4 100644 --- a/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx @@ -5,6 +5,8 @@ import { RootState, useAppDispatch } from '@renderer/store' import { setForceDollarMathInMarkdown, setmarkdownExportPath, + setShowModelNameInMarkdown, + setShowModelProviderInMarkdown, setUseTopicNamingForMessageTitle } from '@renderer/store/settings' import { Button, Switch } from 'antd' @@ -23,6 +25,8 @@ const MarkdownExportSettings: FC = () => { const markdownExportPath = useSelector((state: RootState) => state.settings.markdownExportPath) const forceDollarMathInMarkdown = useSelector((state: RootState) => state.settings.forceDollarMathInMarkdown) const useTopicNamingForMessageTitle = useSelector((state: RootState) => state.settings.useTopicNamingForMessageTitle) + const showModelNameInExport = useSelector((state: RootState) => state.settings.showModelNameInMarkdown) + const showModelProviderInMarkdown = useSelector((state: RootState) => state.settings.showModelProviderInMarkdown) const handleSelectFolder = async () => { const path = await window.api.file.selectFolder() @@ -43,6 +47,14 @@ const MarkdownExportSettings: FC = () => { dispatch(setUseTopicNamingForMessageTitle(checked)) } + const handleToggleShowModelName = (checked: boolean) => { + dispatch(setShowModelNameInMarkdown(checked)) + } + + const handleToggleShowModelProvider = (checked: boolean) => { + dispatch(setShowModelProviderInMarkdown(checked)) + } + return ( {t('settings.data.markdown_export.title')} @@ -86,6 +98,22 @@ const MarkdownExportSettings: FC = () => { {t('settings.data.message_title.use_topic_naming.help')} + + + {t('settings.data.markdown_export.show_model_name.title')} + + + + {t('settings.data.markdown_export.show_model_name.help')} + + + + {t('settings.data.markdown_export.show_model_provider.title')} + + + + {t('settings.data.markdown_export.show_model_provider.help')} + ) } diff --git a/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx b/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx index de670ad43..719a2363d 100644 --- a/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx @@ -6,12 +6,11 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { RootState, useAppDispatch } from '@renderer/store' import { setNotionApiKey, - setNotionAutoSplit, setNotionDatabaseID, - setNotionPageNameKey, - setNotionSplitSize + setNotionExportReasoning, + setNotionPageNameKey } from '@renderer/store/settings' -import { Button, InputNumber, Switch, Tooltip } from 'antd' +import { Button, Switch, Tooltip } from 'antd' import Input from 'antd/es/input/Input' import { FC } from 'react' import { useTranslation } from 'react-i18next' @@ -27,8 +26,7 @@ const NotionSettings: FC = () => { const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey) const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID) const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey) - const notionAutoSplit = useSelector((state: RootState) => state.settings.notionAutoSplit) - const notionSplitSize = useSelector((state: RootState) => state.settings.notionSplitSize) + const notionExportReasoning = useSelector((state: RootState) => state.settings.notionExportReasoning) const handleNotionTokenChange = (e: React.ChangeEvent) => { dispatch(setNotionApiKey(e.target.value)) @@ -76,14 +74,8 @@ const NotionSettings: FC = () => { }) } - const handleNotionAutoSplitChange = (checked: boolean) => { - dispatch(setNotionAutoSplit(checked)) - } - - const handleNotionSplitSizeChange = (value: number | null) => { - if (value !== null) { - dispatch(setNotionSplitSize(value)) - } + const handleNotionExportReasoningChange = (checked: boolean) => { + dispatch(setNotionExportReasoning(checked)) } return ( @@ -140,38 +132,14 @@ const NotionSettings: FC = () => { - {/* 添加分割线 */} + - - - - {t('settings.data.notion.auto_split')} - - - - - + {t('settings.data.notion.export_reasoning.title')} + + + + {t('settings.data.notion.export_reasoning.help')} - {notionAutoSplit && ( - <> - - - {t('settings.data.notion.split_size')} - - - - {t('settings.data.notion.split_size_help')} - - - )} ) } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 70e91e56a..2f5e62754 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -129,14 +129,16 @@ export interface SettingsState { markdownExportPath: string | null forceDollarMathInMarkdown: boolean useTopicNamingForMessageTitle: boolean + showModelNameInMarkdown: boolean + showModelProviderInMarkdown: boolean thoughtAutoCollapse: boolean - notionAutoSplit: boolean - notionSplitSize: number + notionExportReasoning: boolean yuqueToken: string | null yuqueUrl: string | null yuqueRepoId: string | null joplinToken: string | null joplinUrl: string | null + joplinExportReasoning: boolean defaultObsidianVault: string | null defaultAgent: string | null // 思源笔记配置 @@ -271,14 +273,16 @@ export const initialState: SettingsState = { markdownExportPath: null, forceDollarMathInMarkdown: false, useTopicNamingForMessageTitle: false, + showModelNameInMarkdown: false, + showModelProviderInMarkdown: false, thoughtAutoCollapse: true, - notionAutoSplit: false, - notionSplitSize: 90, + notionExportReasoning: false, yuqueToken: '', yuqueUrl: '', yuqueRepoId: '', joplinToken: '', joplinUrl: '', + joplinExportReasoning: false, defaultObsidianVault: null, defaultAgent: null, siyuanApiUrl: null, @@ -580,14 +584,17 @@ const settingsSlice = createSlice({ setUseTopicNamingForMessageTitle: (state, action: PayloadAction) => { state.useTopicNamingForMessageTitle = action.payload }, + setShowModelNameInMarkdown: (state, action: PayloadAction) => { + state.showModelNameInMarkdown = action.payload + }, + setShowModelProviderInMarkdown: (state, action: PayloadAction) => { + state.showModelProviderInMarkdown = action.payload + }, setThoughtAutoCollapse: (state, action: PayloadAction) => { state.thoughtAutoCollapse = action.payload }, - setNotionAutoSplit: (state, action: PayloadAction) => { - state.notionAutoSplit = action.payload - }, - setNotionSplitSize: (state, action: PayloadAction) => { - state.notionSplitSize = action.payload + setNotionExportReasoning: (state, action: PayloadAction) => { + state.notionExportReasoning = action.payload }, setYuqueToken: (state, action: PayloadAction) => { state.yuqueToken = action.payload @@ -604,6 +611,9 @@ const settingsSlice = createSlice({ setJoplinUrl: (state, action: PayloadAction) => { state.joplinUrl = action.payload }, + setJoplinExportReasoning: (state, action: PayloadAction) => { + state.joplinExportReasoning = action.payload + }, setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => { state.messageNavigation = action.payload }, @@ -665,6 +675,8 @@ const settingsSlice = createSlice({ }) export const { + setShowModelNameInMarkdown, + setShowModelProviderInMarkdown, setShowAssistants, toggleShowAssistants, setShowTopics, @@ -737,13 +749,13 @@ export const { setForceDollarMathInMarkdown, setUseTopicNamingForMessageTitle, setThoughtAutoCollapse, - setNotionAutoSplit, - setNotionSplitSize, + setNotionExportReasoning, setYuqueToken, setYuqueRepoId, setYuqueUrl, setJoplinToken, setJoplinUrl, + setJoplinExportReasoning, setMessageNavigation, setDefaultObsidianVault, setDefaultAgent, diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index ef52156fb..0c858647b 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -11,7 +11,6 @@ import { convertMathFormula } from '@renderer/utils/markdown' import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find' import { markdownToBlocks } from '@tryfabric/martian' import dayjs from 'dayjs' -//TODO: 添加对思考内容的支持 /** * 从消息内容中提取标题,限制长度并处理换行和标点符号。用于导出功能。 @@ -43,9 +42,35 @@ export function getTitleFromString(str: string, length: number = 80) { return title } +const getRoleText = (role: string, modelName?: string, modelProvider?: string) => { + const { showModelNameInMarkdown, showModelProviderInMarkdown } = store.getState().settings + + if (role === 'user') { + return '🧑‍💻 User' + } else if (role === 'system') { + return '🤖 System' + } else { + let assistantText = '🤖 ' + if (showModelNameInMarkdown && modelName) { + assistantText += `${modelName}` + if (showModelProviderInMarkdown && modelProvider) { + const providerDisplayName = i18n.t(`provider.${modelProvider}`, { defaultValue: modelProvider }) + assistantText += ` | ${providerDisplayName}` + return assistantText + } + return assistantText + } else if (showModelProviderInMarkdown && modelProvider) { + const providerDisplayName = i18n.t(`provider.${modelProvider}`, { defaultValue: modelProvider }) + assistantText += `Assistant | ${providerDisplayName}` + return assistantText + } + return assistantText + 'Assistant' + } +} + const createBaseMarkdown = (message: Message, includeReasoning: boolean = false) => { const { forceDollarMathInMarkdown } = store.getState().settings - const roleText = message.role === 'user' ? '🧑‍💻 User' : '🤖 Assistant' + const roleText = getRoleText(message.role, message.model?.name, message.model?.provider) const titleSection = `### ${roleText}` let reasoningSection = '' @@ -57,15 +82,22 @@ const createBaseMarkdown = (message: Message, includeReasoning: boolean = false) } else if (reasoningContent.startsWith('')) { reasoningContent = reasoningContent.substring(7) } - reasoningContent = reasoningContent.replace(/\n/g, '
') - + reasoningContent = reasoningContent + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\n/g, '
') if (forceDollarMathInMarkdown) { reasoningContent = convertMathFormula(reasoningContent) } - reasoningSection = `
- ${i18n.t('common.reasoning_content')}
+ reasoningSection = `
+
+ ${i18n.t('common.reasoning_content')} ${reasoningContent} -
` +
+` } } @@ -81,7 +113,6 @@ export const messageToMarkdown = (message: Message) => { return [titleSection, '', contentSection, citation].join('\n\n') } -// 保留接口用于其它导出方法使用 export const messageToMarkdownWithReasoning = (message: Message) => { const { titleSection, reasoningSection, contentSection, citation } = createBaseMarkdown(message, true) return [titleSection, '', reasoningSection + contentSection, citation].join('\n\n') @@ -167,14 +198,10 @@ export const exportMessageAsMarkdown = async (message: Message, exportReasoning? const convertMarkdownToNotionBlocks = async (markdown: string) => { return markdownToBlocks(markdown) } -// 修改 splitNotionBlocks 函数 -const splitNotionBlocks = (blocks: any[]) => { - const { notionAutoSplit, notionSplitSize } = store.getState().settings - // 如果未开启自动分页,返回单页 - if (!notionAutoSplit) { - return [blocks] - } +const splitNotionBlocks = (blocks: any[]) => { + // Notion API限制单次传输100块 + const notionSplitSize = 95 const pages: any[][] = [] let currentPage: any[] = [] @@ -195,25 +222,68 @@ const splitNotionBlocks = (blocks: any[]) => { return pages } -export const exportTopicToNotion = async (topic: Topic) => { +const convertThinkingToNotionBlocks = async (thinkingContent: string): Promise => { + if (!thinkingContent.trim()) { + 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 + } + } + ] + } + } + ] + } + } + ] + + return thinkingBlocks +} + +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 + return null } - setExportState({ - isExporting: true - }) + + setExportState({ isExporting: true }) + 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' }) - return + setExportState({ isExporting: false }) + return null } try { const notion = new Client({ auth: notionApiKey }) - const markdown = await topicToMarkdown(topic) - const allBlocks = await convertMarkdownToNotionBlocks(markdown) const blockPages = splitNotionBlocks(allBlocks) if (blockPages.length === 0) { @@ -223,25 +293,33 @@ export const exportTopicToNotion = async (topic: Topic) => { // 创建主页面和子页面 let mainPageResponse: any = null let parentBlockId: string | null = null + for (let i = 0; i < blockPages.length; i++) { - const pageTitle = topic.name const pageBlocks = blockPages[i] // 导出进度提示 - window.message.loading({ - content: i18n.t('message.loading.notion.exporting_progress', { - current: i + 1, - total: blockPages.length - }), - key: 'notion-export-progress' - }) + if (blockPages.length > 1) { + window.message.loading({ + content: i18n.t('message.loading.notion.exporting_progress', { + current: i + 1, + total: blockPages.length + }), + key: 'notion-export-progress' + }) + } else { + window.message.loading({ + content: i18n.t('message.loading.notion.preparing'), + key: 'notion-export-progress' + }) + } if (i === 0) { + // 创建主页面 const response = await notion.pages.create({ parent: { database_id: notionDatabaseID }, properties: { [store.getState().settings.notionPageNameKey || 'Name']: { - title: [{ text: { content: pageTitle } }] + title: [{ text: { content: title } }] } }, children: pageBlocks @@ -249,6 +327,7 @@ export const exportTopicToNotion = async (topic: Topic) => { mainPageResponse = response parentBlockId = response.id } else { + // 追加后续页面的块到主页面 if (!parentBlockId) { throw new Error('Parent block ID is null') } @@ -259,63 +338,71 @@ export const exportTopicToNotion = async (topic: Topic) => { } } - window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-export-progress' }) + const messageKey = blockPages.length > 1 ? 'notion-export-progress' : 'notion-success' + window.message.success({ content: i18n.t('message.success.notion.export'), key: messageKey }) return mainPageResponse } catch (error: any) { window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-export-progress' }) return null } finally { - setExportState({ - isExporting: false - }) + setExportState({ isExporting: false }) } } -export const exportMarkdownToNotion = async (title: string, content: string) => { - const { isExporting } = store.getState().runtime.export +export const exportMessageToNotion = async (title: string, content: string, message?: Message) => { + const { notionExportReasoning } = store.getState().settings - if (isExporting) { - window.message.warning({ content: i18n.t('message.warn.notion.exporting'), key: 'notion-exporting' }) - return + const notionBlocks = await convertMarkdownToNotionBlocks(content) + + if (notionExportReasoning && message) { + const thinkingContent = getThinkingContent(message) + if (thinkingContent) { + const thinkingBlocks = await convertThinkingToNotionBlocks(thinkingContent) + if (notionBlocks.length > 0) { + notionBlocks.splice(1, 0, ...thinkingBlocks) + } else { + notionBlocks.push(...thinkingBlocks) + } + } } - setExportState({ isExporting: true }) + return executeNotionExport(title, notionBlocks) +} - const { notionDatabaseID, notionApiKey } = store.getState().settings +export const exportTopicToNotion = async (topic: Topic) => { + const { notionExportReasoning } = store.getState().settings - if (!notionApiKey || !notionDatabaseID) { - window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' }) - return - } + // 获取话题消息 + const topicRecord = await db.topics.get(topic.id) + const topicMessages = topicRecord?.messages || [] - try { - const notion = new Client({ auth: notionApiKey }) - const notionBlocks = await convertMarkdownToNotionBlocks(content) + // 创建话题标题块 + const titleBlocks = await convertMarkdownToNotionBlocks(`# ${topic.name}`) - if (notionBlocks.length === 0) { - throw new Error('No content to export') + // 为每个消息创建blocks + const allBlocks: any[] = [...titleBlocks] + + for (const message of topicMessages) { + // 将单个消息转换为markdown + const messageMarkdown = messageToMarkdown(message) + const messageBlocks = await convertMarkdownToNotionBlocks(messageMarkdown) + + if (notionExportReasoning) { + const thinkingContent = getThinkingContent(message) + if (thinkingContent) { + const thinkingBlocks = await convertThinkingToNotionBlocks(thinkingContent) + if (messageBlocks.length > 0) { + messageBlocks.splice(1, 0, ...thinkingBlocks) + } else { + messageBlocks.push(...thinkingBlocks) + } + } } - const response = await notion.pages.create({ - parent: { database_id: notionDatabaseID }, - properties: { - [store.getState().settings.notionPageNameKey || 'Name']: { - title: [{ text: { content: title } }] - } - }, - children: notionBlocks as any[] - }) - - window.message.success({ content: i18n.t('message.success.notion.export'), key: 'notion-success' }) - return response - } catch (error: any) { - window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-error' }) - return null - } finally { - setExportState({ - isExporting: false - }) + allBlocks.push(...messageBlocks) } + + return executeNotionExport(topic.name, allBlocks) } export const exportMarkdownToYuque = async (title: string, content: string) => { @@ -464,7 +551,6 @@ export const exportMarkdownToObsidian = async (attributes: any) => { * @param fileName * @returns */ - function transformObsidianFileName(fileName: string): string { const platform = window.navigator.userAgent const isWindows = /win/i.test(platform) @@ -482,7 +568,7 @@ function transformObsidianFileName(fileName: string): string { } else if (isMac) { // Mac 的清理 sanitized = sanitized - .replace(/[/:\u0020-\u007E]/g, '') // 移除无效字符 + .replace(/[<>:"\\/\\|?*]/g, '') // 移除无效字符 .replace(/^\./, '_') // 避免以句点开头 } else { // Linux 或其他系统 @@ -504,14 +590,27 @@ function transformObsidianFileName(fileName: string): string { return sanitized } -export const exportMarkdownToJoplin = async (title: string, content: string) => { - const { joplinUrl, joplinToken } = store.getState().settings + +export const exportMarkdownToJoplin = async (title: string, contentOrMessages: string | Message | Message[]) => { + const { joplinUrl, joplinToken, joplinExportReasoning } = store.getState().settings if (!joplinUrl || !joplinToken) { window.message.error(i18n.t('message.error.joplin.no_config')) return } + let content: string + if (typeof contentOrMessages === 'string') { + content = contentOrMessages + } else if (Array.isArray(contentOrMessages)) { + content = messagesToMarkdown(contentOrMessages, joplinExportReasoning) + } else { + // 单条Message + content = joplinExportReasoning + ? messageToMarkdownWithReasoning(contentOrMessages) + : messageToMarkdown(contentOrMessages) + } + try { const baseUrl = joplinUrl.endsWith('/') ? joplinUrl : `${joplinUrl}/` const response = await fetch(`${baseUrl}notes?token=${joplinToken}`, { diff --git a/src/renderer/src/utils/messageUtils/find.ts b/src/renderer/src/utils/messageUtils/find.ts index a2d804a10..fa6c8cd66 100644 --- a/src/renderer/src/utils/messageUtils/find.ts +++ b/src/renderer/src/utils/messageUtils/find.ts @@ -133,7 +133,7 @@ export const getCitationContent = (message: Message): string => { return citationBlocks .map((block) => formatCitationsFromBlock(block)) .flat() - .map((citation) => `[${citation.number}] [${citation.url}](${citation.title || citation.url})`) + .map((citation) => `[${citation.number}] [${citation.title || citation.url}](${citation.url})`) .join('\n\n') } From b2d10b7a6b05e84d74b89c5bc2accf5c3d61b1af Mon Sep 17 00:00:00 2001 From: Murphy <69335326+MurphyLo@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:39:56 +0800 Subject: [PATCH 10/10] fix: add blank lines between reasoning summary parts (#6827) Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com> --- .../src/providers/AiProvider/OpenAIResponseProvider.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts index d4375b08d..e59b3fabe 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIResponseProvider.ts @@ -571,6 +571,16 @@ export abstract class BaseOpenAIProvider extends BaseProvider { if (time_first_token_millsec === 0) { time_first_token_millsec = new Date().getTime() } + // Insert separation between summary parts + if (thinkContent.length > 0) { + const separator = '\n\n' + onChunk({ + type: ChunkType.THINKING_DELTA, + text: separator, + thinking_millsec: new Date().getTime() - time_first_token_millsec + }) + thinkContent += separator + } break case 'response.reasoning_summary_text.delta': onChunk({